Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help


status: evidence-map blueprint_revision: 5 pins: pyproject.toml and uv.lock regenerated: null reviewed: 2026-09-13

Python libraries — capability map

Current binding: blueprint revision 5. The original survey and version/probe receipts below retain their historical scope; they were not regenerated in this update. Earlier revision comparisons and parked recommendations are historical. The revision-4 review and ADR-0039–ADR-0048 determine the corrected design bindings below. Platform integration remains Proposed; interface and probe evidence are labeled at their original scope. No Python behavioral corpus was re-audited by this revision.

Current bindingDecision and limitEvidence / authority
Arrow admissionExplicit recursive contract validation, registration/loss checks and typed outcomesADR-0039/0043; blueprint §21.1
Pyomo affine loweringLinearExpression only for a proven affine-in-variables body; constants have degree 0ADR-0047; review R4-09
Guarded and opaque bindingsActual writer/solver capability and numerical-policy preflight; Expr_if construction is not lazy-evaluation proofADR-0043/0047; blueprint §21.2
Bounded streamsSafe owning buffers and measured bounded coalescing; no whole-dataset transfer copyADR-0047/0048; R-24

Fourth and final map in the series, completing blueprint §3.3. The Rust side is covered by arrow-rust.md, datafusion-rust.md and supporting-rust-libraries.md; this covers line 331 — the Python row — plus the adjacent libraries the Python boundary actually depends on.

Compiled 2026-09-13.


0. Purpose, sourcing constraint, and evidence rules

0.1 What this document is

Per cluster: what each library can actually do, which APIs the blueprint’s requirements land on, what we deliberately will not use, and where the blueprint assumes a capability that does not exist or is under-specified. A second pass asks what these libraries offer that the blueprint has not claimed, which would improve alignment with design_principles/DATA_MODEL_DESIGN_CHARTER.md (DM-01…DM-60, gates G1–G7).

This boundary deserves the scrutiny. Everything crossing it was already typed once, in Rust, against a registry. §4.4 asserts that Python consumers “register matching pyarrow.ExtensionType classes”; §21.5 asserts typed attrs classes with no Any fields. Those are DM-42 and G2 claims about a language with no compiler to enforce them — so they were measured rather than assumed.

0.2 Sourcing constraint

Nothing is taken from existing repository documentation. This is both an instruction and a fact: docs/library_ref/ has been deleted, taking the former pyomo.md, pyarrow.md, attrs.md, cattrs.md and msgspec.md references with it. Four fresh lanes:

LaneMarkerWhat it is
locally generated API surface[api:lib@version]The rustdoc analogue: a script walking each library’s public API via inspect/importlib, emitting JSON — modules, classes, callables, signatures, __all__, MRO. Authoritative for “does this exist, with this signature, at this version”.
locally run probes[probe]The arrow_probe.rs analogue: scripts that measure behaviour the blueprint asserts. Eight were run; their output is quoted inline.
context7[c7:/id]Idiom and intent
PyPI / upstream[pypi:pkg], [docs:url]Versions, release metadata, dependency requirements

Checked mechanically: zero [ref:] citations, and every marker resolves to a ledger row. A claim carrying no marker is a defect.

0.3 Status vocabulary

leverage · build on top · confirmed · refined / erratum · adopt / evaluate / reject · open item. Pass-2 recommendations are disciplined by charter §G and DM-58: a recommendation naming no principle and replacing no identified hand-written work does not appear.

0.4 A correction to this document’s own reconnaissance

An early probe of the neighbouring environment suggested that an unregistered pse.* extension type would surface in Python as pyarrow.UnknownExtensionType. Measurement showed otherwise (§3, PROBE 1): it deserializes to its plain storage type. UnknownExtensionType exists in the API but is not what this path produces. The corrected finding is materially different — and better — because the degradation is silent at the type level but detectable through retained field metadata. Recorded here because the wrong version briefly informed the plan.


1. Version anchors and environment receipts

1.1 Anchors — every library at its current PyPI release

Owner’s instruction: use the latest release of every library, not the versions §3.1 names. All eight verified against PyPI at compile time [pypi:*].

LibraryThis mapBlueprint §3.1Gap
Pyomo6.10.1“6.9+”open lower bound → pin
pyarrow25.0.1“15+”ten majors behind → pin
attrs26.1.0unpinnedno pin at all
cattrs26.2.0unpinnedno pin at all
msgspec0.21.1unpinnedno pin at all
numpy2.5.3unpinnedarrives via Pyomo/pyarrow
scipy1.18.1unpinnedarrives via Pyomo
idaes-pse2.12.0“2.10 for parity tests only”2.10 → 2.12
pint0.26.1excluded by §3.3see §5 — required, not optional

1.2 Two interpreters, and why

idaes-pse 2.12.0 classifies Python 3.10–3.13 only [pypi:idaes-pse]; its requires_python is unset, so pip would not block a 3.14 install, but it is untested there. Rather than guess, the extraction uses two environments:

py314/   CPython 3.14.7   pyomo 6.10.1, pyarrow 25.0.1, attrs 26.1.0, cattrs 26.2.0,
                          msgspec 0.21.1, numpy 2.5.3, scipy 1.18.1 (+ pint 0.26.1, see §5)
py313/   CPython 3.13.12  the same set plus idaes-pse 2.12.0
                          (pulls pint 0.26.1, pydantic 2.13.5, sympy 1.14.0, networkx 3.6.1)

Every library ==-pinned, mirroring the =-pin discipline §3.1 adopted for Rust. IDAES imports cleanly on 3.13.12 [probe].

This split is itself a §3.1 finding. §3.1 says “Python 3.11+”; IDAES currently caps the interpreter at 3.13 for anything that must run the parity harness (§24). The platform’s own Python package can target 3.14; the parity environment cannot.

1.3 Extraction receipt

DumpLibraryVersionInterpreterModulesItems
pyarrow.jsonpyarrow25.0.13.14.7281445
pyomo.jsonpyomo6.10.13.14.75055898
attrs.jsonattrs26.1.03.14.7661
cattrs.jsoncattrs26.2.03.14.720416
msgspec.jsonmsgspec0.21.13.14.7791
numpy.jsonnumpy2.5.33.14.7692414
scipy.jsonscipy1.18.13.14.71561772
idaes.jsonidaes2.12.03.13.121912762

Scripts, both requirement sets and all probe programs are committed under docs/capability-maps/evidence/python/ so every [api:…] and [probe] marker is re-derivable.


2. pyarrow — PyCapsule interop and the stream boundary (P1)

Role: §21.1 — “every table crosses as an Arrow C stream (__arrow_c_stream__), never as row objects”; D12 — “one adapter consumes a CanonicalMathProblem bundle over the Arrow C stream interface”.

Capability inventory

CapabilitySurfaceProvenance
PyCapsule producer/consumerTable.__arrow_c_stream__, RecordBatchReader.__arrow_c_stream__, DataType.__arrow_c_schema__, ExtensionType.__arrow_c_schema__[api:pyarrow@25.0.1]
Stream construction from any producerpa.RecordBatchReader.from_stream(obj) — consumes anything exposing __arrow_c_stream__, regardless of which library produced it[api:pyarrow@25.0.1], [probe]
Core containersTable, RecordBatch, RecordBatchReader, Schema, Field, ChunkedArray, Array, ExtensionArray, ExtensionScalar[api:pyarrow@25.0.1]
IPCpa.ipc.new_stream, pa.ipc.open_stream, file variants[api:pyarrow@25.0.1]

