DiscoveryTaskResult — Design Document#

Status: DESIGN-FROZEN in v0.31a; RUNTIME IMPLEMENTED in v0.31b1 for target_convention="pde_library"; WeakPDELibrary diagnostic wrapper RUNTIME IMPLEMENTED in v0.31b2 as a SEPARATE summary type (pdelie_weak_pde_library_diagnostic), not as a discovery_task_result variant.

This document defines the composed TaskResult artifact returned by the pdelie.tasks.discovery submodule when a caller runs a PySINDy-backed PDE discovery task over a canonical scalar 1D PDELie input. It is normative for v0.31 proper.

Motivation#

The existing downstream-discovery surface consists of three narrow, disconnected pieces:

  • pdelie.discovery.to_pysindy_trajectories — flattens a periodic scalar 1D FieldBatch into PySINDy-shape trajectories (src/pdelie/discovery/pysindy_bridge.py)

  • pdelie.discovery.fit_pysindy_discovery(config=None) — runs a default PySINDy fit over the bridge output and refuses non-default configs (src/pdelie/discovery/pysindy_adapter.py:196-204)

  • pdelie.discovery.summarize_discovery_result — normalizes a backend-native discovery-result dict into a JSON-compatible summary (src/pdelie/discovery/contracts.py:305-409)

A user who wants to run a PDELibrary-backed PDE discovery over their canonical data currently has to build the bridge, hand-configure PySINDy, run the fit outside PDELie, and then re-enter the reporting layer through summarize_discovery_result. The task bridge composes these three pieces into one call and produces a single stable JSON-compatible artifact that carries backend provenance, recovery metrics, and residuals side-by-side.

Why a new schema#

summarize_discovery_result is a stable v0.22 surface. It is intentionally backend-native: it wraps any dict shaped like PySINDy’s output. It does not carry backend versions, does not carry input-layout provenance, does not carry a weak_contract block, and does not enforce strict-JSON (allow_nan=False). The task-bridge output needs all four of these, and it needs to compose cleanly with summarize_discovery_result as a sibling embedded payload rather than replacing it.

Proposed schema#

TaskResult is the return type of the (design-only) pdelie.tasks.discovery.run_pysindy_pde_task(...) runtime API. Its stable serialized form is:

{
    "summary_schema_version": "0.1",
    "summary_type": "discovery_task_result",
    "task_name": str,
    "backend_name": Literal["pysindy"],
    "backend_version": dict[str, str],
    "target_convention": Literal["pde_library", "weak_pde_library"],
    "derivative_backend": str,
    "input_layout": Literal["scalar_1d_uniform"],
    "library_feature_names": list[str],
    "selected_terms": dict[str, dict[str, float]],
    "coefficients": dict | None,
    "support_precision": float,
    "support_recall": float,
    "support_f1": float,
    "exact_support": bool,
    "coefficient_relative_l2": float,
    "train_residual": dict | None,
    "heldout_residual": dict | None,
    "weak_contract": dict | None,
    "warnings": list[str],
    "pysindy_bridge_variant": Literal["periodic_only_v1"],
    "underlying_discovery_result": dict,
}

