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 1DFieldBatchinto 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_name—Literal["pysindy"]in v0.31. Expansion of the Literal set (e.g. to"pysr") is av0.32+concern.backend_version— dict with required keyspysindy,sklearn, andpdelie. Values are version strings (importlib.metadata.version(...)).target_convention—Literal["pde_library", "weak_pde_library"]. Selects between the strong PySINDyPDELibrarybridge and theWeakPDELibrarydiagnostic wrapper.derivative_backend— string promoted fromfit_config["pysindy_model"]["differentiation_method"]. This is the PySINDy-side identifier (e.g."FiniteDifference","SmoothedFiniteDifference", or"SpectralDerivative"). The v0.30c PDELie-sidespectral_fd/finite_differencedispatch continues to govern PDELie-native derivative computation; this field records which method PySINDy used, not which PDELie backend was invoked.input_layout—Literal["scalar_1d_uniform"]. Reserved enum forv0.34+multi-channel/2D expansion. The field is first-class now so thatv0.34a-cdo not retrofit the schema; only"scalar_1d_uniform"is accepted at runtime.library_feature_names— list of non-empty strings, unique. Renamed fromlibrary_termsfor continuity withsummarize_discovery_result(src/pdelie/discovery/contracts.py:395, which reportslibrary_feature_namesfromresult_mapping["library_feature_names"]).selected_terms— dict keyed by PySINDy target-feature name; values aredict[str, float]mapping candidate-term string to fitted coefficient. Verbatim from theequation_termsblock of the embeddeddiscovery_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 tolerateNone.support_precision,support_recall,support_f1— configured recovery metrics; produced via the existingpdelie.discovery.evaluate_discovery_recoverymachinery.exact_support— bool;Trueiffsupport_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_noneatsrc/pdelie/discovery/contracts.py:198-214.weak_contract— optional dict. Non-null ifftarget_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_variant—Literal["periodic_only_v1"]. Provenance identifier for the PySINDy discovery bridge variant that produced thisTaskResult."periodic_only_v1"is the sole accepted value in v0.31 and encodes the periodic-only-x contract enforced at runtime by thepdelie.tasks.discoveryentry 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 savedTaskResultcan determine which bridge produced it without inferring fromderivative_backendor 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 bysummarize_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:
The
pdelie.tasks.discoverytask-runner entry point — before assembling PySINDy trajectories, the runner callsis_x_periodic(field)frompdelie._boundary. If the field is not periodic, the runner raisesPySINDyDiscoveryUnsupportedBoundaryError(a new dedicated exception inpdelie.tasks.discovery, subclass ofScopeValidationError) with a message pointing topysindy_bridge_variant == "periodic_only_v1".The existing
to_pysindy_trajectoriesgate atsrc/pdelie/discovery/pysindy_bridge.py:27-28continues 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:
Nonewhentarget_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_resultcan, in principle, carry NaN residual entries under the permissive summarizer pathtrain_residualandheldout_residualare numeric-valued and an NaN in either would silently pass the permissive validatordownstream consumers (dashboards, notebook checks, discovery-workflow reporting) treat TaskResult as a canonical artifact and would break on
NaNdeserialization 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_supportabilitycontinues to treat the pdelie-nativeweak_1dslice assupported_existing_slice(Heat/Burgers); the wrapper report cannot promote a PDE tosupported_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 surfaceexisting 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_resultis unchanged. TaskResult composes with it — it does not replace it. A caller who wants only the discovery-result summary continues to callsummarize_discovery_resultdirectly.fit_pysindy_discovery(config=None)continues to work at its existing default; the loosening in v0.31 acceptsconfig={"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:
the v0.31 PySINDy
PDELibrarybridge shipping a documented replacement path, ANDthe
WeakPDELibrarydiagnostic wrapper landing a validated parity harness with a documentedO((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
pdelieexports for any TaskResult, WeakPDELibrary wrapper, or discovery-task APIWSINDy 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.32external-data readiness cookbooks)promotion of the v0.30e advisory
lint/typecheck/coverageCI 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 returnsummary_schema_version == "0.1"andsummary_type == "discovery_task_result"are literal-string invariantsbackend_name in {"pysindy"}andtarget_convention in {"pde_library", "weak_pde_library"}are literal-set invariantsinput_layout == "scalar_1d_uniform"is a literal invariant (widened only under a scope freeze)weak_contractnon-null ifftarget_convention == "weak_pde_library"underlying_discovery_result["summary_type"] == "discovery_result"backend versions are strings from
importlib.metadata.version(...)forpysindy,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 existingto_pysindy_trajectories+fit_pysindy_discovery+summarize_discovery_result+evaluate_discovery_recoverymachinerya strict-JSON helper import from
pdelie.reporting.summaries— reuse_validate_strict_json_compatiblewithout duplicating logica
WeakPDELibraryDiagnosticdataclass (submodule-only) with anas_dict()that produces the wrapper-payload shape aboveno new root
pdelieexport; no modification ofpdelie.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:
inflate
discovery_task_resultfrom 22 keys to a variable-shape payload whose key set depends ontarget_convention, orforce 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 = Truemethod_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_contractdict (with its own innerdiagnostic_only: true) is unchanged from v0.31a;discovery_task_resultremains 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 anas_dict()producing the strict-JSON top-level shape above.pdelie.tasks.weak_pde_library.summarize_pysindy_weak_pde_library_diagnostic— takes aWeakPDELibraryDiagnosticinstance (or the raw PySINDy assemblage produced byinspect_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’sWeakPDELibraryunder a periodic scalar 1DFieldBatchand returns aWeakPDELibraryDiagnostic.
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
pdelieroot export for the three new namesno promotion of any PDE to
supported_existing_slicevia the wrapper (the markerdiagnostic_only=Trueis 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-nativeweak_1doutput (contingent onweak_1dremoval 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_trajectoriesbridge gate) covers the wrapper path as wellno removal of
weak_1d; retention is guaranteed through v0.32 closeno change to
discovery_task_result’s 22-key top-level shape, and no new top-leveldiagnostic_onlykey ondiscovery_task_result