What the blueprint binds to

Blueprint requirementAPIStatus
§21.1 tables cross as __arrow_c_stream__, never as row objectspresent on Table and RecordBatchReaderconfirmed [api:pyarrow@25.0.1]
§21.1 “object exposing __arrow_c_stream__ → pyarrow / polars / datafusion-python”RecordBatchReader.from_stream consumes the protocol genericallyconfirmed — the capsule protocol is implementation-neutral, so the Rust side need not know which Python Arrow library the consumer uses
§21.1 “Transfer is one bundle, not per-node calls”a stream per named tableconfirmed as the right shape — see DM-37 below

Pass 2 — alignment opportunities

ItemDM / gateRecommendation
Cross the boundary as a stream, not a materialised TableDM-37 (cross expensive boundaries in coarse, typed units)adopt, and state it. §21.1 already says “one bundle”; the sharper rule is that each table should cross as a RecordBatchReader the consumer drains, not a fully materialised Table. One FFI crossing per relation either way, but the streaming form does not require the whole relation resident on both sides simultaneously.
Keep pyarrow out of the contractDM-41 (adapters mechanical), DM-57adopt. The capsule protocol is the contract; pyarrow is one implementation of it. §21.5’s typed classes should be built from the streamed batches, never from pyarrow-specific types in their field annotations — otherwise the Python contract layer is pinned to one Arrow implementation.

Gaps and risks

  • datafusion (Python) lagged the Rust engine in the environment surveyed — 54.0.0 against the Rust side’s 55.1.0. It is only a consumer of the capsule protocol, so version skew is tolerable by design; worth stating explicitly, since §21.1 names it and a reader could infer the versions must match.
  • pyarrow 25.0.1 against §3.1’s “pyarrow 15+” is a ten-major gap. Nothing observed here requires 15; but an unpinned floor means CI could resolve anything in that range, and extension-type behaviour (§3) is exactly the kind of thing that moved over those releases. Pin it.

3. pyarrow — extension types, metadata, and the loss profile (P2)

The cluster that decides whether the semantic typing built in Rust survives into Python. DM-06, DM-42, DM-44, G2.

Capability inventory

CapabilitySurfaceProvenance
Extension basepa.ExtensionType(storage_type, extension_name); bases BaseExtensionType → DataType[api:pyarrow@25.0.1]
Required overrides__arrow_ext_serialize__(self) -> bytes, __arrow_ext_deserialize__(cls, storage_type, serialized)[api:pyarrow@25.0.1]
Optional overrides__arrow_ext_class__, __arrow_ext_scalar_class__[api:pyarrow@25.0.1]
Registrypa.register_extension_type(instance), pa.unregister_extension_type(name)[api:pyarrow@25.0.1]
Inspectionextension_name, storage_type, wrap_array, field, num_buffers, byte_width[api:pyarrow@25.0.1]
Unresolved markerpa.UnknownExtensionType exists as a type[api:pyarrow@25.0.1]
Array/scalarpa.ExtensionArray.from_storage(type, storage), pa.ExtensionScalar[api:pyarrow@25.0.1]

Measured: the loss profile (PROBE 1 and 2)

A pse.semantic_id-shaped extension over binary(16), with pse.* schema and field metadata, round-tripped through IPC and through __arrow_c_stream__:

PROBE 1  extension type through IPC
   registered   -> field type: SemanticId          | extension_name: pse.semantic_id
   UNregistered -> field type: FixedSizeBinaryType | extension_name: None | storage: fixed_size_binary[16]
   unregistered field metadata retains ARROW:extension:name: True

PROBE 2  metadata across IPC and PyCapsule
   IPC  schema md : b'rel-1'      IPC  field md : b'qt-demo'
   caps schema md : b'rel-1'      caps field md : b'qt-demo'
   caps id type   : SemanticId
Blueprint claimVerdict
§4.4 “Python consumers register matching pyarrow.ExtensionType classes; unknown consumers see storage types”confirmed exactly. With a registered class the type round-trips as SemanticId; without one it becomes fixed_size_binary[16].
§4.3 metadata “preserved through IPC/Parquet” — the Python halfconfirmed for IPC and for the PyCapsule path, at both schema and field level.
§21.1 the capsule path preserves extension typesconfirmed — caps id type: SemanticId.
This document’s own earlier claim that unregistered types surface as UnknownExtensionTyperefuted — they surface as the plain storage type.

Pass 2 — alignment opportunities

ItemDM / gateRecommendation
Detect degradation via retained ARROW:extension:name metadataDM-42, G2, DM-59adopt — the key finding of this cluster. Loss of an extension type is silent at the type level: a consumer sees fixed_size_binary[16] with no error. But the field metadata still carries ARROW:extension:name [probe]. So the Python contract layer can assert, per column, “this field claims extension X — is X registered?” and fail loudly if not. That converts an invisible G2 degradation into a detectable, testable condition, and it costs one check at the boundary.
Ship and register the pse.* extension classes as part of the Python packageDM-44 (extensions complete, versioned, conformance-testable)adopt. §4.4 says consumers register matching classes but does not say who ships them. If each consumer writes its own, the serialization contract forks. The ten pse.* classes should be generated from the same registry that generates the Rust side (§4.2) and shipped in python/pse/, with a conformance test mirroring the Rust one.
Declare the loss profile per column in the bundleG7, DM-42adopt. §21.1’s manifest should record, per table, which fields carry extension types — so a consumer can verify coverage rather than discover a degradation by its absence.
__arrow_ext_scalar_class__ for pse.quantity_valueDM-06evaluate. A struct-backed extension exposed as a bare struct scalar loses its meaning at the point of use. A custom scalar class is the Python-side way to keep it. Only worth it for types a human actually inspects in a notebook.
pa.UnknownExtensionType—reject as a mechanism. It exists, but is not what the IPC/capsule path produces [probe]; relying on it would be relying on a path we measured not to occur.

Gaps and risks

  • Registration is global and process-wide. register_extension_type mutates a global registry; a second registration of the same name raises. A library that registers on import interacts badly with test isolation and with any host process that also registers pse.*. Recommend an idempotent register-once helper in python/pse/.
  • Extension metadata is versioned by nothing. __arrow_ext_serialize__ returns opaque bytes; if the Rust-side Metadata shape changes, the Python __arrow_ext_deserialize__ must change in lockstep or silently misparse. Same gap the Arrow map recorded for the Rust side (DM-44, DM-51) — and it is worse here, because the two implementations are in different languages and different repositories’ release cycles.

4. Pyomo — model construction and expression lowering (P3)

Role: §21.2 steps 1–2 — the adapter algorithm; §18.5’s Pyomo binding row; D12.

Capability inventory

CapabilitySurfaceProvenance
Model and componentsConcreteModel, Var, Param(mutable=True), Constraint, Objective, Set, RangeSet, Suffix, Block[api:pyomo@6.10.1]
Affine formpyomo.core.expr.numeric_expr.LinearExpression(constant, linear_coefs, linear_vars)[api:pyomo@6.10.1], [probe]
Conditionalpyo.Expr_if(IF=…, THEN=…, ELSE=…) → Expr_ifExpression[probe]
External functionspyo.ExternalFunction(library=…, function=…) → AMPLExternalFunction; calling it yields ExternalFunctionExpression[probe]
Solvepyo.SolverFactory(name), .available(False), .solve(model, …)[api:pyomo@6.10.1], [probe]