Field semantics#

  • summary_schema_version — literal "0.1".

  • summary_type — literal "discovery_task_result".

  • task_name — caller-supplied non-empty string identifying the task instance for provenance and logging.

  • backend_nameLiteral["pysindy"] in v0.31. Expansion of the Literal set (e.g. to "pysr") is a v0.32+ concern.

  • backend_version — dict with required keys pysindy, sklearn, and pdelie. Values are version strings (importlib.metadata.version(...)).

  • target_conventionLiteral["pde_library", "weak_pde_library"]. Selects between the strong PySINDy PDELibrary bridge and the WeakPDELibrary diagnostic wrapper.

  • derivative_backend — string promoted from fit_config["pysindy_model"]["differentiation_method"]. This is the PySINDy-side identifier (e.g. "FiniteDifference", "SmoothedFiniteDifference", or "SpectralDerivative"). The v0.30c PDELie-side spectral_fd / finite_difference dispatch continues to govern PDELie-native derivative computation; this field records which method PySINDy used, not which PDELie backend was invoked.

  • input_layoutLiteral["scalar_1d_uniform"]. Reserved enum for v0.34+ multi-channel/2D expansion. The field is first-class now so that v0.34a-c do not retrofit the schema; only "scalar_1d_uniform" is accepted at runtime.

  • library_feature_names — list of non-empty strings, unique. Renamed from library_terms for continuity with summarize_discovery_result (src/pdelie/discovery/contracts.py:395, which reports library_feature_names from result_mapping["library_feature_names"]).

  • selected_terms — dict keyed by PySINDy target-feature name; values are dict[str, float] mapping candidate-term string to fitted coefficient. Verbatim from the equation_terms block of the embedded discovery_result (src/pdelie/discovery/contracts.py:396).

  • coefficients — optional dict carrying the raw coefficient matrix in a JSON-compatible form (e.g. {"shape": [num_features, num_terms], "values": list[list[float]], "epsilon": support_epsilon}). Opt-in shape; consumers must tolerate None.

  • support_precision, support_recall, support_f1 — configured recovery metrics; produced via the existing pdelie.discovery.evaluate_discovery_recovery machinery.

  • exact_support — bool; True iff support_precision == 1.0 and support_recall == 1.0.

  • coefficient_relative_l2 — float; ||fit - target||_2 / max(||target||_2, epsilon).

  • train_residual, heldout_residual — optional strict-numeric dicts of the shape {"size": int, "l2_norm": float, "rms": float, "max_abs": float}, matching _residual_summary_or_none at src/pdelie/discovery/contracts.py:198-214.

  • weak_contract — optional dict. Non-null iff target_convention == "weak_pde_library". Its structure is documented under “WeakPDELibrary wrapper” below.

  • warnings — list of non-empty strings drawn from a controlled vocabulary ("heldout_residual_missing", "support_epsilon_defaulted", "weak_contract_diagnostic_only", "differentiation_method_defaulted"; extended per implementation review).

  • pysindy_bridge_variantLiteral["periodic_only_v1"]. Provenance identifier for the PySINDy discovery bridge variant that produced this TaskResult. "periodic_only_v1" is the sole accepted value in v0.31 and encodes the periodic-only-x contract enforced at runtime by the pdelie.tasks.discovery entry point (see “Runtime boundary-condition guard” below). Future FD-nonperiodic variants (v0.32.5 target) will add distinct string values (e.g. "nonperiodic_x_finite_difference_v1") so consumers reading a saved TaskResult can determine which bridge produced it without inferring from derivative_backend or the embedded field metadata. This is a first-class field so extension does not require a schema-version bump.

  • underlying_discovery_result — the payload returned by summarize_discovery_result(...). TaskResult composes with the existing summarizer as a sibling wrapper, not as a parent that swallows the child.

Runtime boundary-condition guard#

The v0.31a periodic-only fence is enforced at two places in v0.31b runtime:

  1. The pdelie.tasks.discovery task-runner entry point — before assembling PySINDy trajectories, the runner calls is_x_periodic(field) from pdelie._boundary. If the field is not periodic, the runner raises PySINDyDiscoveryUnsupportedBoundaryError (a new dedicated exception in pdelie.tasks.discovery, subclass of ScopeValidationError) with a message pointing to pysindy_bridge_variant == "periodic_only_v1".

  2. The existing to_pysindy_trajectories gate at src/pdelie/discovery/pysindy_bridge.py:27-28 continues to reject nonperiodic fields.

Both are required. Relying on the to_pysindy_trajectories gate alone leaves a hole: fit_pysindy_discovery accepts raw trajectory arrays and does not consult boundary metadata, so a caller who assembles trajectories directly bypasses the bridge gate. The task-runner-level guard closes that hole for v0.31.

The pysindy_bridge_variant field records which enforcement path was in effect at task time. When v0.32.5 lands FD-nonperiodic discovery, it will emit a distinct variant string; consumers reading old TaskResult payloads can filter by the periodic-only variant explicitly.

weak_contract trigger predicate#

weak_contract is:

  • None when target_convention == "pde_library"

  • a non-null dict when target_convention == "weak_pde_library"

There is no third state. The runtime rejects (SchemaValidationError) any TaskResult where target_convention == "weak_pde_library" and weak_contract is None, or where target_convention == "pde_library" and weak_contract is not None.

NaN-safety contract#

The composed payload MUST pass:

assert json.loads(json.dumps(payload, allow_nan=False)) == payload

before return. The runtime accomplishes this by routing the final payload through the existing helper _validate_strict_json_compatible(payload, name="discovery_task_result summary") at src/pdelie/reporting/summaries.py:196-202, mirroring the two existing strict-JSON summaries (summarize_xarray_dataset_readiness at summaries.py:1311 and summarize_weak_form_supportability at summaries.py:1954).

