fresh-vdyp 0.1.0__tar.gz
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.
- fresh_vdyp-0.1.0/LICENSE +21 -0
- fresh_vdyp-0.1.0/PKG-INFO +126 -0
- fresh_vdyp-0.1.0/README.md +110 -0
- fresh_vdyp-0.1.0/pyproject.toml +92 -0
- fresh_vdyp-0.1.0/scripts/assistant_tools.py +262 -0
- fresh_vdyp-0.1.0/scripts/bundle_service.py +236 -0
- fresh_vdyp-0.1.0/scripts/check_workflow.py +1194 -0
- fresh_vdyp-0.1.0/scripts/coe_parser.py +600 -0
- fresh_vdyp-0.1.0/scripts/config_parser.py +566 -0
- fresh_vdyp-0.1.0/scripts/curve_api.py +566 -0
- fresh_vdyp-0.1.0/scripts/dashboard.py +313 -0
- fresh_vdyp-0.1.0/scripts/equations.py +1051 -0
- fresh_vdyp-0.1.0/scripts/fresh_vdyp/__init__.py +58 -0
- fresh_vdyp-0.1.0/scripts/fresh_vdyp.egg-info/PKG-INFO +126 -0
- fresh_vdyp-0.1.0/scripts/fresh_vdyp.egg-info/SOURCES.txt +61 -0
- fresh_vdyp-0.1.0/scripts/fresh_vdyp.egg-info/dependency_links.txt +1 -0
- fresh_vdyp-0.1.0/scripts/fresh_vdyp.egg-info/entry_points.txt +3 -0
- fresh_vdyp-0.1.0/scripts/fresh_vdyp.egg-info/requires.txt +5 -0
- fresh_vdyp-0.1.0/scripts/fresh_vdyp.egg-info/top_level.txt +20 -0
- fresh_vdyp-0.1.0/scripts/fvdyp_api.py +317 -0
- fresh_vdyp-0.1.0/scripts/fvdyp_cli.py +259 -0
- fresh_vdyp-0.1.0/scripts/inventory_vdyp.py +381 -0
- fresh_vdyp-0.1.0/scripts/key_inference.py +492 -0
- fresh_vdyp-0.1.0/scripts/normalize.py +459 -0
- fresh_vdyp-0.1.0/scripts/provenance.py +383 -0
- fresh_vdyp-0.1.0/scripts/run_console.py +492 -0
- fresh_vdyp-0.1.0/scripts/vdyp_control.py +500 -0
- fresh_vdyp-0.1.0/scripts/vdyp_map.py +349 -0
- fresh_vdyp-0.1.0/scripts/vdyp_runtime.py +355 -0
- fresh_vdyp-0.1.0/scripts/yield_parser.py +445 -0
- fresh_vdyp-0.1.0/setup.cfg +4 -0
- fresh_vdyp-0.1.0/tests/test_api_docstrings.py +65 -0
- fresh_vdyp-0.1.0/tests/test_assistant_tools.py +96 -0
- fresh_vdyp-0.1.0/tests/test_bundle_service.py +202 -0
- fresh_vdyp-0.1.0/tests/test_check_workflow.py +809 -0
- fresh_vdyp-0.1.0/tests/test_cli.py +75 -0
- fresh_vdyp-0.1.0/tests/test_coe_parser.py +220 -0
- fresh_vdyp-0.1.0/tests/test_coe_schemas.py +41 -0
- fresh_vdyp-0.1.0/tests/test_config_parser.py +157 -0
- fresh_vdyp-0.1.0/tests/test_console_fixture.py +66 -0
- fresh_vdyp-0.1.0/tests/test_curve_api.py +180 -0
- fresh_vdyp-0.1.0/tests/test_curve_properties.py +54 -0
- fresh_vdyp-0.1.0/tests/test_dashboard.py +217 -0
- fresh_vdyp-0.1.0/tests/test_end_to_end.py +67 -0
- fresh_vdyp-0.1.0/tests/test_equation_matrix.py +56 -0
- fresh_vdyp-0.1.0/tests/test_equations.py +674 -0
- fresh_vdyp-0.1.0/tests/test_fixtures.py +63 -0
- fresh_vdyp-0.1.0/tests/test_fvdyp_api.py +125 -0
- fresh_vdyp-0.1.0/tests/test_fvdyp_cli.py +148 -0
- fresh_vdyp-0.1.0/tests/test_inventory_vdyp.py +227 -0
- fresh_vdyp-0.1.0/tests/test_key_inference.py +206 -0
- fresh_vdyp-0.1.0/tests/test_normalize.py +224 -0
- fresh_vdyp-0.1.0/tests/test_package_metadata.py +34 -0
- fresh_vdyp-0.1.0/tests/test_parser_properties.py +38 -0
- fresh_vdyp-0.1.0/tests/test_properties.py +110 -0
- fresh_vdyp-0.1.0/tests/test_provenance.py +170 -0
- fresh_vdyp-0.1.0/tests/test_release_regression.py +63 -0
- fresh_vdyp-0.1.0/tests/test_run_console.py +265 -0
- fresh_vdyp-0.1.0/tests/test_vdyp_control.py +215 -0
- fresh_vdyp-0.1.0/tests/test_vdyp_map.py +223 -0
- fresh_vdyp-0.1.0/tests/test_vdyp_runtime.py +213 -0
- fresh_vdyp-0.1.0/tests/test_workflow.py +19 -0
- fresh_vdyp-0.1.0/tests/test_yield_parser.py +247 -0
fresh_vdyp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 UBC FRESH Lab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fresh-vdyp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A public interoperability layer for the BC VDYP7 coefficient bundle, console runner, and yield curves.
|
|
5
|
+
Author: UBC FRESH Lab
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: fastapi>=0.110
|
|
11
|
+
Requires-Dist: httpx>=0.27
|
|
12
|
+
Requires-Dist: pydantic>=2
|
|
13
|
+
Requires-Dist: typer>=0.12
|
|
14
|
+
Requires-Dist: uvicorn>=0.29
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# fresh-vdyp
|
|
18
|
+
|
|
19
|
+
A public, tested Python package for reading the BC VDYP7 coefficient and control
|
|
20
|
+
bundle, wrapping the VDYP7Console runner, exposing typed yield curves, and
|
|
21
|
+
validating incremental Python growth equations against the official console
|
|
22
|
+
outputs.
|
|
23
|
+
|
|
24
|
+
`fresh-vdyp` is an interoperability and reimplementation layer. It does not
|
|
25
|
+
claim authorship of VDYP7 or BC government data. It exists so the model's
|
|
26
|
+
plain-text coefficients, control files, and yield tables can be catalogued,
|
|
27
|
+
queried, tested, and used by tools such as FEMIC without a GUI boundary.
|
|
28
|
+
|
|
29
|
+
Repository: <https://github.com/UBC-FRESH/fresh-vdyp>
|
|
30
|
+
|
|
31
|
+
## Status
|
|
32
|
+
|
|
33
|
+
All phases P0-P7 are complete. `ROADMAP.md` lists the phases,
|
|
34
|
+
and GitHub issues carry the live status. The first working milestone is the
|
|
35
|
+
source inventory and coefficient catalog, not a full VDYP growth-model rewrite.
|
|
36
|
+
|
|
37
|
+
## What this is
|
|
38
|
+
|
|
39
|
+
- A parser and catalog for the VDYP7 `VDYP_CFG/coe` bundle.
|
|
40
|
+
- A control-file map for `VDYP.CTR`, `VDYPBACK.CTR`, `.COE`, `.PRM`, and `.DAT`.
|
|
41
|
+
- A runner and parser for `VDYP7Console` CSV yield tables.
|
|
42
|
+
- A typed Python curve API and public data assets.
|
|
43
|
+
- An incremental Python equation layer validated one family at a time.
|
|
44
|
+
- A Typer CLI, FastAPI service, FRESH-style web UI, and bounded LLM assistant.
|
|
45
|
+
- A package that FEMIC and other UBC-FRESH tools can depend on.
|
|
46
|
+
|
|
47
|
+
## What this is not
|
|
48
|
+
|
|
49
|
+
- A claim that UBC-FRESH invented VDYP7.
|
|
50
|
+
- A byte-for-byte clone of the native VDYP7 binaries.
|
|
51
|
+
- A substitute for measured field data or a complete growth-model validation.
|
|
52
|
+
- A place to commit raw government model bundles or credentials.
|
|
53
|
+
|
|
54
|
+
## Source and attribution
|
|
55
|
+
|
|
56
|
+
Every extracted coefficient, control file, curve, and derived value records:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
source
|
|
60
|
+
source version
|
|
61
|
+
source file
|
|
62
|
+
source SHA256
|
|
63
|
+
extraction date
|
|
64
|
+
citation
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The original source remains the BC government's work. Reimplemented logic is
|
|
68
|
+
labelled `reimplemented` and validated against console outputs where possible.
|
|
69
|
+
|
|
70
|
+
## Plan
|
|
71
|
+
|
|
72
|
+
Read these in order:
|
|
73
|
+
|
|
74
|
+
1. [`notes/plan/0001-program-plan.md`](notes/plan/0001-program-plan.md)
|
|
75
|
+
2. [`notes/plan/0002-data-provenance.md`](notes/plan/0002-data-provenance.md)
|
|
76
|
+
3. [`notes/plan/0003-architecture.md`](notes/plan/0003-architecture.md)
|
|
77
|
+
4. [`notes/plan/0004-validation-release.md`](notes/plan/0004-validation-release.md)
|
|
78
|
+
5. [`notes/reference/vdyp-inventory.md`](notes/reference/vdyp-inventory.md)
|
|
79
|
+
6. [`notes/reference/vdyp-model-map.md`](notes/reference/vdyp-model-map.md)
|
|
80
|
+
|
|
81
|
+
## Quick start
|
|
82
|
+
|
|
83
|
+
This seed is a planning repository. The Python package and data workflows land
|
|
84
|
+
in Phase 2. Until then, the workflow gates are:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
python -m venv .venv
|
|
88
|
+
. .venv/bin/activate
|
|
89
|
+
python -m pip install -r requirements-dev.txt
|
|
90
|
+
python scripts/dashboard.py
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Bundle inspection
|
|
94
|
+
|
|
95
|
+
Until the installable package lands, the first commands run from the repository:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
export VDYP_CFG_DIR=/path/to/VDYP_CFG
|
|
99
|
+
python scripts/fvdyp_cli.py bundle validate
|
|
100
|
+
python scripts/fvdyp_cli.py bundle catalog
|
|
101
|
+
python scripts/fvdyp_cli.py bundle extract --output-dir data/normalized
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`validate` checks required files, control targets, and key lookups. `catalog`
|
|
105
|
+
prints the family and row counts. `extract` writes normalized CSV files plus a
|
|
106
|
+
provenance manifest; the CSV files stay under ignored `data/normalized/`.
|
|
107
|
+
|
|
108
|
+
## Repository map
|
|
109
|
+
|
|
110
|
+
| Path | Purpose |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| `AGENTS.md` | Operating contract for coding agents |
|
|
113
|
+
| `HANDOFF.md` | First-read handoff note for a new session agent |
|
|
114
|
+
| `ROADMAP.md` | Plan of record and issue tracker map |
|
|
115
|
+
| `CHANGE_LOG.md` | Dated narrative of changes |
|
|
116
|
+
| `notes/plan/` | Detailed program, data, architecture, and validation plans |
|
|
117
|
+
| `notes/reference/` | VDYP inventory and coefficient-map facts |
|
|
118
|
+
| `notes/adr/` | Accepted design decisions |
|
|
119
|
+
| `docs/` | Sphinx RTFM surface |
|
|
120
|
+
| `scripts/check_workflow.py` | Workflow validator |
|
|
121
|
+
| `tests/` | pytest suite, including planning-surface checks |
|
|
122
|
+
|
|
123
|
+
## License and citation
|
|
124
|
+
|
|
125
|
+
Code is MIT licensed. Coefficient files and model outputs retain their original
|
|
126
|
+
BC government attribution. Cite the source model and file, not only this package.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# fresh-vdyp
|
|
2
|
+
|
|
3
|
+
A public, tested Python package for reading the BC VDYP7 coefficient and control
|
|
4
|
+
bundle, wrapping the VDYP7Console runner, exposing typed yield curves, and
|
|
5
|
+
validating incremental Python growth equations against the official console
|
|
6
|
+
outputs.
|
|
7
|
+
|
|
8
|
+
`fresh-vdyp` is an interoperability and reimplementation layer. It does not
|
|
9
|
+
claim authorship of VDYP7 or BC government data. It exists so the model's
|
|
10
|
+
plain-text coefficients, control files, and yield tables can be catalogued,
|
|
11
|
+
queried, tested, and used by tools such as FEMIC without a GUI boundary.
|
|
12
|
+
|
|
13
|
+
Repository: <https://github.com/UBC-FRESH/fresh-vdyp>
|
|
14
|
+
|
|
15
|
+
## Status
|
|
16
|
+
|
|
17
|
+
All phases P0-P7 are complete. `ROADMAP.md` lists the phases,
|
|
18
|
+
and GitHub issues carry the live status. The first working milestone is the
|
|
19
|
+
source inventory and coefficient catalog, not a full VDYP growth-model rewrite.
|
|
20
|
+
|
|
21
|
+
## What this is
|
|
22
|
+
|
|
23
|
+
- A parser and catalog for the VDYP7 `VDYP_CFG/coe` bundle.
|
|
24
|
+
- A control-file map for `VDYP.CTR`, `VDYPBACK.CTR`, `.COE`, `.PRM`, and `.DAT`.
|
|
25
|
+
- A runner and parser for `VDYP7Console` CSV yield tables.
|
|
26
|
+
- A typed Python curve API and public data assets.
|
|
27
|
+
- An incremental Python equation layer validated one family at a time.
|
|
28
|
+
- A Typer CLI, FastAPI service, FRESH-style web UI, and bounded LLM assistant.
|
|
29
|
+
- A package that FEMIC and other UBC-FRESH tools can depend on.
|
|
30
|
+
|
|
31
|
+
## What this is not
|
|
32
|
+
|
|
33
|
+
- A claim that UBC-FRESH invented VDYP7.
|
|
34
|
+
- A byte-for-byte clone of the native VDYP7 binaries.
|
|
35
|
+
- A substitute for measured field data or a complete growth-model validation.
|
|
36
|
+
- A place to commit raw government model bundles or credentials.
|
|
37
|
+
|
|
38
|
+
## Source and attribution
|
|
39
|
+
|
|
40
|
+
Every extracted coefficient, control file, curve, and derived value records:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
source
|
|
44
|
+
source version
|
|
45
|
+
source file
|
|
46
|
+
source SHA256
|
|
47
|
+
extraction date
|
|
48
|
+
citation
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The original source remains the BC government's work. Reimplemented logic is
|
|
52
|
+
labelled `reimplemented` and validated against console outputs where possible.
|
|
53
|
+
|
|
54
|
+
## Plan
|
|
55
|
+
|
|
56
|
+
Read these in order:
|
|
57
|
+
|
|
58
|
+
1. [`notes/plan/0001-program-plan.md`](notes/plan/0001-program-plan.md)
|
|
59
|
+
2. [`notes/plan/0002-data-provenance.md`](notes/plan/0002-data-provenance.md)
|
|
60
|
+
3. [`notes/plan/0003-architecture.md`](notes/plan/0003-architecture.md)
|
|
61
|
+
4. [`notes/plan/0004-validation-release.md`](notes/plan/0004-validation-release.md)
|
|
62
|
+
5. [`notes/reference/vdyp-inventory.md`](notes/reference/vdyp-inventory.md)
|
|
63
|
+
6. [`notes/reference/vdyp-model-map.md`](notes/reference/vdyp-model-map.md)
|
|
64
|
+
|
|
65
|
+
## Quick start
|
|
66
|
+
|
|
67
|
+
This seed is a planning repository. The Python package and data workflows land
|
|
68
|
+
in Phase 2. Until then, the workflow gates are:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
python -m venv .venv
|
|
72
|
+
. .venv/bin/activate
|
|
73
|
+
python -m pip install -r requirements-dev.txt
|
|
74
|
+
python scripts/dashboard.py
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Bundle inspection
|
|
78
|
+
|
|
79
|
+
Until the installable package lands, the first commands run from the repository:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
export VDYP_CFG_DIR=/path/to/VDYP_CFG
|
|
83
|
+
python scripts/fvdyp_cli.py bundle validate
|
|
84
|
+
python scripts/fvdyp_cli.py bundle catalog
|
|
85
|
+
python scripts/fvdyp_cli.py bundle extract --output-dir data/normalized
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`validate` checks required files, control targets, and key lookups. `catalog`
|
|
89
|
+
prints the family and row counts. `extract` writes normalized CSV files plus a
|
|
90
|
+
provenance manifest; the CSV files stay under ignored `data/normalized/`.
|
|
91
|
+
|
|
92
|
+
## Repository map
|
|
93
|
+
|
|
94
|
+
| Path | Purpose |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| `AGENTS.md` | Operating contract for coding agents |
|
|
97
|
+
| `HANDOFF.md` | First-read handoff note for a new session agent |
|
|
98
|
+
| `ROADMAP.md` | Plan of record and issue tracker map |
|
|
99
|
+
| `CHANGE_LOG.md` | Dated narrative of changes |
|
|
100
|
+
| `notes/plan/` | Detailed program, data, architecture, and validation plans |
|
|
101
|
+
| `notes/reference/` | VDYP inventory and coefficient-map facts |
|
|
102
|
+
| `notes/adr/` | Accepted design decisions |
|
|
103
|
+
| `docs/` | Sphinx RTFM surface |
|
|
104
|
+
| `scripts/check_workflow.py` | Workflow validator |
|
|
105
|
+
| `tests/` | pytest suite, including planning-surface checks |
|
|
106
|
+
|
|
107
|
+
## License and citation
|
|
108
|
+
|
|
109
|
+
Code is MIT licensed. Coefficient files and model outputs retain their original
|
|
110
|
+
BC government attribution. Cite the source model and file, not only this package.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Build and tool configuration for fresh-vdyp. The installable modules live in
|
|
2
|
+
# scripts/ and the public facade lives in scripts/fresh_vdyp/. Each gate
|
|
3
|
+
# configured here is also a check in the green dashboard (scripts/dashboard.py).
|
|
4
|
+
|
|
5
|
+
[build-system]
|
|
6
|
+
requires = ["setuptools>=69", "wheel"]
|
|
7
|
+
build-backend = "setuptools.build_meta"
|
|
8
|
+
|
|
9
|
+
[project]
|
|
10
|
+
name = "fresh-vdyp"
|
|
11
|
+
version = "0.1.0"
|
|
12
|
+
description = "A public interoperability layer for the BC VDYP7 coefficient bundle, console runner, and yield curves."
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
requires-python = ">=3.11"
|
|
15
|
+
license = "MIT"
|
|
16
|
+
authors = [{ name = "UBC FRESH Lab" }]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"fastapi>=0.110",
|
|
19
|
+
"httpx>=0.27",
|
|
20
|
+
"pydantic>=2",
|
|
21
|
+
"typer>=0.12",
|
|
22
|
+
"uvicorn>=0.29",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
fvdyp = "fvdyp_cli:main"
|
|
27
|
+
fresh-vdyp = "fvdyp_cli:main"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools]
|
|
30
|
+
package-dir = { "" = "scripts" }
|
|
31
|
+
packages = ["fresh_vdyp"]
|
|
32
|
+
py-modules = [
|
|
33
|
+
"assistant_tools",
|
|
34
|
+
"bundle_service",
|
|
35
|
+
"check_workflow",
|
|
36
|
+
"coe_parser",
|
|
37
|
+
"config_parser",
|
|
38
|
+
"curve_api",
|
|
39
|
+
"dashboard",
|
|
40
|
+
"equations",
|
|
41
|
+
"fvdyp_api",
|
|
42
|
+
"fvdyp_cli",
|
|
43
|
+
"inventory_vdyp",
|
|
44
|
+
"key_inference",
|
|
45
|
+
"normalize",
|
|
46
|
+
"provenance",
|
|
47
|
+
"run_console",
|
|
48
|
+
"vdyp_control",
|
|
49
|
+
"vdyp_map",
|
|
50
|
+
"vdyp_runtime",
|
|
51
|
+
"yield_parser",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
line-length = 100
|
|
56
|
+
target-version = "py311"
|
|
57
|
+
src = ["scripts", "tests"]
|
|
58
|
+
extend-exclude = ["tmp", "_build"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
select = ["E", "W", "F", "I", "B", "UP", "D", "ANN", "N", "PT", "SIM", "RUF", "C4"]
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint.pydocstyle]
|
|
64
|
+
convention = "numpy"
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint.per-file-ignores]
|
|
67
|
+
"tests/**" = ["D"]
|
|
68
|
+
"templates/project/tests/**" = ["D"]
|
|
69
|
+
"docs/conf.py" = ["D", "ANN", "N"]
|
|
70
|
+
|
|
71
|
+
[tool.mypy]
|
|
72
|
+
python_version = "3.11"
|
|
73
|
+
strict = true
|
|
74
|
+
warn_unreachable = true
|
|
75
|
+
mypy_path = "scripts"
|
|
76
|
+
files = ["scripts", "tests", "docs/conf.py"]
|
|
77
|
+
|
|
78
|
+
[[tool.mypy.overrides]]
|
|
79
|
+
module = ["numpydoc", "numpydoc.*"]
|
|
80
|
+
ignore_missing_imports = true
|
|
81
|
+
|
|
82
|
+
[tool.pytest.ini_options]
|
|
83
|
+
testpaths = ["tests"]
|
|
84
|
+
addopts = "--cov=scripts --cov-branch --cov-report=term-missing --cov-fail-under=95"
|
|
85
|
+
|
|
86
|
+
[tool.coverage.run]
|
|
87
|
+
branch = true
|
|
88
|
+
source = ["scripts"]
|
|
89
|
+
|
|
90
|
+
[tool.coverage.report]
|
|
91
|
+
show_missing = true
|
|
92
|
+
exclude_also = ['if __name__ == "__main__":']
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Bounded assistant tools over the catalog, curves, provenance, and matrix.
|
|
3
|
+
|
|
4
|
+
Every handler is deterministic and returns an :class:`AssistantAnswer` with at
|
|
5
|
+
least one source. The validator rejects an answer without a source, so an
|
|
6
|
+
assistant cannot present a coefficient, equation, or curve without attribution.
|
|
7
|
+
No network or model provider is used.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Final
|
|
16
|
+
|
|
17
|
+
from bundle_service import catalog_payload
|
|
18
|
+
from curve_api import CurveQuery, CurveService
|
|
19
|
+
from yield_parser import DEFAULT_ALIASES, DEFAULT_SCHEMA
|
|
20
|
+
|
|
21
|
+
DEFAULT_MATRIX: Final[Path] = Path("notes/reference/vdyp-equation-matrix.json")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AssistantAnswerError(ValueError):
|
|
25
|
+
"""Raised when an answer has no usable source."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class ToolSpec:
|
|
30
|
+
"""The schema of one assistant tool.
|
|
31
|
+
|
|
32
|
+
Attributes
|
|
33
|
+
----------
|
|
34
|
+
name : str
|
|
35
|
+
Tool name.
|
|
36
|
+
description : str
|
|
37
|
+
What the tool answers.
|
|
38
|
+
parameters : dict of str to str
|
|
39
|
+
Parameter names and their descriptions.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
name: str
|
|
43
|
+
description: str
|
|
44
|
+
parameters: dict[str, str] = field(default_factory=dict)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class AssistantAnswer:
|
|
49
|
+
"""One source-backed assistant answer.
|
|
50
|
+
|
|
51
|
+
Attributes
|
|
52
|
+
----------
|
|
53
|
+
text : str
|
|
54
|
+
Short answer text.
|
|
55
|
+
sources : tuple of str
|
|
56
|
+
Source labels. Must be non-empty.
|
|
57
|
+
data : dict of str to object
|
|
58
|
+
Structured result for a caller.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
text: str
|
|
62
|
+
sources: tuple[str, ...]
|
|
63
|
+
data: dict[str, object] = field(default_factory=dict)
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> dict[str, object]:
|
|
66
|
+
"""Render the answer as a JSON-ready mapping.
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
dict of str to object
|
|
71
|
+
Text, sources, and data.
|
|
72
|
+
"""
|
|
73
|
+
return {"text": self.text, "sources": list(self.sources), "data": self.data}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
TOOL_SPECS: Final[tuple[ToolSpec, ...]] = (
|
|
77
|
+
ToolSpec(
|
|
78
|
+
"catalog_search",
|
|
79
|
+
"Search the coefficient catalog by family, equation family, or status.",
|
|
80
|
+
{"query": "case-insensitive text"},
|
|
81
|
+
),
|
|
82
|
+
ToolSpec(
|
|
83
|
+
"curve_query",
|
|
84
|
+
"Query the typed curve service with optional species/year/age filters.",
|
|
85
|
+
{
|
|
86
|
+
"species": "species code",
|
|
87
|
+
"year_start": "int",
|
|
88
|
+
"year_end": "int",
|
|
89
|
+
"age_start": "int",
|
|
90
|
+
"age_end": "int",
|
|
91
|
+
},
|
|
92
|
+
),
|
|
93
|
+
ToolSpec(
|
|
94
|
+
"provenance_lookup",
|
|
95
|
+
"Return the source and runtime provenance of the loaded curve table.",
|
|
96
|
+
{},
|
|
97
|
+
),
|
|
98
|
+
ToolSpec(
|
|
99
|
+
"validation_status",
|
|
100
|
+
"Return the validation status of one equation family.",
|
|
101
|
+
{"family": "family name"},
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def validate_answer(answer: AssistantAnswer) -> None:
|
|
107
|
+
"""Validate that an answer names at least one source.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
answer : AssistantAnswer
|
|
112
|
+
Answer to validate.
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
None
|
|
117
|
+
The answer is source-backed.
|
|
118
|
+
|
|
119
|
+
Raises
|
|
120
|
+
------
|
|
121
|
+
AssistantAnswerError
|
|
122
|
+
When the source tuple is empty or contains a blank label.
|
|
123
|
+
"""
|
|
124
|
+
if not answer.sources:
|
|
125
|
+
raise AssistantAnswerError("answer has no source")
|
|
126
|
+
if any(not source.strip() for source in answer.sources):
|
|
127
|
+
raise AssistantAnswerError("answer has a blank source")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def catalog_search(config_dir: Path, query: str) -> AssistantAnswer:
|
|
131
|
+
"""Search the catalog and return matching families with their source files.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
config_dir : Path
|
|
136
|
+
VDYP_CFG directory.
|
|
137
|
+
query : str
|
|
138
|
+
Case-insensitive family, equation-family, or status text.
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
AssistantAnswer
|
|
143
|
+
Matching families and source labels.
|
|
144
|
+
"""
|
|
145
|
+
payload = catalog_payload(config_dir)
|
|
146
|
+
rows = payload.get("families")
|
|
147
|
+
families = rows if isinstance(rows, list) else []
|
|
148
|
+
needle = query.strip().lower()
|
|
149
|
+
matches = [
|
|
150
|
+
row
|
|
151
|
+
for row in families
|
|
152
|
+
if isinstance(row, dict)
|
|
153
|
+
and any(
|
|
154
|
+
needle in str(row.get(key, "")).lower()
|
|
155
|
+
for key in ("family", "equation_family", "status")
|
|
156
|
+
)
|
|
157
|
+
]
|
|
158
|
+
sources = tuple(
|
|
159
|
+
str(row["source_file"])
|
|
160
|
+
for row in matches
|
|
161
|
+
if isinstance(row, dict) and row.get("source_file")
|
|
162
|
+
) or ("VDYP_CFG",)
|
|
163
|
+
answer = AssistantAnswer(
|
|
164
|
+
f"{len(matches)} families match {query!r}.",
|
|
165
|
+
sources,
|
|
166
|
+
{"families": matches},
|
|
167
|
+
)
|
|
168
|
+
validate_answer(answer)
|
|
169
|
+
return answer
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def curve_query(service: CurveService, query: CurveQuery) -> AssistantAnswer:
|
|
173
|
+
"""Run a curve query and return a source-backed summary.
|
|
174
|
+
|
|
175
|
+
Parameters
|
|
176
|
+
----------
|
|
177
|
+
service : CurveService
|
|
178
|
+
Loaded curve service.
|
|
179
|
+
query : CurveQuery
|
|
180
|
+
Query inputs.
|
|
181
|
+
|
|
182
|
+
Returns
|
|
183
|
+
-------
|
|
184
|
+
AssistantAnswer
|
|
185
|
+
Status, point count, and provenance.
|
|
186
|
+
"""
|
|
187
|
+
result = service.query(query)
|
|
188
|
+
answer = AssistantAnswer(
|
|
189
|
+
f"{result.status}: {len(result.points)} points. {result.reason}".strip(),
|
|
190
|
+
(result.provenance.source_file, result.provenance.runtime),
|
|
191
|
+
result.to_dict(),
|
|
192
|
+
)
|
|
193
|
+
validate_answer(answer)
|
|
194
|
+
return answer
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def provenance_lookup(service: CurveService) -> AssistantAnswer:
|
|
198
|
+
"""Return the provenance of the loaded curve table.
|
|
199
|
+
|
|
200
|
+
Parameters
|
|
201
|
+
----------
|
|
202
|
+
service : CurveService
|
|
203
|
+
Loaded curve service.
|
|
204
|
+
|
|
205
|
+
Returns
|
|
206
|
+
-------
|
|
207
|
+
AssistantAnswer
|
|
208
|
+
Source file, runtime, console version, and schema labels.
|
|
209
|
+
"""
|
|
210
|
+
answer = AssistantAnswer(
|
|
211
|
+
f"Source {service.table.source_file}; runtime {service.runtime}.",
|
|
212
|
+
(service.table.source_file, service.runtime),
|
|
213
|
+
{
|
|
214
|
+
"source_file": service.table.source_file,
|
|
215
|
+
"runtime": service.runtime,
|
|
216
|
+
"console_version": service.console_version,
|
|
217
|
+
"control_sequence": service.control_sequence,
|
|
218
|
+
"schema": str(DEFAULT_SCHEMA),
|
|
219
|
+
"aliases": str(DEFAULT_ALIASES),
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
validate_answer(answer)
|
|
223
|
+
return answer
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def validation_status(matrix: Path, family: str) -> AssistantAnswer:
|
|
227
|
+
"""Return the validation status of one family from the matrix.
|
|
228
|
+
|
|
229
|
+
Parameters
|
|
230
|
+
----------
|
|
231
|
+
matrix : Path
|
|
232
|
+
Equation matrix JSON.
|
|
233
|
+
family : str
|
|
234
|
+
Family name.
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
AssistantAnswer
|
|
239
|
+
Status and evidence, or ``not_implemented`` when the family is absent.
|
|
240
|
+
"""
|
|
241
|
+
payload = json.loads(matrix.read_text(encoding="utf-8"))
|
|
242
|
+
rows = payload.get("families") if isinstance(payload, dict) else None
|
|
243
|
+
if not isinstance(rows, list):
|
|
244
|
+
raise AssistantAnswerError("matrix has no families")
|
|
245
|
+
match = next(
|
|
246
|
+
(row for row in rows if isinstance(row, dict) and row.get("family") == family),
|
|
247
|
+
None,
|
|
248
|
+
)
|
|
249
|
+
if match is None:
|
|
250
|
+
answer = AssistantAnswer(
|
|
251
|
+
f"{family}: not_implemented (not in the matrix).",
|
|
252
|
+
(str(matrix),),
|
|
253
|
+
{"family": family, "status": "not_implemented"},
|
|
254
|
+
)
|
|
255
|
+
else:
|
|
256
|
+
answer = AssistantAnswer(
|
|
257
|
+
f"{family}: {match.get('status')}.",
|
|
258
|
+
(str(match.get("evidence", matrix)),),
|
|
259
|
+
match,
|
|
260
|
+
)
|
|
261
|
+
validate_answer(answer)
|
|
262
|
+
return answer
|