Measured (PROBE 3)

   LinearExpression      : LinearExpression | value: 2.0
     nargs 3 | polynomial_degree: 1
   Expr_if               : Expr_ifExpression | value: 1.0
   ExternalFunction decl : AMPLExternalFunction | callable in expr: ExternalFunctionExpression
   (warning) Defining AMPL external function, but cannot locate specified library "libpse_kernels.so"
§21.2 step 2 requirementVerdict
Affine loweringQualified: the probe constructs a truly linear expression. Arbitrary Affine children may be nonlinear; use LinearExpression only after proving affine dependence on active variables (ADR-0047).
Conditional to Expr_ifInterface/probe scope: expression construction works; guarded evaluation must be qualified for the actual Pyomo writer and selected solver/numerical policy (ADR-0047).
“KernelCall → … otherwise ExternalFunction(library, function)”confirmed as a declaration; see the risk below
“ImplicitRef never appears (implicit systems stay as equations)”platform rule, nothing in Pyomo to confirm or refute

Pass 2 — alignment opportunities

ItemDM / gateRecommendation
A missing external-function library is a warning, not an errorG7 (truthful capability claims), DM-43, DM-30adopt a hard check. ExternalFunction with an unresolvable library emitted a warning and still produced a usable expression object [probe]; the failure surfaces only at solve time, from the solver, far from the cause. §21.4 already requires an end-to-end test per pyomo_external_function kernel — the stronger rule is that the adapter must verify the library resolves before building the model, and raise capability.backend (§23.2) with the kernel id if not. A warning on stderr is not a typed failure.
polynomial_degree() as a lowering assertionDM-24, DM-53Adopt with classification: compare with the expected degree, 0 for constants and 1 for a nonconstant affine-in-variables form. An Affine opcode alone does not justify degree 1 (blueprint §21.2).
Build through Blocks mirroring compiled relation structureDM-18reject. Tempting for readability, but D12 is explicit that the adapter “builds no IDAES classes” and “engineering names live in the source map”. Blocks would recreate a hierarchy the design deliberately flattened to ordinals.

Gaps and risks

  • Param(mutable=True) is required for §21.2’s parameters to be updatable between solves; an immutable Param is folded into expressions at construction. The blueprint says mutable=True — correct, and worth keeping as a generated-code invariant rather than a convention.
  • Pyomo’s expression system is large and has several near-equivalent spellings for the same mathematics (LinearExpression vs. a sum of products). Since the adapter is generated from the IR, the choice should be made once in the generator, not per operator.

5. Pyomo — units, suffixes, solve, and results (P4)

This cluster contains the map’s sharpest finding: a direct contradiction between §21.2 and §3.3.

The pint contradiction

§21.2 step 1 requires “units attached from the unit set so that assert_units_consistent passes”. §3.3’s closing line states: “Explicitly not added: … a Python-side units library.”

Measured [probe], on a clean environment with the §3.3-compliant dependency set (no pint):

   m2.T = pyo.Var(units=u.K, initialize=320.0)
   pyomo.common.errors.DeferredImportError: The pint module (an optional Pyomo dependency)
   failed to import: ModuleNotFoundError: No module named 'pint'

Pyomo’s units system is pint. pyomo.core.base.units_container defers to pint_module and raises on attribute access when it is absent [probe]. So §21.2’s requirement cannot be satisfied under §3.3’s exclusion. This is not a transitive-dependency inconvenience — the two statements are incompatible as written.

Recommended resolution (and the basis for an amendment): §3.3’s exclusion is about authority, not about presence. Reword it to say that no Python-side units library is an authority for quantities — §8’s dynamic registry remains sole authority — while acknowledging that pint is required by Pyomo’s units machinery and is therefore present in the adapter and parity environments. The boundary rule then becomes enforceable: pint may validate what the adapter emits; it may never define a quantity, and no pse relation may be derived from a pint object.

Capability inventory and measurements (PROBE 4, 5)

CapabilitySurfaceProvenance
Units containerpyo.units (PyomoUnitsContainer), backed by pint[api:pyomo@6.10.1], [probe]
Consistency assertionpyomo.util.check_units.assert_units_consistent(obj) — not in pyomo.environ[api:pyomo@6.10.1], [probe]
Culprit identificationpyomo.util.check_units.identify_inconsistent_units(block) → the offending components[probe]
Relatedassert_units_equivalent, check_units_equivalent, UnitsError, InconsistentUnitsError[api:pyomo@6.10.1]
Suffixespyo.Suffix(direction=Suffix.EXPORT | IMPORT); scaling_factor, dual, ipopt_zL_out, ipopt_zU_out[probe]
PROBE 4   consistent model      : PASSES
          inconsistent model    : raises InconsistentUnitsError ("kelvin not compatible with …")
          identify_inconsistent_units -> ['bad']        <- names the culprit constraint

PROBE 5   Suffix EXPORT/IMPORT  : declared ok; scaling_factor[x] = 0.001
          SolverFactory(ipopt) available: False
          SolverFactory(cbc)   available: False
          SolverFactory(glpk)  available: False
Blueprint requirementStatus
§21.2 step 1 “assert_units_consistent passes”confirmed working — with pint present, and erratum: the function is in pyomo.util.check_units, not pyomo.environ as the phrasing implies
§21.2 step 3 scaling_factor suffix exportconfirmed
§21.2 step 4 dual, ipopt_zL_out, ipopt_zU_out importdeclaration confirmed; round-trip [UNVERIFIED] — no solver is installed in this environment, so the probe could not complete the loop. Recorded as unverified rather than asserted.

Pass 2 — alignment opportunities

ItemDM / gateRecommendation
Use identify_inconsistent_units, not a bare assertDM-47 (structured evidence), DM-30, §23.2adopt. assert_units_consistent raises; identify_inconsistent_units returns the offending components [probe]. §23.2’s compile.math class exists to name the culprit — “unit inconsistency” with a component list is a finding; an exception with a message is not.
Units as a cross-check, never a sourceDM-02 (one authority per fact), G1adopt as the boundary rule. The unit set crosses in the bundle from §8’s registry; Pyomo/pint validates that what the adapter built matches. If the two disagree, the registry is right and the adapter has a bug. Making that direction explicit is what keeps pint from becoming a second authority.
Solver availability probed before the model is builtDM-43, G7adopt. SolverFactory(name).available(False) is a cheap pre-flight [probe]. §18.3 already does a capability probe for ipopt_has_linear_solver; the same discipline belongs at the Pyomo boundary so a missing solver is a declared capability failure rather than a late crash.

6. Pyomo — ecosystem tools and the ASL boundary (P5)

Role: §21.3 — “parity testing, and the Pyomo ecosystem: parmest, PyROS, GDPopt, pyomo.dae utilities, and IncidenceGraphInterface-based cross-checks”; §21.4 opaque kernels and ASL; §18.9’s capability matrix.

Availability — all confirmed present at 6.10.1 [probe]