This is required BECAUSE only 2 of 14 current summarize_* functions enforce allow_nan=False; the other 12 route through the permissive _summary_payload funnel (summaries.py:255-261 calling _validate_json_compatible at :187-193, which catches only TypeError and permits NaN/Inf). TaskResult must not inherit that assumption because:

  • the embedded underlying_discovery_result can, in principle, carry NaN residual entries under the permissive summarizer path

  • train_residual and heldout_residual are numeric-valued and an NaN in either would silently pass the permissive validator

  • downstream consumers (dashboards, notebook checks, discovery-workflow reporting) treat TaskResult as a canonical artifact and would break on NaN deserialization in strict JSON parsers

The runtime uses _validate_strict_json_compatible at the composition boundary — after all sub-payloads are assembled, immediately before return. The check catches any NaN or Inf that leaked in through an embedded sub-summary.

WeakPDELibrary wrapper#

When target_convention == "weak_pde_library", the task bridge additionally composes a diagnostic report from PySINDy’s WeakPDELibrary output. This wrapper carries its own top-level payload with:

{
    "summary_schema_version": "0.1",
    "summary_type": "pdelie_weak_pde_library_diagnostic",
    "method_family": "pysindy_weak_pde_library_polynomial_gauss_v1",
    "test_function_family": "pysindy_weak_pde_library_polynomial_bump_v1",
    "quadrature_rule": "pysindy_weak_pde_library_composite_gauss_v1",
    "diagnostic_only": true,
    ...
}

and its condensed form is stored inside the TaskResult’s weak_contract field:

{
    "method_family": "pysindy_weak_pde_library_polynomial_gauss_v1",
    "test_function_family": "pysindy_weak_pde_library_polynomial_bump_v1",
    "quadrature_rule": "pysindy_weak_pde_library_composite_gauss_v1",
    "diagnostic_only": true,
}

Why these identifier strings#

The three identifier strings above are distinct from the pdelie-native weak_1d identifiers:

  • method_family: pdelie-native is "local_separable_quartic_bump_trapezoid_v1" (src/pdelie/residuals/weak_1d.py:20)

  • test_function / test_function_family: pdelie-native is "separable_quartic_bump_beta" (src/pdelie/residuals/weak_1d.py:22)

  • quadrature / quadrature_rule: pdelie-native is "composite_tensor_product_trapezoidal_native_window" (src/pdelie/residuals/weak_1d.py:21)

The wrapper’s strings deliberately use the pysindy_weak_pde_library_* prefix so downstream consumers (summarize_weak_residual_report, summarize_split_leakage_provenance, any future summarize_weak_form_supportability extension) can disambiguate provenance. There is no enum or allowlist gating these strings today — they are free-form string constants — so nothing at the schema layer prevents accidental collision. The scope freeze documents the wrapper’s chosen strings explicitly as the disambiguation mechanism.

The diagnostic_only: true marker#

The wrapper payload always carries "diagnostic_only": true. This marker exists so that no downstream summarizer can mistake a WeakPDELibrary wrapper report for a public weak-form supportability claim. In particular:

  • summarize_weak_form_supportability continues to treat the pdelie-native weak_1d slice as supported_existing_slice (Heat/Burgers); the wrapper report cannot promote a PDE to supported_existing_slice.

  • The wrapper contributes to supportability_label = "diagnostic_only" at most.

Supportability policy update#

The supportability-policy dict gains one new key and one re-scoping note:

  • new key: "supports_pysindy_weak_library_diagnostic": True — records that the wrapper is available as a diagnostic-only surface

  • existing key "supports_weak_derivative_backend" is re-scoped to public strong-derivative-only pdelie-native path, with an explicit inline note that the WeakPDELibrary wrapper is a diagnostic-scope weak derivative backend

Both changes are additive; no existing field is removed and no existing value is silently flipped.

Legacy compatibility#

  • summarize_discovery_result is unchanged. TaskResult composes with it — it does not replace it. A caller who wants only the discovery-result summary continues to call summarize_discovery_result directly.

  • fit_pysindy_discovery(config=None) continues to work at its existing default; the loosening in v0.31 accepts config={"pysindy_model": <configured_SINDy>} as an additional shape.

  • The existing periodic-only guard in to_pysindy_trajectories (src/pdelie/discovery/pysindy_bridge.py:18-28) is unchanged; PySINDy PDELibrary bridge is periodic-only in v0.31; FD-nonperiodic extension is explicitly deferred.

