Skip to content

Commit 046beec

Browse files
mihiarcclaude
andcommitted
Implement native FVS validation system with ctypes bindings
Add pyfvs.native subpackage for validating Python growth models against the official USDA FVS Fortran shared library. Species maps cover all 10 variants (SN/LS/PN/WC/NE/CS/OP/CA/OC/WS). Library loading is lazy so imports never fail when the Fortran library is absent. - native/species_map.py: 10 variant species code-to-index maps from blkdat.f - native/library_loader.py: Platform-aware .dylib/.so/.dll discovery - native/fvs_bindings.py: ctypes wrappers for FVS API (apisubs.f) - native/native_stand.py: NativeStand class mirroring Stand API - native/BUILD.md: Instructions for building FVS shared libraries - tests/test_native.py: 40 pass, 7 skip gracefully when library absent - validation/native/compare_native.py: Side-by-side yield table comparison - examples/native_fvs_comparison.py: Consolidated demo script Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0781f4d commit 046beec

File tree

13 files changed

+3003
-1
lines changed

13 files changed

+3003
-1
lines changed

examples/native_fvs_comparison.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
"""
2+
Native FVS Comparison Example
3+
4+
Demonstrates running growth simulations through both pyfvs (Python)
5+
and the native FVS Fortran library, then comparing results.
6+
7+
Prerequisites:
8+
- Build and install the FVS shared library (see native/BUILD.md)
9+
- Set FVS_LIB_PATH if not installed to a standard location
10+
11+
Usage:
12+
uv run python examples/native_fvs_comparison.py
13+
"""
14+
15+
from rich.console import Console
16+
from rich.table import Table
17+
from rich.panel import Panel
18+
19+
from pyfvs import Stand
20+
from pyfvs.native import fvs_library_available, get_library_info
21+
22+
console = Console()
23+
24+
25+
def check_library_availability():
26+
"""Check which FVS variant libraries are available."""
27+
console.print(Panel("[bold]FVS Library Availability[/bold]"))
28+
29+
variants = ["SN", "LS", "PN", "WC", "NE", "CS", "OP", "CA", "OC", "WS"]
30+
31+
table = Table()
32+
table.add_column("Variant", style="bold")
33+
table.add_column("Available")
34+
table.add_column("Path")
35+
36+
any_available = False
37+
for variant in variants:
38+
info = get_library_info(variant)
39+
available = info["available"]
40+
path = info["path"] or "-"
41+
status = "[green]Yes[/green]" if available else "[red]No[/red]"
42+
table.add_row(variant, status, path)
43+
if available:
44+
any_available = True
45+
46+
console.print(table)
47+
console.print()
48+
49+
if not any_available:
50+
console.print(
51+
"[yellow]No FVS shared libraries found.[/yellow]\n"
52+
"To build and install, see: src/pyfvs/native/BUILD.md\n"
53+
"Or set: export FVS_LIB_PATH=/path/to/libs\n"
54+
)
55+
56+
return any_available
57+
58+
59+
def run_pyfvs_only_demo():
60+
"""Run a pyfvs-only simulation to demonstrate the API."""
61+
console.print(Panel("[bold]PyFVS Simulation (Python-only)[/bold]"))
62+
63+
stand = Stand.initialize_planted(
64+
trees_per_acre=500,
65+
site_index=70,
66+
species="LP",
67+
variant="SN",
68+
ecounit="M231",
69+
)
70+
71+
table = Table(title="Loblolly Pine Yield Table (SN variant, SI=70, M231)")
72+
table.add_column("Year", justify="center")
73+
table.add_column("TPA", justify="right")
74+
table.add_column("BA", justify="right")
75+
table.add_column("QMD", justify="right")
76+
table.add_column("Volume", justify="right")
77+
78+
for year in range(5, 55, 5):
79+
stand.grow(years=5)
80+
m = stand.get_metrics()
81+
table.add_row(
82+
str(year),
83+
f"{m['tpa']:.0f}",
84+
f"{m['basal_area']:.1f}",
85+
f"{m['qmd']:.2f}",
86+
f"{m['volume']:.0f}",
87+
)
88+
89+
console.print(table)
90+
console.print()
91+
92+
93+
def run_native_comparison():
94+
"""Run side-by-side comparison if a native library is available."""
95+
# Find the first available variant
96+
for variant, species, si, tpa in [
97+
("SN", "LP", 70, 500),
98+
("PN", "DF", 120, 400),
99+
("LS", "RN", 65, 500),
100+
]:
101+
if fvs_library_available(variant):
102+
console.print(
103+
Panel(f"[bold]PyFVS vs Native FVS: {variant} variant[/bold]")
104+
)
105+
106+
from validation.native.compare_native import (
107+
compare_stand_growth,
108+
generate_validation_report,
109+
)
110+
111+
results = compare_stand_growth(
112+
trees_per_acre=tpa,
113+
site_index=si,
114+
species=species,
115+
variant=variant,
116+
years=50,
117+
)
118+
generate_validation_report(results)
119+
return True
120+
121+
return False
122+
123+
124+
def main():
125+
console.print()
126+
console.rule("[bold blue]PyFVS Native FVS Comparison[/bold blue]")
127+
console.print()
128+
129+
# Step 1: Check library availability
130+
libraries_found = check_library_availability()
131+
132+
# Step 2: Always show pyfvs-only demo
133+
run_pyfvs_only_demo()
134+
135+
# Step 3: Run comparison if libraries available
136+
if libraries_found:
137+
ran_comparison = run_native_comparison()
138+
if ran_comparison:
139+
console.print(
140+
"[green]Comparison complete. "
141+
"See validation/results/native/ for detailed results.[/green]"
142+
)
143+
else:
144+
console.print(
145+
"[dim]Install FVS shared libraries to enable native comparison.[/dim]"
146+
)
147+
148+
149+
if __name__ == "__main__":
150+
main()

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ markers = [
195195
"integration: marks tests as integration tests",
196196
"unit: marks tests as unit tests",
197197
"benchmark: marks tests as performance benchmarks",
198+
"native: marks tests that require the native FVS shared library (skip with '-m \"not native\"')",
198199
]
199200