ModulePublic namesBlueprint use
pyomo.contrib.parmest.parmest48§21.3 parameter estimation; §19.4
pyomo.contrib.pyros22§21.3 robust optimization; §19.8 uncertainty
pyomo.contrib.gdpopt18§21.3, §18.7’s GDP solve plan
pyomo.dae11§21.3 DAE utilities; §13.4 discretization cross-checks
pyomo.contrib.incidence_analysis17§21.3 cross-checks of native structural results (§15.3)
pyomo.contrib.iis4not named in the blueprint — see below

IncidenceGraphInterface resolves at pyomo.contrib.incidence_analysis.interface [probe], confirming §21.3’s naming.

Pass 2 — alignment opportunities

ItemDM / gateRecommendation
pyomo.contrib.iis for infeasibility explanationDM-47, DM-30evaluate. §15.5 specifies a hand-built minimal-intractable-system procedure (three elasticization rounds plus a deletion filter). Pyomo ships an IIS facility that solves the same problem. Since the Pyomo adapter already exists for parity, this is available as a cross-check oracle for the native MIS implementation at near-zero cost — the same role §21.3 already assigns to IncidenceGraphInterface. Not a replacement: §15.5’s version runs natively, over case overlays, without model cloning.
Treat every ecosystem tool as a declared capability with a versionDM-43, G7, DM-31adopt. §18.9’s capability matrix says which operators each backend supports; it does not record which version of parmest/PyROS/GDPopt a result came from. These are pyomo.contrib packages — explicitly lower-stability than Pyomo’s core — and their behaviour changes between releases. A run that used one must record it in runtime.runs.environment (§20.3) or the result is not reproducible.
Cross-check native structural results against IncidenceGraphInterface in CI, not ad hocDM-53 (verify equivalence across representations), DM-54adopt. §21.3 describes this as a use of the adapter; making it a standing parity test over the golden snapshots (§24) turns it from an available technique into a regression control.

Gaps and risks

  • pyomo.contrib.* is a contrib namespace. Five of the six tools §21.3 relies on live there. That is a stability profile the blueprint should state, because §18.9’s matrix currently reads as though these are equivalent in standing to the native and NL backends. They are not: they are the least stable surface in the entire dependency set.
  • §21.4’s rule that “Python callback functions are not used for solves because ASL-based solvers cannot execute them” is a correct and important constraint, and the ExternalFunction probe (§4) shows why it must be enforced early: the object constructs happily either way, and only the solver notices.

7. attrs + cattrs — the typed contract layer (P6)

Role: §21.5 — “typed attrs classes with explicit converters at the boundary, cattrs for structuring, no Any fields, and no Python-side model classes that duplicate relations.”

Note: §21.5 cites docs/library_ref/contract_substrate_discipline.md for this discipline. That file and its directory have been deleted — so the referenced definition is currently unreadable (erratum E1). What follows measures the discipline’s stated properties directly.

Capability inventory

CapabilitySurfaceProvenance
Class definitionattrs.define, attrs.frozen, attrs.field, attrs.mutable[api:attrs@26.1.0]
Field machineryvalidators, converters, defaults, kw_only, alias, metadata[api:attrs@26.1.0]
Introspectionattrs.fields(cls), attrs.asdict, attrs.astuple, attrs.evolve[api:attrs@26.1.0], [probe]
Structuringcattrs.Converter, structure, unstructure, register_structure_hook, register_unstructure_hook[api:cattrs@26.2.0]
Strictness optionsConverter(forbid_extra_keys=…, detailed_validation=…); detailed validation is on by default since 22.1.0[probe], [c7:/python-attrs/cattrs]
ErrorsClassValidationError (exception group of sub-errors); ForbiddenExtraKeysError[probe], [c7:/python-attrs/cattrs]
Error renderingcattrs.transform_error(exc, format_exception=…) → list of per-field messages[c7:/python-attrs/cattrs]
Global strictness idiomregister_structure_hook_factory(has, …) + cattrs.gen.make_dict_structure_fn(cl, c, _cattrs_forbid_extra_keys=True)[c7:/python-attrs/cattrs]

Measured (PROBE 6)

   structure happy path  : VarRow(ordinal=1, semantic_id=b'…', lower=0.0)
   unknown key           : ACCEPTED (silently dropped)
   forbid_extra_keys     : rejected -> ClassValidationError
   wrong type            : rejected -> ClassValidationError
   detailed_validation   : ClassValidationError | sub-errors: 1
   Any field             : ACCEPTED  (nothing in cattrs forbids it)
   attrs introspection   : fields() -> ['ordinal', 'semantic_id', 'lower']
§21.5 requirementVerdict
“typed attrs classes with explicit converters at the boundary”confirmed available
“cattrs for structuring”confirmed
“no Any fields”erratum — unenforced. cattrs accepts an Any field and structures whatever it is given [probe]. §21.5 states this as a property of the design; it is a convention with nothing behind it.
Implicit expectation that structuring is strictrefuted by default. A default cattrs.Converter() silently drops unknown keys [probe].

Pass 2 — alignment opportunities

ItemDM / gateRecommendation
Forbid extra keys at every boundaryDM-42 (reject silent degradation), G2, G3adopt — the highest-value item in this cluster. Measured: the default silently discards unknown keys [probe], and upstream states the same intent — “by default, cattrs ignores extra keys”; forbid_extra_keys “enforces a stricter contract… especially when fields have default values” [c7:/python-attrs/cattrs]. At a boundary carrying a versioned contract, an unknown key is evidence of a version mismatch; dropping it converts detectable schema drift into silent data loss. Two spellings work: Converter(forbid_extra_keys=True) [probe], or the upstream idiom applying it to every attrs class at once — register_structure_hook_factory(has, lambda cl: make_dict_structure_fn(cl, c, _cattrs_forbid_extra_keys=True)), raising ForbiddenExtraKeysError [c7:/python-attrs/cattrs]. Prefer the factory: it cannot be forgotten on a new class.
Consume the sub-errors — and do not disable detailed validationDM-47, DM-30adopt, with a correction to this document’s own first pass. Detailed validation is on by default since cattrs 22.1.0 [c7:/python-attrs/cattrs], so the work is not enabling it — it is (a) never passing detailed_validation=False for speed, and (b) actually consuming the sub-errors. ClassValidationError is an exception group carrying one per bad field [probe], and cattrs.transform_error() renders it as a list of per-field messages [c7:/python-attrs/cattrs] — a ready-made bridge to §23.2’s structured findings. Taking only the first message discards exactly the structure that makes a validation failure actionable.
A governance check for AnyDM-06, G3adopt. Since nothing at runtime forbids Any, the rule needs a lint: walk attrs.fields() of every contract class at import or test time and fail on Any/bare dict/bare list. attrs.fields() makes this ~10 lines [probe], and it is the only way §21.5’s claim becomes true.
Generate the contract classes from the registryDM-52 (generate mechanical artifacts from shared contracts), DM-23adopt. §4.2 already generates nine artifacts per relation from RelationSpec, including a serde_arrow schema for the Rust side. The Python contract classes are the same projection in another language. Hand-writing them re-expresses the column list a second time — which §4.2 calls a governance failure — and guarantees drift. This is the single largest structural opportunity in the Python surface.
attrs.frozen for anything crossing the boundaryDM-13, DM-20adopt. Bundle rows are observations of an immutable snapshot; frozen classes make that structural and keep inspection non-mutating (DM-20).

8. msgspec — wire contracts, constraints, and schema generation (P7)