Retention window for weak_1d#

weak_1d is retained through v0.32 close. Its removal is contingent on:

  1. the v0.31 PySINDy PDELibrary bridge shipping a documented replacement path, AND

  2. the WeakPDELibrary diagnostic wrapper landing a validated parity harness with a documented O((dx)^p) tolerance

Neither condition is expected to hold at the v0.31 close; the wrapper’s diagnostic_only: true marker makes this explicit.

Out of scope for v0.31#

The following are deliberately deferred to later releases:

  • root pdelie exports for any TaskResult, WeakPDELibrary wrapper, or discovery-task API

  • WSINDy design-matrix / SR3 weak sparse recovery

  • weak-form nonperiodic surface

  • multi-channel or 2D FieldBatch dispatch (v0.34)

  • SymmetryCandidate contract (v0.30.1)

  • LieGG / trained-model extraction (v0.35a)

  • benchmark policy or clean/noisy gating

  • PDEBench / The Well support claims (v0.32 external-data readiness cookbooks)

  • promotion of the v0.30e advisory lint / typecheck / coverage CI jobs to blocking

Validation requirements (for v0.31 proper, not v0.31a)#

When the TaskResult runtime lands (v0.31b+):

  • run_pysindy_pde_task(...) must call _validate_strict_json_compatible(payload, name="discovery_task_result summary") on the composed payload before return

  • summary_schema_version == "0.1" and summary_type == "discovery_task_result" are literal-string invariants

  • backend_name in {"pysindy"} and target_convention in {"pde_library", "weak_pde_library"} are literal-set invariants

  • input_layout == "scalar_1d_uniform" is a literal invariant (widened only under a scope freeze)

  • weak_contract non-null iff target_convention == "weak_pde_library"

  • underlying_discovery_result["summary_type"] == "discovery_result"

  • backend versions are strings from importlib.metadata.version(...) for pysindy, sklearn, pdelie

Implementation pointer (for v0.31 proper)#

The most surgical implementation places a new submodule src/pdelie/tasks/discovery.py with:

  • run_pysindy_pde_task(field, *, config, target_convention, task_name, target_terms=None, ...) composing the existing to_pysindy_trajectories + fit_pysindy_discovery + summarize_discovery_result + evaluate_discovery_recovery machinery

  • a strict-JSON helper import from pdelie.reporting.summaries — reuse _validate_strict_json_compatible without duplicating logic

  • a WeakPDELibraryDiagnostic dataclass (submodule-only) with an as_dict() that produces the wrapper-payload shape above

  • no new root pdelie export; no modification of pdelie.residuals.weak_1d

The existing summarizer strict-JSON helper (_validate_strict_json_compatible at src/pdelie/reporting/summaries.py:196-202) handles the serialization enforcement without modification.

v0.31b2 — WeakPDELibrary diagnostic as a SEPARATE summary type#

Status update: In v0.31b1, target_convention="weak_pde_library" was DEFERRED. In v0.31b2, it is RUNTIME IMPLEMENTED — but the runtime lands on a separate top-level summary type (pdelie_weak_pde_library_diagnostic), not as a variant of discovery_task_result. The v0.31b1 discovery_task_result schema is not extended and its 22-key top-level shape is preserved.

Why a separate summary type#

The peer-review of the v0.31b arc flagged that embedding a full diagnostic-only WeakPDELibrary report inside discovery_task_result would either:

  1. inflate discovery_task_result from 22 keys to a variable-shape payload whose key set depends on target_convention, or

  2. force downstream consumers of the v0.31b1 discovery-task-result schema to disambiguate two very different payload shapes at read time.

Both outcomes are avoided by giving the WeakPDELibrary diagnostic its own strict-JSON top-level summary type. The condensed weak_contract dict (already frozen at 4 keys in v0.31a) remains embedded inside discovery_task_result when target_convention == "weak_pde_library", so the composition boundary between the two summary types is only the condensed 4-key weak_contract block. The full WeakPDELibrary diagnostic payload is not embedded into discovery_task_result and does not change the 22-key discovery_task_result top-level shape.

pdelie_weak_pde_library_diagnostic top-level key set#

The v0.31b2 wrapper’s public JSON payload has exactly the following top-level keys:

summary_schema_version
summary_type
diagnostic_only
method_family
backend_name
backend_version
input_layout
boundary_policy
target_convention
library_configuration
test_function_family
quadrature_rule
spatiotemporal_grid_shape
input_field_shape
weak_feature_names
weak_matrix_shape
weak_target_shape
retained_weak_rows
skipped_weak_rows
skipped_row_reasons
finite_value_status
column_norms
matrix_rank
matrix_condition_number
warnings
compatibility_notes
provenance

Frozen literal values:

  • summary_schema_version = "0.1"

  • summary_type = "pdelie_weak_pde_library_diagnostic"

  • diagnostic_only = True

  • method_family = "pysindy_weak_pde_library_polynomial_gauss_v1"

  • input_layout = "scalar_1d_uniform"

The three identifier strings frozen in v0.31a (method_family, test_function_family, quadrature_rule) continue to carry their pysindy_weak_pde_library_* prefix so the pdelie-native weak_1d provenance strings (local_separable_quartic_bump_trapezoid_v1, separable_quartic_bump_beta, composite_tensor_product_trapezoidal_native_window) remain unambiguously disambiguated.

diagnostic_only=True does NOT change discovery_task_result#

The diagnostic_only: true marker is a top-level field on pdelie_weak_pde_library_diagnostic only. It is not promoted to a top-level key on discovery_task_result. The v0.31b1 22-key top-level shape of discovery_task_result is preserved verbatim in v0.31b2:

  • no new top-level key is added to discovery_task_result;

  • the embedded 4-key weak_contract dict (with its own inner diagnostic_only: true) is unchanged from v0.31a;

  • discovery_task_result remains a 22-key top-level shape.

Callers who want the full diagnostic report call the wrapper (via pdelie.tasks.weak_pde_library) directly; callers who want a discovery-task record with the condensed weak-contract provenance continue to use pdelie.tasks.discovery.run_pysindy_pde_task unchanged. The two entry points are siblings, not parent/child.

Public surface (v0.31b2)#

Three new submodule-only names, none of which is re-exported at the pdelie root:

  • pdelie.tasks.weak_pde_library.WeakPDELibraryDiagnostic — dataclass carrying the wrapper payload; has an as_dict() producing the strict-JSON top-level shape above.

  • pdelie.tasks.weak_pde_library.summarize_pysindy_weak_pde_library_diagnostic — takes a WeakPDELibraryDiagnostic instance (or the raw PySINDy assemblage produced by inspect_pysindy_weak_pde_library) and returns the strict-JSON-validated summary dict.

  • pdelie.tasks.weak_pde_library.inspect_pysindy_weak_pde_library — the inspector that runs PySINDy’s WeakPDELibrary under a periodic scalar 1D FieldBatch and returns a WeakPDELibraryDiagnostic.

All three names are also available from the pdelie.tasks package as re-exports; they are not exported at the pdelie root. No pdelie.residuals, pdelie.reporting, or pdelie.discovery symbol carries any of these names. The strict-JSON boundary is enforced by routing the final payload through _validate_strict_json_compatible at src/pdelie/reporting/summaries.py:196-202 exactly once, immediately before return.

Exception reuse#

PySINDyDiscoveryUnsupportedBoundaryError (defined in pdelie.tasks.discovery since v0.31b1, subclass of ScopeValidationError) is the correct raise for nonperiodic-x inputs to inspect_pysindy_weak_pde_library. No new exception is introduced. General schema violations raise SchemaValidationError per the existing v0.22 convention.

Non-goals (v0.31b2)#

  • no pdelie root export for the three new names

  • no promotion of any PDE to supported_existing_slice via the wrapper (the marker diagnostic_only=True is load-bearing)

  • no WSINDy design matrix; no SR3-style weak sparse recovery; no noise-robustness certification; no clean/noisy gate

  • no validated O((dx)^p) parity harness with the pdelie-native weak_1d output (contingent on weak_1d removal beyond v0.32 close and explicitly deferred)

  • no PDEBench / The Well benchmark claim

  • no widening of the periodic-only-x guard; the two-layer runtime BC guard from v0.31b1 (task-entry is_x_periodic + to_pysindy_trajectories bridge gate) covers the wrapper path as well

  • no removal of weak_1d; retention is guaranteed through v0.32 close

  • no change to discovery_task_result’s 22-key top-level shape, and no new top-level diagnostic_only key on discovery_task_result