swatplus-builder 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- swatplus_builder/__init__.py +26 -0
- swatplus_builder/artifacts/__init__.py +34 -0
- swatplus_builder/artifacts/hashing.py +49 -0
- swatplus_builder/artifacts/models.py +128 -0
- swatplus_builder/artifacts/store.py +198 -0
- swatplus_builder/autoresearch/__init__.py +47 -0
- swatplus_builder/autoresearch/loop.py +411 -0
- swatplus_builder/autoresearch/surrogate.py +557 -0
- swatplus_builder/calibration/__init__.py +52 -0
- swatplus_builder/calibration/bridge_diagnostics.py +339 -0
- swatplus_builder/calibration/calibrator.py +1172 -0
- swatplus_builder/calibration/diagnostic_calibrator.py +1025 -0
- swatplus_builder/calibration/forward.py +472 -0
- swatplus_builder/calibration/locked_benchmark.py +1433 -0
- swatplus_builder/calibration/nwis.py +61 -0
- swatplus_builder/calibration/pyswatplus_runtime.py +93 -0
- swatplus_builder/calibration/real_engine.py +467 -0
- swatplus_builder/calibration/report.py +476 -0
- swatplus_builder/calibration/sensitivity_screen.py +126 -0
- swatplus_builder/calibration/spotpy_adapter.py +144 -0
- swatplus_builder/cli.py +1751 -0
- swatplus_builder/config.py +72 -0
- swatplus_builder/db/__init__.py +1 -0
- swatplus_builder/db/mock_datasets.py +470 -0
- swatplus_builder/db/project.py +275 -0
- swatplus_builder/db/schema.py +367 -0
- swatplus_builder/db/seed.py +132 -0
- swatplus_builder/db/writer.py +415 -0
- swatplus_builder/diagnostics.py +822 -0
- swatplus_builder/editor/__init__.py +5 -0
- swatplus_builder/editor/api.py +726 -0
- swatplus_builder/editor/vendored/Pipfile +13 -0
- swatplus_builder/editor/vendored/actions/__init__.py +0 -0
- swatplus_builder/editor/vendored/actions/create_databases.py +119 -0
- swatplus_builder/editor/vendored/actions/get_swatplus_check.py +902 -0
- swatplus_builder/editor/vendored/actions/import_export_data.py +249 -0
- swatplus_builder/editor/vendored/actions/import_gis.py +1644 -0
- swatplus_builder/editor/vendored/actions/import_gis_legacy.py +2007 -0
- swatplus_builder/editor/vendored/actions/import_weather.py +1466 -0
- swatplus_builder/editor/vendored/actions/load_scenarios.py +97 -0
- swatplus_builder/editor/vendored/actions/read_output.py +432 -0
- swatplus_builder/editor/vendored/actions/read_output_legacy.py +350 -0
- swatplus_builder/editor/vendored/actions/reimport_gis.py +100 -0
- swatplus_builder/editor/vendored/actions/run_all.py +105 -0
- swatplus_builder/editor/vendored/actions/setup_project.py +154 -0
- swatplus_builder/editor/vendored/actions/update_datasets.py +485 -0
- swatplus_builder/editor/vendored/actions/update_project.py +1132 -0
- swatplus_builder/editor/vendored/actions/write_files.py +1098 -0
- swatplus_builder/editor/vendored/database/__init__.py +0 -0
- swatplus_builder/editor/vendored/database/datasets/__init__.py +0 -0
- swatplus_builder/editor/vendored/database/datasets/base.py +8 -0
- swatplus_builder/editor/vendored/database/datasets/basin.py +77 -0
- swatplus_builder/editor/vendored/database/datasets/change.py +10 -0
- swatplus_builder/editor/vendored/database/datasets/climate.py +29 -0
- swatplus_builder/editor/vendored/database/datasets/decision_table.py +41 -0
- swatplus_builder/editor/vendored/database/datasets/definitions.py +88 -0
- swatplus_builder/editor/vendored/database/datasets/hru_parm_db.py +165 -0
- swatplus_builder/editor/vendored/database/datasets/init.py +21 -0
- swatplus_builder/editor/vendored/database/datasets/lum.py +70 -0
- swatplus_builder/editor/vendored/database/datasets/ops.py +61 -0
- swatplus_builder/editor/vendored/database/datasets/setup.py +732 -0
- swatplus_builder/editor/vendored/database/datasets/soils.py +41 -0
- swatplus_builder/editor/vendored/database/datasets/structural.py +78 -0
- swatplus_builder/editor/vendored/database/lib.py +95 -0
- swatplus_builder/editor/vendored/database/project/__init__.py +0 -0
- swatplus_builder/editor/vendored/database/project/aquifer.py +80 -0
- swatplus_builder/editor/vendored/database/project/base.py +8 -0
- swatplus_builder/editor/vendored/database/project/basin.py +78 -0
- swatplus_builder/editor/vendored/database/project/change.py +147 -0
- swatplus_builder/editor/vendored/database/project/channel.py +146 -0
- swatplus_builder/editor/vendored/database/project/climate.py +110 -0
- swatplus_builder/editor/vendored/database/project/config.py +79 -0
- swatplus_builder/editor/vendored/database/project/connect.py +148 -0
- swatplus_builder/editor/vendored/database/project/decision_table.py +41 -0
- swatplus_builder/editor/vendored/database/project/dr.py +93 -0
- swatplus_builder/editor/vendored/database/project/exco.py +93 -0
- swatplus_builder/editor/vendored/database/project/gis.py +125 -0
- swatplus_builder/editor/vendored/database/project/gwflow.py +160 -0
- swatplus_builder/editor/vendored/database/project/hru.py +52 -0
- swatplus_builder/editor/vendored/database/project/hru_parm_db.py +172 -0
- swatplus_builder/editor/vendored/database/project/hydrology.py +62 -0
- swatplus_builder/editor/vendored/database/project/init.py +240 -0
- swatplus_builder/editor/vendored/database/project/link.py +21 -0
- swatplus_builder/editor/vendored/database/project/lum.py +74 -0
- swatplus_builder/editor/vendored/database/project/ops.py +61 -0
- swatplus_builder/editor/vendored/database/project/recall.py +35 -0
- swatplus_builder/editor/vendored/database/project/regions.py +134 -0
- swatplus_builder/editor/vendored/database/project/reservoir.py +105 -0
- swatplus_builder/editor/vendored/database/project/routing_unit.py +57 -0
- swatplus_builder/editor/vendored/database/project/salts.py +467 -0
- swatplus_builder/editor/vendored/database/project/setup.py +307 -0
- swatplus_builder/editor/vendored/database/project/simulation.py +114 -0
- swatplus_builder/editor/vendored/database/project/soils.py +53 -0
- swatplus_builder/editor/vendored/database/project/structural.py +78 -0
- swatplus_builder/editor/vendored/database/project/water_rights.py +49 -0
- swatplus_builder/editor/vendored/database/soils.py +207 -0
- swatplus_builder/editor/vendored/database/vardefs.py +50 -0
- swatplus_builder/editor/vendored/database/wgn.py +139 -0
- swatplus_builder/editor/vendored/fileio/__init__.py +0 -0
- swatplus_builder/editor/vendored/fileio/aquifer.py +124 -0
- swatplus_builder/editor/vendored/fileio/base.py +497 -0
- swatplus_builder/editor/vendored/fileio/basin.py +55 -0
- swatplus_builder/editor/vendored/fileio/change.py +405 -0
- swatplus_builder/editor/vendored/fileio/channel.py +178 -0
- swatplus_builder/editor/vendored/fileio/climate.py +261 -0
- swatplus_builder/editor/vendored/fileio/config.py +350 -0
- swatplus_builder/editor/vendored/fileio/connect.py +329 -0
- swatplus_builder/editor/vendored/fileio/decision_table.py +241 -0
- swatplus_builder/editor/vendored/fileio/dr.py +51 -0
- swatplus_builder/editor/vendored/fileio/exco.py +195 -0
- swatplus_builder/editor/vendored/fileio/gwflow.py +717 -0
- swatplus_builder/editor/vendored/fileio/hru.py +109 -0
- swatplus_builder/editor/vendored/fileio/hru_parm_db.py +177 -0
- swatplus_builder/editor/vendored/fileio/hydrology.py +100 -0
- swatplus_builder/editor/vendored/fileio/init.py +272 -0
- swatplus_builder/editor/vendored/fileio/lum.py +373 -0
- swatplus_builder/editor/vendored/fileio/ops.py +259 -0
- swatplus_builder/editor/vendored/fileio/recall.py +199 -0
- swatplus_builder/editor/vendored/fileio/regions.py +132 -0
- swatplus_builder/editor/vendored/fileio/reservoir.py +223 -0
- swatplus_builder/editor/vendored/fileio/routing_unit.py +136 -0
- swatplus_builder/editor/vendored/fileio/salts.py +1152 -0
- swatplus_builder/editor/vendored/fileio/simulation.py +284 -0
- swatplus_builder/editor/vendored/fileio/soils.py +114 -0
- swatplus_builder/editor/vendored/fileio/structural.py +243 -0
- swatplus_builder/editor/vendored/fileio/water_rights.py +170 -0
- swatplus_builder/editor/vendored/get-pip.py +32992 -0
- swatplus_builder/editor/vendored/helpers/executable_api.py +19 -0
- swatplus_builder/editor/vendored/helpers/table_mapper.py +186 -0
- swatplus_builder/editor/vendored/helpers/utils.py +193 -0
- swatplus_builder/editor/vendored/python-build-linux.sh +3 -0
- swatplus_builder/editor/vendored/python-build-mac.sh +3 -0
- swatplus_builder/editor/vendored/python-build-windows.bat +2 -0
- swatplus_builder/editor/vendored/rest/aquifer.py +340 -0
- swatplus_builder/editor/vendored/rest/auto_complete.py +261 -0
- swatplus_builder/editor/vendored/rest/basin.py +23 -0
- swatplus_builder/editor/vendored/rest/change.py +362 -0
- swatplus_builder/editor/vendored/rest/channel.py +461 -0
- swatplus_builder/editor/vendored/rest/climate.py +457 -0
- swatplus_builder/editor/vendored/rest/config.py +17 -0
- swatplus_builder/editor/vendored/rest/decision_table.py +296 -0
- swatplus_builder/editor/vendored/rest/defaults.py +608 -0
- swatplus_builder/editor/vendored/rest/definitions.py +60 -0
- swatplus_builder/editor/vendored/rest/gwflow.py +565 -0
- swatplus_builder/editor/vendored/rest/hru.py +306 -0
- swatplus_builder/editor/vendored/rest/hru_lte.py +212 -0
- swatplus_builder/editor/vendored/rest/hru_parm_db.py +336 -0
- swatplus_builder/editor/vendored/rest/hydrology.py +110 -0
- swatplus_builder/editor/vendored/rest/init.py +478 -0
- swatplus_builder/editor/vendored/rest/lum.py +504 -0
- swatplus_builder/editor/vendored/rest/ops.py +252 -0
- swatplus_builder/editor/vendored/rest/recall.py +331 -0
- swatplus_builder/editor/vendored/rest/regions.py +132 -0
- swatplus_builder/editor/vendored/rest/reservoir.py +723 -0
- swatplus_builder/editor/vendored/rest/routing_unit.py +248 -0
- swatplus_builder/editor/vendored/rest/salts.py +871 -0
- swatplus_builder/editor/vendored/rest/setup.py +458 -0
- swatplus_builder/editor/vendored/rest/soils.py +119 -0
- swatplus_builder/editor/vendored/rest/structural.py +212 -0
- swatplus_builder/editor/vendored/rest/water_rights.py +106 -0
- swatplus_builder/editor/vendored/swatplus_api.py +206 -0
- swatplus_builder/editor/vendored/swatplus_rest_api.py +73 -0
- swatplus_builder/errors.py +43 -0
- swatplus_builder/full_mode/__init__.py +0 -0
- swatplus_builder/full_mode/parameter_bridge.py +476 -0
- swatplus_builder/full_mode/routing_fixes.py +303 -0
- swatplus_builder/full_mode/topology_converter.py +367 -0
- swatplus_builder/full_mode/verify_conversion.py +246 -0
- swatplus_builder/full_mode/warmup.py +182 -0
- swatplus_builder/full_mode/water_balance_gate.py +354 -0
- swatplus_builder/gis/__init__.py +17 -0
- swatplus_builder/gis/complexity.py +130 -0
- swatplus_builder/gis/delineation.py +1172 -0
- swatplus_builder/gis/hru.py +910 -0
- swatplus_builder/gis/landuse.py +178 -0
- swatplus_builder/gis/nldi_fallback.py +72 -0
- swatplus_builder/gis/overlay_repair.py +286 -0
- swatplus_builder/gis/soil.py +574 -0
- swatplus_builder/gis/tables.py +653 -0
- swatplus_builder/gis/terrain.py +61 -0
- swatplus_builder/gis/topology.py +84 -0
- swatplus_builder/gis/validate.py +286 -0
- swatplus_builder/mcp/__init__.py +5 -0
- swatplus_builder/mcp/server.py +513 -0
- swatplus_builder/orchestrate.py +280 -0
- swatplus_builder/output/__init__.py +35 -0
- swatplus_builder/output/et_diagnostics.py +344 -0
- swatplus_builder/output/eval.py +682 -0
- swatplus_builder/output/mass_diagnostics.py +279 -0
- swatplus_builder/output/mass_trace.py +2496 -0
- swatplus_builder/output/metadata.py +85 -0
- swatplus_builder/output/metrics.py +279 -0
- swatplus_builder/output/plots/__init__.py +35 -0
- swatplus_builder/output/plots/fdc.py +50 -0
- swatplus_builder/output/plots/hydrograph.py +62 -0
- swatplus_builder/output/plots/residuals.py +51 -0
- swatplus_builder/output/plots/scatter.py +55 -0
- swatplus_builder/output/plots/seasonal.py +55 -0
- swatplus_builder/output/plots/soil.py +78 -0
- swatplus_builder/output/plots/spatial.py +107 -0
- swatplus_builder/output/plots/style.py +32 -0
- swatplus_builder/output/plots/utils.py +117 -0
- swatplus_builder/output/plots/wrapper.py +175 -0
- swatplus_builder/output/reader.py +305 -0
- swatplus_builder/output/realism.py +423 -0
- swatplus_builder/output/summary.py +202 -0
- swatplus_builder/output/volume_diagnostics.py +3109 -0
- swatplus_builder/output/weather_forcing.py +352 -0
- swatplus_builder/params/__init__.py +37 -0
- swatplus_builder/params/governance.py +219 -0
- swatplus_builder/params/registry.py +452 -0
- swatplus_builder/ref/__init__.py +38 -0
- swatplus_builder/ref/bootstrap.py +308 -0
- swatplus_builder/ref/catalog.py +99 -0
- swatplus_builder/run/__init__.py +18 -0
- swatplus_builder/run/swatplus.py +712 -0
- swatplus_builder/sensitivity.py +217 -0
- swatplus_builder/skills/__init__.py +17 -0
- swatplus_builder/skills/swatplus_playbook/README.md +21 -0
- swatplus_builder/skills/swatplus_playbook/__init__.py +13 -0
- swatplus_builder/skills/swatplus_playbook/rules.py +64 -0
- swatplus_builder/skills/swatplus_playbook/schemas.py +54 -0
- swatplus_builder/skills/swatplus_playbook/update.py +41 -0
- swatplus_builder/soil/__init__.py +53 -0
- swatplus_builder/soil/builder.py +157 -0
- swatplus_builder/soil/gnatsgo.py +763 -0
- swatplus_builder/soil/models.py +36 -0
- swatplus_builder/soil/params.py +330 -0
- swatplus_builder/soil/pc.py +242 -0
- swatplus_builder/soil/plot.py +167 -0
- swatplus_builder/soil/sda.py +277 -0
- swatplus_builder/soil/soilgrids.py +114 -0
- swatplus_builder/soil/writer.py +260 -0
- swatplus_builder/tools/__init__.py +18 -0
- swatplus_builder/tools/agent.py +159 -0
- swatplus_builder/types.py +518 -0
- swatplus_builder/validation/__init__.py +6 -0
- swatplus_builder/validation/runner.py +403 -0
- swatplus_builder/weather/__init__.py +29 -0
- swatplus_builder/weather/daymet.py +311 -0
- swatplus_builder/weather/gridmet.py +510 -0
- swatplus_builder/weather/synthetic.py +253 -0
- swatplus_builder/weather/wgn.py +33 -0
- swatplus_builder/weather/writer.py +369 -0
- swatplus_builder/workflows/__init__.py +5 -0
- swatplus_builder/workflows/contracts.py +108 -0
- swatplus_builder/workflows/full_build.py +343 -0
- swatplus_builder/workflows/usgs_e2e.py +2251 -0
- swatplus_builder-0.4.0.dist-info/METADATA +429 -0
- swatplus_builder-0.4.0.dist-info/RECORD +253 -0
- swatplus_builder-0.4.0.dist-info/WHEEL +4 -0
- swatplus_builder-0.4.0.dist-info/entry_points.txt +3 -0
- swatplus_builder-0.4.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""swatplus_builder — headless, agent-friendly generator for SWAT+ project inputs.
|
|
2
|
+
|
|
3
|
+
Public surface is intentionally tiny. See :mod:`swatplus_builder.tools` for the four
|
|
4
|
+
agent-facing functions, or :mod:`swatplus_builder.cli` for the Typer CLI.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
__version__ = "0.4.0"
|
|
14
|
+
|
|
15
|
+
__all__ = ["__version__"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _ensure_writable_matplotlib_config_dir() -> None:
|
|
19
|
+
if os.environ.get("MPLCONFIGDIR"):
|
|
20
|
+
return
|
|
21
|
+
cfg = Path(tempfile.gettempdir()) / "swatplus_builder_mplconfig"
|
|
22
|
+
cfg.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
os.environ["MPLCONFIGDIR"] = str(cfg)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_ensure_writable_matplotlib_config_dir()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Typed run-artifact schemas and hashing helpers.
|
|
2
|
+
|
|
3
|
+
Phase 3B introduces a canonical artifact contract under ``runs/<content_hash>/``.
|
|
4
|
+
This package defines the typed JSON payloads and deterministic hash utilities
|
|
5
|
+
that power content-addressed caching.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .models import (
|
|
9
|
+
ArtifactMetadata,
|
|
10
|
+
ArtifactQuery,
|
|
11
|
+
ArtifactMetrics,
|
|
12
|
+
ArtifactProvenance,
|
|
13
|
+
ArtifactRecord,
|
|
14
|
+
ArtifactSummary,
|
|
15
|
+
OutletMetadata,
|
|
16
|
+
RunConfig,
|
|
17
|
+
)
|
|
18
|
+
from .hashing import canonical_config_json, compute_content_hash
|
|
19
|
+
from .store import ArtifactStore, LocalArtifactStore
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"ArtifactMetadata",
|
|
23
|
+
"ArtifactQuery",
|
|
24
|
+
"ArtifactMetrics",
|
|
25
|
+
"ArtifactProvenance",
|
|
26
|
+
"ArtifactRecord",
|
|
27
|
+
"ArtifactSummary",
|
|
28
|
+
"ArtifactStore",
|
|
29
|
+
"LocalArtifactStore",
|
|
30
|
+
"OutletMetadata",
|
|
31
|
+
"RunConfig",
|
|
32
|
+
"canonical_config_json",
|
|
33
|
+
"compute_content_hash",
|
|
34
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Deterministic content-hash utilities for run artifacts.
|
|
2
|
+
|
|
3
|
+
Content hash contract (Roadmap Appendix A):
|
|
4
|
+
|
|
5
|
+
SHA256(canonical_json(config) || engine_version || builder_git_sha)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
|
|
13
|
+
from .models import RunConfig
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def canonical_config_json(config: RunConfig | dict[str, object]) -> bytes:
|
|
17
|
+
"""Return canonical UTF-8 JSON bytes for content hashing.
|
|
18
|
+
|
|
19
|
+
Canonicalization rules:
|
|
20
|
+
- sorted keys
|
|
21
|
+
- no extra whitespace
|
|
22
|
+
- UTF-8 encoding
|
|
23
|
+
"""
|
|
24
|
+
payload: dict[str, object]
|
|
25
|
+
if isinstance(config, RunConfig):
|
|
26
|
+
payload = config.model_dump(mode="json")
|
|
27
|
+
else:
|
|
28
|
+
payload = RunConfig.model_validate(config).model_dump(mode="json")
|
|
29
|
+
txt = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
|
30
|
+
return txt.encode("utf-8")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def compute_content_hash(
|
|
34
|
+
config: RunConfig | dict[str, object],
|
|
35
|
+
*,
|
|
36
|
+
engine_version: str,
|
|
37
|
+
builder_git_sha: str,
|
|
38
|
+
) -> str:
|
|
39
|
+
"""Compute the Phase 3B content hash.
|
|
40
|
+
|
|
41
|
+
Failure modes:
|
|
42
|
+
- Raises `pydantic.ValidationError` when `config` is invalid.
|
|
43
|
+
"""
|
|
44
|
+
h = hashlib.sha256()
|
|
45
|
+
h.update(canonical_config_json(config))
|
|
46
|
+
h.update(engine_version.encode("utf-8"))
|
|
47
|
+
h.update(builder_git_sha.encode("utf-8"))
|
|
48
|
+
return h.hexdigest()
|
|
49
|
+
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Pydantic schemas for Phase 3B run artifacts.
|
|
2
|
+
|
|
3
|
+
These models correspond to the JSON files defined in `ROADMAP.md` Appendix A:
|
|
4
|
+
`config.json`, `metadata.json`, `metrics.json`, and `provenance.json`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from datetime import date
|
|
10
|
+
from typing import Literal, TypeAlias
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
JsonScalar: TypeAlias = str | int | float | bool | None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ParameterValue(BaseModel):
|
|
18
|
+
"""One calibration parameter assignment within `config.parameters`."""
|
|
19
|
+
|
|
20
|
+
value: float = Field(..., description="Numeric parameter value.")
|
|
21
|
+
scope: Literal["global", "hru", "subbasin", "channel"] = Field(
|
|
22
|
+
..., description="Application scope for this parameter value."
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RunConfig(BaseModel):
|
|
27
|
+
"""Artifact `config.json` model used for content hashing."""
|
|
28
|
+
|
|
29
|
+
basin_id: str = Field(..., min_length=1)
|
|
30
|
+
bbox: tuple[float, float, float, float] | None = Field(
|
|
31
|
+
default=None,
|
|
32
|
+
description="Bounding box [minx, miny, maxx, maxy] in lon/lat.",
|
|
33
|
+
)
|
|
34
|
+
simulation_start: date = Field(...)
|
|
35
|
+
simulation_end: date = Field(...)
|
|
36
|
+
parameters: dict[str, ParameterValue] = Field(default_factory=dict)
|
|
37
|
+
options: dict[str, JsonScalar] = Field(default_factory=dict)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OutletMetadata(BaseModel):
|
|
41
|
+
"""Outlet selection metadata in `metadata.json`."""
|
|
42
|
+
|
|
43
|
+
gis_id: int | None = Field(default=None)
|
|
44
|
+
auto_detected: bool = Field(default=False)
|
|
45
|
+
reason: str | None = Field(default=None)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ArtifactMetadata(BaseModel):
|
|
49
|
+
"""Artifact `metadata.json` model (provenance, non-hash inputs)."""
|
|
50
|
+
|
|
51
|
+
run_id: str | None = Field(default=None, description="Content hash / run identifier.")
|
|
52
|
+
timestamp_utc: str = Field(..., description="UTC ISO-8601 timestamp.")
|
|
53
|
+
engine_version: str | None = Field(default=None)
|
|
54
|
+
builder_version: str | None = Field(default=None)
|
|
55
|
+
git_sha: str | None = Field(default=None)
|
|
56
|
+
outlet: OutletMetadata | None = Field(default=None)
|
|
57
|
+
soil_mode: Literal["high_fidelity", "fallback", "synthetic"] | None = Field(default=None)
|
|
58
|
+
pct_fallback_soils: float | None = Field(default=None, ge=0.0, le=1.0)
|
|
59
|
+
weather_coverage: dict[str, float] = Field(default_factory=dict)
|
|
60
|
+
n_subbasins: int | None = Field(default=None, ge=0)
|
|
61
|
+
n_hrus: int | None = Field(default=None, ge=0)
|
|
62
|
+
runtime_seconds: float | None = Field(default=None, ge=0.0)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class MetricsPeriod(BaseModel):
|
|
66
|
+
"""Evaluation period included in `metrics.json`."""
|
|
67
|
+
|
|
68
|
+
start: date = Field(...)
|
|
69
|
+
end: date = Field(...)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ArtifactMetrics(BaseModel):
|
|
73
|
+
"""Artifact `metrics.json` model."""
|
|
74
|
+
|
|
75
|
+
outlet_id: int | None = Field(default=None)
|
|
76
|
+
period: MetricsPeriod | None = Field(default=None)
|
|
77
|
+
nse: float | None = Field(default=None)
|
|
78
|
+
log_nse: float | None = Field(default=None)
|
|
79
|
+
kge: float | None = Field(default=None)
|
|
80
|
+
pbias: float | None = Field(default=None)
|
|
81
|
+
bfi_observed: float | None = Field(default=None)
|
|
82
|
+
bfi_simulated: float | None = Field(default=None)
|
|
83
|
+
peak_flow_error_pct: float | None = Field(default=None)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class AgentContext(BaseModel):
|
|
87
|
+
"""Agent context fields in `provenance.json`."""
|
|
88
|
+
|
|
89
|
+
agent_id: str | None = Field(default=None)
|
|
90
|
+
experiment_id: str | None = Field(default=None)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ArtifactProvenance(BaseModel):
|
|
94
|
+
"""Artifact `provenance.json` model."""
|
|
95
|
+
|
|
96
|
+
parent_run: str | None = Field(default=None)
|
|
97
|
+
proposal_source: str | None = Field(default=None)
|
|
98
|
+
agent_context: AgentContext | None = Field(default=None)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ArtifactRecord(BaseModel):
|
|
102
|
+
"""In-memory representation of one artifact record."""
|
|
103
|
+
|
|
104
|
+
content_hash: str = Field(..., min_length=8)
|
|
105
|
+
config: RunConfig
|
|
106
|
+
metadata: ArtifactMetadata
|
|
107
|
+
metrics: ArtifactMetrics | None = Field(default=None)
|
|
108
|
+
provenance: ArtifactProvenance | None = Field(default=None)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ArtifactSummary(BaseModel):
|
|
112
|
+
"""Lightweight listing payload used by `ArtifactStore.query()`."""
|
|
113
|
+
|
|
114
|
+
content_hash: str = Field(..., min_length=8)
|
|
115
|
+
basin_id: str = Field(..., min_length=1)
|
|
116
|
+
simulation_start: date = Field(...)
|
|
117
|
+
simulation_end: date = Field(...)
|
|
118
|
+
soil_mode: Literal["high_fidelity", "fallback", "synthetic"] | None = Field(default=None)
|
|
119
|
+
nse: float | None = Field(default=None)
|
|
120
|
+
parent_run: str | None = Field(default=None)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ArtifactQuery(BaseModel):
|
|
124
|
+
"""Optional filters for artifact query/list operations."""
|
|
125
|
+
|
|
126
|
+
basin_id: str | None = Field(default=None)
|
|
127
|
+
soil_mode: Literal["high_fidelity", "fallback", "synthetic"] | None = Field(default=None)
|
|
128
|
+
nse_min: float | None = Field(default=None)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Local run-artifact storage (Phase 3B.2).
|
|
2
|
+
|
|
3
|
+
Directory contract:
|
|
4
|
+
|
|
5
|
+
<root>/runs/<content_hash>/
|
|
6
|
+
config.json
|
|
7
|
+
metadata.json
|
|
8
|
+
metrics.json (optional)
|
|
9
|
+
provenance.json (optional)
|
|
10
|
+
timeseries.parquet (optional)
|
|
11
|
+
plots/* (optional)
|
|
12
|
+
logs/* (optional)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import shutil
|
|
19
|
+
from abc import ABC, abstractmethod
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Iterable
|
|
22
|
+
|
|
23
|
+
from .models import (
|
|
24
|
+
ArtifactMetadata,
|
|
25
|
+
ArtifactMetrics,
|
|
26
|
+
ArtifactProvenance,
|
|
27
|
+
ArtifactQuery,
|
|
28
|
+
ArtifactRecord,
|
|
29
|
+
ArtifactSummary,
|
|
30
|
+
RunConfig,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ArtifactStore(ABC):
|
|
35
|
+
"""Abstract artifact store backend."""
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def write(
|
|
39
|
+
self,
|
|
40
|
+
record: ArtifactRecord,
|
|
41
|
+
*,
|
|
42
|
+
timeseries_parquet: Path | None = None,
|
|
43
|
+
plot_files: Iterable[Path] = (),
|
|
44
|
+
log_files: Iterable[Path] = (),
|
|
45
|
+
) -> Path:
|
|
46
|
+
"""Persist one artifact record and optional payload files."""
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def read(self, content_hash: str) -> ArtifactRecord:
|
|
50
|
+
"""Read one artifact record by hash."""
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def exists(self, content_hash: str) -> bool:
|
|
54
|
+
"""Return true when artifact directory exists."""
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def query(self, filters: ArtifactQuery | None = None) -> list[ArtifactSummary]:
|
|
58
|
+
"""List artifact summaries, optionally filtered."""
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def lineage(self, content_hash: str) -> list[str]:
|
|
62
|
+
"""Return parent chain starting at `content_hash`."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class LocalArtifactStore(ArtifactStore):
|
|
66
|
+
"""Filesystem artifact store rooted at `<root>/runs`."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, root: Path | str):
|
|
69
|
+
self.root = Path(root).expanduser().resolve()
|
|
70
|
+
self.runs_dir = self.root / "runs"
|
|
71
|
+
self.runs_dir.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
|
|
73
|
+
def write(
|
|
74
|
+
self,
|
|
75
|
+
record: ArtifactRecord,
|
|
76
|
+
*,
|
|
77
|
+
timeseries_parquet: Path | None = None,
|
|
78
|
+
plot_files: Iterable[Path] = (),
|
|
79
|
+
log_files: Iterable[Path] = (),
|
|
80
|
+
) -> Path:
|
|
81
|
+
run_dir = self._run_dir(record.content_hash)
|
|
82
|
+
run_dir.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
|
|
84
|
+
self._write_json(run_dir / "config.json", record.config.model_dump(mode="json"))
|
|
85
|
+
self._write_json(run_dir / "metadata.json", record.metadata.model_dump(mode="json"))
|
|
86
|
+
if record.metrics is not None:
|
|
87
|
+
self._write_json(run_dir / "metrics.json", record.metrics.model_dump(mode="json"))
|
|
88
|
+
if record.provenance is not None:
|
|
89
|
+
self._write_json(run_dir / "provenance.json", record.provenance.model_dump(mode="json"))
|
|
90
|
+
|
|
91
|
+
if timeseries_parquet is not None:
|
|
92
|
+
dst = run_dir / "timeseries.parquet"
|
|
93
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
shutil.copy2(timeseries_parquet, dst)
|
|
95
|
+
|
|
96
|
+
self._copy_many(plot_files, run_dir / "plots")
|
|
97
|
+
self._copy_many(log_files, run_dir / "logs")
|
|
98
|
+
return run_dir
|
|
99
|
+
|
|
100
|
+
def read(self, content_hash: str) -> ArtifactRecord:
|
|
101
|
+
run_dir = self._run_dir(content_hash)
|
|
102
|
+
config = RunConfig.model_validate(self._read_json(run_dir / "config.json"))
|
|
103
|
+
metadata = ArtifactMetadata.model_validate(self._read_json(run_dir / "metadata.json"))
|
|
104
|
+
|
|
105
|
+
metrics_path = run_dir / "metrics.json"
|
|
106
|
+
prov_path = run_dir / "provenance.json"
|
|
107
|
+
metrics = (
|
|
108
|
+
ArtifactMetrics.model_validate(self._read_json(metrics_path))
|
|
109
|
+
if metrics_path.exists()
|
|
110
|
+
else None
|
|
111
|
+
)
|
|
112
|
+
provenance = (
|
|
113
|
+
ArtifactProvenance.model_validate(self._read_json(prov_path))
|
|
114
|
+
if prov_path.exists()
|
|
115
|
+
else None
|
|
116
|
+
)
|
|
117
|
+
return ArtifactRecord(
|
|
118
|
+
content_hash=content_hash,
|
|
119
|
+
config=config,
|
|
120
|
+
metadata=metadata,
|
|
121
|
+
metrics=metrics,
|
|
122
|
+
provenance=provenance,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def exists(self, content_hash: str) -> bool:
|
|
126
|
+
return self._run_dir(content_hash).is_dir()
|
|
127
|
+
|
|
128
|
+
def query(self, filters: ArtifactQuery | None = None) -> list[ArtifactSummary]:
|
|
129
|
+
q = filters or ArtifactQuery()
|
|
130
|
+
out: list[ArtifactSummary] = []
|
|
131
|
+
for run_dir in sorted(self.runs_dir.iterdir()):
|
|
132
|
+
if not run_dir.is_dir():
|
|
133
|
+
continue
|
|
134
|
+
try:
|
|
135
|
+
record = self.read(run_dir.name)
|
|
136
|
+
except Exception:
|
|
137
|
+
continue
|
|
138
|
+
summary = ArtifactSummary(
|
|
139
|
+
content_hash=record.content_hash,
|
|
140
|
+
basin_id=record.config.basin_id,
|
|
141
|
+
simulation_start=record.config.simulation_start,
|
|
142
|
+
simulation_end=record.config.simulation_end,
|
|
143
|
+
soil_mode=record.metadata.soil_mode,
|
|
144
|
+
nse=record.metrics.nse if record.metrics is not None else None,
|
|
145
|
+
parent_run=record.provenance.parent_run if record.provenance is not None else None,
|
|
146
|
+
)
|
|
147
|
+
if not self._match(summary, q):
|
|
148
|
+
continue
|
|
149
|
+
out.append(summary)
|
|
150
|
+
return out
|
|
151
|
+
|
|
152
|
+
def lineage(self, content_hash: str) -> list[str]:
|
|
153
|
+
chain: list[str] = []
|
|
154
|
+
seen: set[str] = set()
|
|
155
|
+
current = content_hash
|
|
156
|
+
while current and current not in seen and self.exists(current):
|
|
157
|
+
seen.add(current)
|
|
158
|
+
chain.append(current)
|
|
159
|
+
record = self.read(current)
|
|
160
|
+
parent = record.provenance.parent_run if record.provenance is not None else None
|
|
161
|
+
if not parent:
|
|
162
|
+
break
|
|
163
|
+
current = parent
|
|
164
|
+
return chain
|
|
165
|
+
|
|
166
|
+
def _run_dir(self, content_hash: str) -> Path:
|
|
167
|
+
return self.runs_dir / content_hash
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def _write_json(path: Path, payload: dict[str, object]) -> None:
|
|
171
|
+
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _read_json(path: Path) -> dict[str, object]:
|
|
175
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def _copy_many(files: Iterable[Path], out_dir: Path) -> None:
|
|
179
|
+
copied = False
|
|
180
|
+
for src in files:
|
|
181
|
+
if not src.exists():
|
|
182
|
+
continue
|
|
183
|
+
if not copied:
|
|
184
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
185
|
+
copied = True
|
|
186
|
+
shutil.copy2(src, out_dir / src.name)
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def _match(summary: ArtifactSummary, q: ArtifactQuery) -> bool:
|
|
190
|
+
if q.basin_id is not None and summary.basin_id != q.basin_id:
|
|
191
|
+
return False
|
|
192
|
+
if q.soil_mode is not None and summary.soil_mode != q.soil_mode:
|
|
193
|
+
return False
|
|
194
|
+
if q.nse_min is not None:
|
|
195
|
+
if summary.nse is None or summary.nse < q.nse_min:
|
|
196
|
+
return False
|
|
197
|
+
return True
|
|
198
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Autoresearch loop primitives (Phase 3D)."""
|
|
2
|
+
|
|
3
|
+
from .loop import (
|
|
4
|
+
LoopIterationResult,
|
|
5
|
+
LoopRequest,
|
|
6
|
+
LoopResult,
|
|
7
|
+
LoopStoppingCriteria,
|
|
8
|
+
SurrogatePrediction,
|
|
9
|
+
run_autoresearch_loop,
|
|
10
|
+
)
|
|
11
|
+
from .surrogate import (
|
|
12
|
+
HoldoutEvaluationCase,
|
|
13
|
+
HoldoutEvaluationReport,
|
|
14
|
+
HoldoutEvaluationRequest,
|
|
15
|
+
RoutingDecision,
|
|
16
|
+
SurrogateEnsemble,
|
|
17
|
+
SurrogateMember,
|
|
18
|
+
SurrogatePredictionEstimate,
|
|
19
|
+
SurrogateTrainingRequest,
|
|
20
|
+
decide_routing_path,
|
|
21
|
+
evaluate_surrogate_holdout,
|
|
22
|
+
make_loop_surrogate_predictor,
|
|
23
|
+
predict_with_surrogate,
|
|
24
|
+
train_surrogate_ensemble,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"LoopIterationResult",
|
|
29
|
+
"LoopRequest",
|
|
30
|
+
"LoopResult",
|
|
31
|
+
"LoopStoppingCriteria",
|
|
32
|
+
"SurrogatePrediction",
|
|
33
|
+
"run_autoresearch_loop",
|
|
34
|
+
"HoldoutEvaluationCase",
|
|
35
|
+
"HoldoutEvaluationReport",
|
|
36
|
+
"HoldoutEvaluationRequest",
|
|
37
|
+
"RoutingDecision",
|
|
38
|
+
"SurrogateEnsemble",
|
|
39
|
+
"SurrogateMember",
|
|
40
|
+
"SurrogatePredictionEstimate",
|
|
41
|
+
"SurrogateTrainingRequest",
|
|
42
|
+
"decide_routing_path",
|
|
43
|
+
"evaluate_surrogate_holdout",
|
|
44
|
+
"make_loop_surrogate_predictor",
|
|
45
|
+
"predict_with_surrogate",
|
|
46
|
+
"train_surrogate_ensemble",
|
|
47
|
+
]
|