Role: §21.5 names msgspec among the Python contracts; §20.2’s manifest is JSON. The blueprint never says what msgspec is for — §9 adjudicates that.

Capability inventory

CapabilitySurfaceProvenance
Typed structsmsgspec.Struct with frozen=, forbid_unknown_fields=, gc=, tag=, rename=[api:msgspec@0.21.1], [probe]
Codecsmsgspec.json, msgspec.msgpack, msgspec.yaml, msgspec.toml — encode / decode(buf, type=T)[api:msgspec@0.21.1]
Constraintsmsgspec.Meta(gt=, ge=, lt=, le=, min_length=, max_length=, pattern=, …) via Annotated[probe]
Validation errorsmsgspec.ValidationError with JSON-path location[probe]
Schema generationmsgspec.json.schema(T) → JSON Schema; schema_components for multiple types[probe]
Conversionmsgspec.convert, msgspec.to_builtins, msgspec.Raw, msgspec.structs.*[api:msgspec@0.21.1]

Measured (PROBE 7)

   encode                : b'{"name":"stoichiometry","rows":12,"ratio":1.0}'
   decode round-trip     : Rel(name='stoichiometry', rows=12, ratio=1.0)
   unknown field         : rejected -> ValidationError | Object contains unknown field `BOGUS`
   wrong type            : rejected -> ValidationError | Expected `int`, got `str` - at `$.rows`
   Meta(gt=0) violation  : rejected -> ValidationError | Expected `float` > 0.0 - at `$.factor`
   json.schema(Rel)      : {'$ref': '#/$defs/Rel', '$defs': {'Rel': {'title': 'Rel', 'type': 'object',
                            'properties': {...}, 'required': ['name','rows'],
                            'additionalProperties': False}}}

Three properties matter for this design, and all three were measured rather than assumed:

  1. Errors carry a location. at $.rows, at $.factor — a JSON path to the offending value. §23.2 requires failures that carry the thing that failed; msgspec supplies it natively.
  2. Constraints are declarative and enforced at decode. Meta(gt=0.0) on a scaling factor is checked at the boundary, not by hand-written code afterwards.
  3. It generates JSON Schema, with additionalProperties: False derived from forbid_unknown_fields.

Pass 2 — alignment opportunities

ItemDM / gateRecommendation
msgspec owns the §20.2 manifestDM-42, DM-47, DM-44adopt. The manifest is JSON on disk, versioned, read by both Rust and Python, and the place where a malformed or drifted artifact must be caught. Struct(frozen=True, forbid_unknown_fields=True) plus Meta constraints gives strict decode with located errors, which is exactly what §20.5’s “a snapshot whose schema registry fingerprint is unknown is rejected” needs.
msgspec.json.schema() vs §4.2’s generated authoring JSON SchemaDM-23 (derived representations traceable and non-competing), DM-52evaluate carefully — this is a competing-authority risk, not a free win. §4.2 already generates “Authoring JSON Schema for editor validation of package documents” from the Rust registry. msgspec can generate JSON Schema from Python structs. If both exist for the same documents, there are two schemas for one contract and they will drift — precisely what DM-23 forbids. Recommendation: the registry-generated schema is authoritative; msgspec’s generator is used only for Python-internal wire types that have no registry counterpart (e.g. the manifest), never for authoring documents.
Meta constraints mirroring registry invariantsDM-07, G3evaluate. Attractive — a scaling_factor > 0 invariant enforced at the boundary. But §4.2 generates invariants as DataFusion plans executed by P2; duplicating them as Meta constraints re-expresses the same rule in a second place (DM-56). Adopt only where the constraint guards the wire format itself rather than model semantics.
msgspec.Raw for pass-through payloadsDM-37evaluate — defers parsing of a nested payload, useful if the manifest carries per-relation blobs the adapter does not need to inspect.

Gaps and risks

  • msgspec is 0.21.1 — pre-1.0, the only pre-1.0 library in the Python set. Its API has been stable in practice, but the version number is the honest signal and it should be ==-pinned, not floated.
  • msgspec.Struct is not attrs — the two class systems do not interoperate, which is the substance of §9.

9. Adjudication: attrs/cattrs versus msgspec (P8)

§3.3 lists three libraries that overlap heavily. §21.5 names attrs and cattrs and never says what msgspec is for. Leaving this open means the same meaning gets expressed twice at the boundary — DM-56 (“optimize for fewer independent semantic decisions”), DM-57 (“a small coherent core”), DM-58.

What the measurements say

Propertyattrs + cattrsmsgspecSource
Unknown keys, defaultsilently droppedrejected[probe]
Unknown keys, strict modeforbid_extra_keys=Truedefault[probe]
Error locationfield name via ClassValidationError sub-errorsJSON path (at $.rows)[probe]
Multiple errors at onceyes (exception group)first error[probe]
Declarative value constraintsattrs validators (imperative)Meta(gt=…) (declarative)[probe]
JSON Schema generationnoyes[probe]
Codec coveragenone (structuring only)json, msgpack, yaml, toml[api:msgspec@0.21.1]
Works on arbitrary existing classesyes — structures into any attrs classno — must be a Struct[api:*]
Introspection for governanceattrs.fields()msgspec.structs.fields[probe]

The adjudication

They are not redundant; they occupy different positions, and the evidence separates them cleanly:

  • msgspec is a wire codec. Strict by default, located errors, schema generation, four formats. Its natural job is bytes ↔ typed object at a serialization boundary: §20.2’s manifest, and any JSON the platform reads or writes.
  • attrs + cattrs is an in-memory contract layer. cattrs structures from already-decoded material — which is what the Arrow path produces. Rows arriving from a RecordBatchReader are Python objects, not bytes; there is no JSON to decode, so msgspec’s strength does not apply, while cattrs’ ability to structure into arbitrary attrs classes does.

Recommended division, to be written into §21.5:

BoundaryLibraryWhy
§20.2 manifest, and all JSON/TOML the platform reads or writesmsgspecstrict decode, located errors, constraints, schema generation
Arrow bundle rows → typed Python objects (§21.1, §21.2)attrs + cattrs, with forbid_extra_keys=True and detailed_validation=Truethe input is decoded material, not bytes; cattrs structures into generated attrs classes
Anything elseneither — it does not existDM-57

The rule that keeps this from becoming two authorities: a given contract is defined in exactly one of the two systems, never both. The manifest is a msgspec Struct; a relation row is a generated attrs class. No type is expressed in both.

This also resolves why three libraries is not over-engineering under DM-58: attrs and cattrs are one thing (classes plus their structuring), msgspec is another (a codec), and each replaces hand-written work that would otherwise exist — provided the division above is stated rather than left to taste.


10. numpy — the array boundary (P9)

Role: §21.1/§21.2 move numeric columns across the boundary; D11 governs layout ownership; §24’s parity harness compares native and Pyomo results numerically.

Measured (PROBE 8) — the null/NaN collapse

   zero-copy with nulls  : REFUSED -> ArrowInvalid
                           "Needed to copy 1 chunks with 1 nulls, but zero_copy_only was True"
   zero_copy_only=False  : [ 1. nan -0.]  | dtype float64 | null became: nan
   no-null zero-copy     : [ 1.  2. -0.]  | shares buffer: True
   -0.0 sign preserved   : True

This is the cluster’s finding, and it is a G2 one.