200201
# Coverage configuration

src/pyfvs/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@
173173
# =============================================================================
174174
from .exceptions import (
175175
FVSError,
176+
FVSNativeError,
176177
ConfigurationError,
177178
SpeciesNotFoundError,
178179
ParameterError,
@@ -185,6 +186,11 @@
185186
InvalidDataError,
186187
)
187188

189+
# =============================================================================
190+
# Native FVS Bindings (optional - requires FVS shared library)
191+
# =============================================================================
192+
from . import native
193+
188194
# =============================================================================
189195
# Base Classes (for extension)
190196
# =============================================================================
@@ -353,6 +359,7 @@
353359
"normalize_ecounit",
354360
# Exceptions
355361
"FVSError",
362+
"FVSNativeError",
356363
"ConfigurationError",
357364
"SpeciesNotFoundError",
358365
"ParameterError",
@@ -363,6 +370,8 @@
363370
"EmptyStandError",
364371
"DataError",
365372
"InvalidDataError",
373+
# Native FVS Bindings
374+
"native",
366375
# Base Classes
367376
"ParameterizedModel",
368377
# Variant-Specific Growth Models

src/pyfvs/exceptions.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,13 @@ class InvalidDataError(DataError):
9090
def __init__(self, data_description: str, reason: str):
9191
self.data_description = data_description
9292
self.reason = reason
93-
super().__init__(f"Invalid {data_description}: {reason}")
93+
super().__init__(f"Invalid {data_description}: {reason}")
94+
95+
96+
class FVSNativeError(FVSError):
97+
"""Raised when native FVS library operations fail.
98+
99+
This includes library loading failures, Fortran API errors,
100+
and symbol resolution issues.
101+
"""
102+
pass

0 commit comments

Comments
 (0)