FactConsequence
Arrow → numpy with nulls present converts null to NaN§4.4 states pse.bound must never use “NaN or null sentinels”, and the Arrow map verified that Arrow keeps null, NaN and -0.0 structurally distinct. A plain ndarray cannot hold that distinction — float64 has no validity bitmap. The moment a nullable column becomes an ndarray, null and NaN are the same value.
zero_copy_only=True refuses rather than silently copyingGood design: the copy is opt-in and visible.
-0.0 survivesconsistent with the Arrow side
Blueprint requirementStatus
D11 “borrow Arrow buffers when layouts permit and copy when they do not” — Python sideconfirmed: zero_copy_only=True succeeds only for a null-free, single-chunk, primitive column [probe]; anything else must copy, and says so
§4.4’s no-sentinel discipline, carried into Pythonat risk — see below

Pass 2 — alignment opportunities

ItemDM / gateRecommendation
Never convert a nullable column to a bare ndarrayDM-08, G2adopt as a boundary rule. Either assert null_count == 0 before converting (true for solver vectors by construction — §18.1 says fixed and parameter symbols are bound, and solution values are total), or carry the mask alongside. The failure mode is silent and numerically plausible, which is the worst kind. The assertion is one line and belongs in the generated adapter.
zero_copy_only=True as the default in adapter codeDM-37, DM-39adopt. It turns an unintended copy into an error rather than a performance mystery, and documents which columns are genuinely borrowable. Where a copy is required, passing False explicitly records that decision at the call site.
combine_chunks() before conversionDM-36note — a chunked column has no single buffer; zero-copy requires one chunk [probe].

Gaps and risks

  • numpy is unpinned in §3.1 and arrives transitively through both Pyomo and pyarrow. numpy 2.x changed several conversion and dtype behaviours relative to 1.x. For a parity harness that compares numbers, the numpy version is part of the reproducibility contract (DM-48) and should be pinned like everything else.

11. scipy — structural and numerical cross-checks (P10)

Role: scipy is not named in §3.3, but arrives with Pyomo and is the natural oracle for §15’s native structural and numerical analyses.

Capability inventory [probe], [api:scipy@1.18.1]

CapabilitySurface
Sparse formatscsr_matrix, csc_matrix, coo_matrix, bsr_matrix, dia_matrix, dok_matrix, lil_matrix
Graph algorithmsscipy.sparse.csgraph.{connected_components, structural_rank, maximum_bipartite_matching, reverse_cuthill_mckee, depth_first_order}
Dense SVDscipy.linalg.{svd, svdvals}
Sparse SVDscipy.sparse.linalg.svds

Pass 2 — the cross-check opportunity

ItemDM / gateRecommendation
maximum_bipartite_matching as an oracle for §15.3’s Hopcroft–KarpDM-53 (verify invariants and equivalence across representations), DM-54adopt as a test-only oracle. The supporting-library map established that petgraph ships only general-graph matching (Gabow, O(|V|³)) and that §15.3 therefore implements Hopcroft–Karp natively. Hand-written matching on the equation×variable incidence graph is exactly the kind of code that is subtly wrong on degenerate inputs — and scipy ships a bipartite implementation that can be run over the same golden snapshots in CI. Matching cardinality must agree; the matching itself need not (it is not unique).
structural_rank as a DOF cross-checkDM-53adopt as a test-only oracle for §15.2’s structural checks and problems.degrees_of_freedom.
svds / svdvals against §15.5’s SVD toolboxDM-53, DM-39evaluate. §15.5 specifies dense faer SVD for small problems and “iterative for large” — which the Arrow-side map flagged as having no faer implementation behind it. scipy’s svds is a working iterative sparse SVD and is a legitimate oracle for the small-problem path, and a reference for what the large path must eventually do.
scipy in production pathsDM-38, D11reject. D11 gives the native numerics ownership of execution layouts. scipy’s role here is exclusively as a test-time oracle running through the existing Python/parity boundary — never inside a pass, a solve, or a diagnostic that the platform ships.

Gaps and risks

  • scipy, like numpy, is unpinned and transitive. If it is going to serve as a correctness oracle in CI, it is part of the test contract and must be pinned (DM-48).
  • An oracle that disagrees is only useful if the disagreement is attributable. These cross-checks should compare invariants (matching cardinality, structural rank, singular-value count below tolerance), not object identity.

12. IDAES 2.12 — the parity surface and its dependency boundary (P11)

Role: §3.1 “IDAES 2.10 for parity tests only”; §24’s parity harness; the source of the behaviours §9–§17 reproduce.

Availability [probe], [api:idaes@2.12.0]

IDAES 2.12.0 imports cleanly on CPython 3.13.12. 191 modules, 2762 public items extracted. The modules the parity harness needs all resolve:

ModuleParity role
idaes.coreflowsheet, control-volume and property-package base classes (§9–§13)
idaes.core.util.model_statisticsDOF and variable/constraint counts (§15.2)
idaes.core.util.scalingscaling factors and the Jacobian conditioning checks (§15.4, §16)
idaes.core.util.model_diagnosticsthe diagnostics toolbox §15 reproduces
idaes.models.properties.modular_propertiesthe GenericParameterBlock configuration §9.1 maps to relations

Version finding

§3.1 names 2.10; current is 2.12.0 [pypi:idaes-pse], and this map is built against 2.12.0 per the owner’s instruction to use latest throughout. Parity is only meaningful against a stated version — §24’s acceptance criteria compare platform behaviour to IDAES behaviour, and IDAES changed between 2.10 and 2.12. This must be pinned exactly, not floored.

The dependency boundary

IDAES 2.12.0 requires pyomo>=6.10.1 — exactly this map’s Pyomo pin, so the two are compatible [pypi:idaes-pse]. It also pulls a substantial tail: pint 0.26.1, pydantic 2.13.5, sympy 1.14.0, networkx 3.6.1, plus pandas, matplotlib and click [probe].

ItemDM / gateRecommendation
pint enters through IDAES and through Pyomo’s own unitsDM-02 (one authority per fact), G1adopt the boundary rule stated in §5: no Python-side units library is an authority; §8’s dynamic registry is. pint may validate; it may never define. The §3.3 exclusion should be reworded from presence to authority, because presence is not achievable — Pyomo’s units machinery is pint.
pydantic and sympy arrive in the parity environmentDM-57, DM-58note, and fence. Neither is a platform dependency; both are capable enough to tempt use. The parity environment should be a separate dependency set from the platform’s Python package, so that what IDAES drags in cannot quietly become something the platform relies on.
Record the IDAES version in every parity resultDM-31, DM-48adopt. §20.3 records solver and package versions for runs; parity comparisons need the same treatment or a parity failure cannot be attributed to a platform change versus an IDAES change.

Gaps and risks

  • IDAES caps the interpreter at 3.13 (classifiers 3.10–3.13; requires_python unset) [pypi:idaes-pse]. §3.1’s “Python 3.11+” is therefore only true for the platform package; the parity environment has an upper bound as well as a lower one. Two dependency sets, two interpreter ranges.
  • The vendored idaes-pse/ tree in this repository is at the version the blueprint named (2.10 line), not 2.12.0. Reading the tree and testing against the pinned package are two different sources; the pinned package is the one parity claims are made against.

13. Survey — pandas, polars, datafusion-python (P12)

LibraryStatusReason
pandas (3.0.5 observed)not a platform dependency; arrives via IDAES§21.1’s bundle is Arrow streams; a pandas conversion loses the null/NaN distinction the same way numpy does (§10) and adds an index concept the relational model does not have. Acceptable inside parity/notebook code, never in the adapter.
polarsnot used; supported by construction§21.1 names it as a possible consumer. Because the boundary is the PyCapsule protocol rather than a pyarrow type, polars can consume the bundle with no platform change — which is the point of §2’s choice and worth stating as a benefit already earned.
datafusion-pythonnot used; consumer onlyObserved at 54.0.0 against the Rust engine’s 55.1.0. Version skew is tolerable precisely because it consumes the capsule protocol, not our engine. Do not let it become a dependency: two DataFusion versions in one process would be a genuine problem, and D10 already fixes DataFusion’s four roles on the Rust side.

14. Principle-alignment register (Python boundary)

Capabilities the blueprint does not claim, which would improve charter alignment. Disciplined by §G and DM-58: every row names a principle and the hand-written work or risk it removes. Rejections are recorded too.

Adopt

#CapabilityWhereDM / gateWhat it removes or prevents
1Generate the Python contract classes from RelationSpec, as §4.2 already does for Rust§7DM-52, DM-23, DM-56Hand-writing the column list a second time — which §4.2 itself calls a governance failure — and the drift that follows. Largest structural item in the Python surface.
2Forbid extra keys at every boundary (prefer the global hook factory)§7DM-42, G2Measured: the default silently drops unknown keys, converting detectable schema drift into silent data loss.
3Consume cattrs sub-errors via transform_error(); never set detailed_validation=False§7DM-47, DM-30Detailed validation is already on by default; the real gap is that nothing consumes the per-field sub-errors §23.2 requires be preserved.
4Governance lint for Any/bare dict/bare list via attrs.fields()§7DM-06, G3§21.5’s “no Any fields” is currently a convention with nothing enforcing it.
5Detect extension-type degradation via retained ARROW:extension:name metadata§3DM-42, G2, DM-59Measured: loss is silent at the type level but the metadata key survives — so it is detectable at the cost of one boundary check.
6Ship and register the ten pse.* ExtensionType classes from the platform package§3DM-44Each consumer writing its own forks the serialization contract.
7Per-column loss profile in the §21.1 manifest§3G7, DM-42Lets a consumer verify extension coverage rather than discover absence.
8Never convert a nullable Arrow column to a bare ndarray; assert null_count == 0§10DM-08, G2Measured: null becomes NaN, collapsing the distinction §4.4 exists to protect. Silent and numerically plausible.
9zero_copy_only=True as the adapter default§10DM-37, DM-39Turns an unintended copy into an error instead of a performance mystery.
10Verify the external-function library resolves before building the model§4G7, DM-43, DM-30Measured: a missing library is a warning; failure surfaces later, from the solver, far from the cause.
11identify_inconsistent_units instead of a bare assert_units_consistent§5DM-47, §23.2Returns the offending components; compile.math exists to name the culprit.
12Units as cross-check only — registry is the source§5, §12DM-02, G1Keeps pint from becoming a second quantity authority.
13SolverFactory(name).available(False) pre-flight§5DM-43, G7A missing solver becomes a declared capability failure, not a late crash.
14msgspec owns the §20.2 manifest; strict, constrained, located errors§8, §9DM-42, DM-47Gives §20.5’s “unknown registry fingerprint is rejected” a real mechanism.
15Write the attrs/cattrs ÷ msgspec division into §21.5§9DM-56, DM-57Three libraries with overlapping capability and no stated division is how the same meaning gets expressed twice.
16IncidenceGraphInterface and scipy cross-checks as standing CI parity tests§6, §11DM-53, DM-54Turns available techniques into regression controls over the golden snapshots.
17Record IDAES, pyomo.contrib.* and numpy/scipy versions in run and parity records§6, §12DM-31, DM-48A parity failure cannot otherwise be attributed to us versus them.
18Pin every Python library ==, both interpreters§1DM-48, DM-51§3.1 currently floors two and omits three entirely.

Evaluate

#CapabilityWhereDMWhy not yet an adopt
19msgspec.json.schema() for Python-internal wire types only§8DM-23Real value, real risk: §4.2 already generates authoring JSON Schema from the registry. Two generators for one contract will drift. Confine to types with no registry counterpart.
20pyomo.contrib.iis as an oracle for §15.5’s MIS§6DM-47, DM-53Free cross-check via the adapter that already exists; not a replacement for the native implementation.
21scipy.sparse.linalg.svds as an oracle for §15.5’s SVD toolbox§11DM-53, DM-39Also a reference for the “iterative for large” path the Arrow map found unimplemented.
22polynomial_degree() assertions after lowering§4DM-24, DM-53Cheap generated equivalence check between IR classification and Pyomo’s.
23msgspec.Meta constraints mirroring registry invariants§8DM-07Only where the constraint guards the wire format; otherwise it re-expresses a rule P2 already enforces (DM-56).
24__arrow_ext_scalar_class__ for pse.quantity_value§3DM-06Worth it only for types a human inspects interactively.

Reject, with reason

#CapabilityWhereWhy
25scipy or numpy in production paths§10, §11D11 gives native numerics ownership of execution layouts. Test-time oracles only.
26pandas anywhere in the adapter§13Loses the null/NaN distinction and adds an index concept the relational model does not have.
27datafusion-python as a dependency§13Two DataFusion versions in one process; D10 already fixes the engine’s roles on the Rust side.
28pa.UnknownExtensionType as the degradation mechanism§3Measured not to occur on the IPC/capsule path.
29Pyomo Blocks mirroring relation structure§4D12: the adapter builds no hierarchy; engineering names live in the source map.
30pint as a quantity authority§5, §12§8’s registry is sole authority. Presence is unavoidable; authority is not.

15. Acceptance-gate review (Python boundary)

The survey’s revision-2 gate results are historical. Blueprint §21 now specifies the following Proposed mechanisms. The revision-5 review assesses the changed contracts; no current end-to-end Python/backend certification is supplied by this map.

GatesCurrent mechanismRemaining acceptance
G1Registry-generated contracts; pint validates the adapter’s units and never defines model relationsGenerated-schema/units disagreement fixtures
G2–G3Recursive ingress/egress admission, extension loss profile, mask-preserving nullable transfer and strict contract structuringMissing registrations, nested metadata, unknown fields, null/NaN and wrong-unit negative cases
G4Solver/external-library preflight and supported numerical-policy/guarded-route checksAn actual selected solver route; constructing Expr_if is insufficient
G5Idempotent extension registration; immutable stream/buffer owners retain reservations until final releaseRepeated import, delayed drain, cancellation and partial-ingestion fixtures
G6–G7LinearExpression only for proved affine-in-active-variable bodies; declared kernel/derivative bindings and versioned backend conformanceNonlinear children in Affine, constant degree zero, guarded failures and genuine solver round trips

16. Leverage matrix

Blueprint requirementAPIStatus
§21.1 tables cross as __arrow_c_stream__Table/RecordBatchReader.__arrow_c_stream__; RecordBatchReader.from_streamconfirmed
§21.1 consumers: pyarrow / polars / datafusion-pythonthe capsule protocol is implementation-neutralconfirmed — and version skew with datafusion-python is tolerable by design
§4.4 Python consumers register pyarrow.ExtensionType; unknown consumers see storage typesregister_extension_type; measured both pathsconfirmed exactly [probe]
§4.3 metadata preserved into Pythonschema + field metadata across IPC and capsuleconfirmed [probe]
§21.2 step 1 units so assert_units_consistent passespyomo.util.check_units — not pyomo.environ; requires pinterratum ×2 — wrong module implied; contradicts §3.3
§21.2 affine-in-variables loweringLinearExpressionQualified probe: constructor works for the measured linear example; nonlinear children take ordinary expression lowering (ADR-0047)
§21.2 runtime ConditionalExpr_ifExpressionconstruction observed; actual guarded writer/solver route remains a conformance requirement (ADR-0047)
§21.2 step 2 KernelCall → ExternalFunctionAMPLExternalFunctionconfirmed — but a missing library only warns
§21.2 step 3 scaling_factor suffixSuffix(direction=EXPORT)confirmed [probe]
§21.2 step 4 dual/ipopt_zL_out/ipopt_zU_outSuffix(direction=IMPORT)declaration confirmed; round-trip [UNVERIFIED] — no solver installed
§21.3 parmest / PyROS / GDPopt / pyomo.dae / IncidenceGraphInterfaceall present at 6.10.1confirmed [probe] — all but pyomo.dae in pyomo.contrib
§21.5 typed attrs classes, cattrs structuringattrs.define, cattrs.Converterconfirmed available
§21.5 “no Any fields”nothing enforces iterratum — unenforced [probe]
§21.5 msgspec’s roleunstated in the blueprintrefined — §9 adjudicates it
§21.5 contract_substrate_discipline.mdfile deleted with docs/library_ref/erratum E1
§3.1 Python pinstwo floors, three omissions, IDAES at 2.10erratum — §1
D11 borrow buffers where layouts permitzero_copy_only=True succeeds only for null-free single-chunk primitivesconfirmed [probe]
§3.3 “no Python-side units library”Pyomo units are pinterratum — reword to authority

17. Evidence ledger

#ClusterInstrumentSourceTargetOutcome
1setupPyPIpypi.org/pypi/{pkg}/jsonlatest version + requires_python for all eightall eight pinned at current release; idaes-pse classifiers 3.10–3.13 only
2setupuv / venvpy314 (CPython 3.14.7)==-pinned install of the 7-library main setreceipt in §1.3
3setupuv / venvpy313 (CPython 3.13.12)main set + idaes-pse==2.12.0IDAES imports cleanly; pulls pint 0.26.1, pydantic 2.13.5, sympy 1.14.0, networkx 3.6.1
4allapi dumpapidump.py on both environmentspublic API of 8 libraries → JSON8 dumps, 982 modules, 15 K items — receipt in §1.3
5P1, P2apipyarrow@25.0.1ExtensionType, PyCapsule dunders, registry functions__arrow_ext_serialize__/__arrow_ext_deserialize__, register_extension_type, UnknownExtensionType
6P2probeprobe_arrow.py PROBE 1extension type through IPC, registered vs notregistered → SemanticId; unregistered → FixedSizeBinaryType, ARROW:extension:name metadata retained
7P1, P2probeprobe_arrow.py PROBE 2schema/field metadata across IPC and __arrow_c_stream__preserved on both paths; extension type survives the capsule path
8P9probeprobe_arrow.py PROBE 8numpy zero-copy and nullszero-copy refused with nulls; zero_copy_only=False turns null → NaN; -0.0 preserved
9P3probeprobe_pyomo.py PROBE 3LinearExpression, Expr_if, ExternalFunctionall confirmed; missing external library only warns
10P4probeprobe_pyomo.py PROBE 4units consistency, with and without pintpyo.units raises DeferredImportError without pint; with pint, InconsistentUnitsError + identify_inconsistent_units names the culprit
11P4probeprobe_pyomo.py PROBE 5suffixes and solver availabilitysuffixes declare OK; no solver available → round-trip [UNVERIFIED]
12P6probeprobe_contracts.py PROBE 6cattrs strictness and Anyunknown keys silently dropped by default; forbid_extra_keys rejects; Any accepted
13P7probeprobe_contracts.py PROBE 7msgspec constraints and schemaunknown field + type + Meta(gt=0) all rejected with JSON-path locations; json.schema() emits additionalProperties: False
14P5probeimport checks on py314parmest, PyROS, GDPopt, pyomo.dae, incidence_analysis, iisall present; IncidenceGraphInterface at pyomo.contrib.incidence_analysis.interface
15P10probescipy@1.18.1sparse formats, csgraph, SVDmaximum_bipartite_matching, structural_rank, svds, svdvals all present
16P11probeidaes@2.12.0 on py313parity-relevant modulesidaes.core, model_statistics, scaling, model_diagnostics, modular_properties all import
17P11PyPIpypi.org/pypi/idaes-pse/jsondependency requirementspyomo>=6.10.1, pint>=0.24.1, pydantic, sympy, networkx, matplotlib, click
18P6c7 resolvecattrsstrict structuring, converter configuration/python-attrs/cattrs (205 snippets); /python-attrs/attrs (475)
19P6c7 query-docs/python-attrs/cattrsforbid_extra_keys / detailed_validation and when to enable themcorrected this document: detailed validation is on by default since 22.1.0; supplied transform_error() and the global register_structure_hook_factory idiom

This is a Proposed design disposition, not a rerun of the Python corpus. The original API/probe sections retain their conditions. Current dependency and interpreter authority is pyproject.toml, uv.lock and blueprint §3.1.

Earlier item / revision-5 findingCurrent dispositionRemaining acceptance
Unit authority and generated contractsBlueprint §3.3/§4.2/§21.5: pint validates, registry generates contract classes; attrs/cattrs handles structured material and msgspec handles wire recordsGenerator/runtime contract checks; no duplicated independently editable schema
Exact pins, interpreter split and parity versionBlueprint §3.1/§6.14 and ADR-0003/0018/0024; IDAES 2.12.0 is the parity referenceActual named parity gate for the relevant slice; historical absence of a solver is not a claim about the current setup
Deleted discipline referenceNormative Python contracts are stated in blueprint §21.5No dependency on the deleted library-guide file
Extension registration and nullable arraysBlueprint §4.4/§21.1/§21.6, ADR-0039; platform admission invokes checks, unaware consumers have an explicit loss profileReal bundle/query ingress, masks, mismatched extension versions and round trips
R4-09: Affine versus a linear problemBlueprint §21.2, ADR-0047: verify degree 0/1 only for the qualified LinearExpression route; nonlinear children use ordinary expressionsConstant and nonlinear-child fixtures, declared numerical policy
R4-02/10: guarded and opaque kernelsBlueprint §18.5/§18.9/§21.2/§21.4, ADR-0043/0047; actual writer/solver support, units and derivatives are requiredExcluded-branch failure tests, native/Pyomo outcome comparisons and external-function solves
Result suffixes and ecosystem toolsBlueprint §21.2/§21.3: record actual versions and cross-check native structuresReal dual/bound-multiplier round trips and supported pyomo.contrib operations; old import/construction probes do not close these
L7: bounded coalescingBlueprint §21.1, ADR-0048, R-24; stream ownership remains mandatoryRepresentative transfer/memory measurement and independent semantic equality

The implementation sequence and boundary fixtures are recorded in plan 02. Performance and solver support remain unmeasured/unverified until those specific paths are exercised.