fresh_fvsbc

The package. It re-exports the names the reference pages name, so a caller can write fresh_fvsbc.check_run or fresh_fvsbc.outputs.check_run and reach the same function. The five modules are the units the contracts are written against, and each is listed below in the order the pipeline uses them: build a tree list, build a keyword set, run, read the outputs, and record the provenance.

A Python wrapper for the BC variant of the Forest Vegetation Simulator.

This package is the public surface that fresh-fvsbc offers a caller. Its contracts are the reference pages under docs/reference/: the tree-list input, the keyword set, the outputs, the provenance record, and the nemora ingestion.

The names re-exported here are the ones those pages name. Each module remains importable on its own, so fresh_fvsbc.treelist.write_tree_record and fresh_fvsbc.write_tree_record are the same function. The package is under construction, one bounded move at a time: every module lands with its tests in the same change set, and the gates configured in pyproject.toml cover it.

The package’s re-exports are documented once, in the module that defines them, and the modules below are listed in the order the pipeline uses them.

The FVS-BC tree-list writer.

docs/reference/tree-list.rst is the contract. BC’s record format is TREFMT, read from canada/bc/blkdat.f, and the field names are the corresponding PyFVS template names. This module parses that format string rather than restating it, so the layout cannot drift from the contract.

T1, and why the layout is not the declared one

BC’s format opens I4,T1,I7. T1 is FORTRAN’s absolute column directive, so it targets column 1, and tree_num is consequently read from columns 1 to 7 — its first four columns are shared with plot_id. Every later field begins four columns earlier than the declared widths suggest, and the line occupies 69 columns rather than the 73 that the widths sum to.

Two pieces of evidence, both reproducible. gfortran reads (I4,T1,I7) from the character string ' 123  456  789  012 345' as 123 and 1234, where (I4,1X,I7) reads 4567; FVS’s own reader is an internal read of a whole line (base/intree.f), so T1 positions within it. And FVSso/sot03.tre, the one tree list in the official corpus that relies on the default format instead of declaring one, decodes into species WF, diam 21.4, and live_ht 105 under this reading, and into the non-species text 10WF under the other.

A consequence to state plainly: because plot_id and tree_num share columns 1 to 4, the number FVS reads as the tree number is the digits of the plot id followed by the digits of the tree number. It stays unique within a plot, which is what FVS requires, but it is a composite. Only a run confirms this against BC’s own reader, which is P2.2’s run.

fresh_fvsbc.treelist.BC_TREFMT: str = '(I4,T1,I7,F6.0,I1,A3,F5.1,F4.1,2F5.1,F5.1,I1,3(I2,I2),2I1,I2,2I3,2I1,F3.0)'

BC’s tree-record format, from canada/bc/blkdat.f, in BLOCK DATA BLKDAT.

fresh_fvsbc.treelist.DONOR_SPECIES: dict[str, str] = {'BA': 'BG', 'DR': 'AT', 'HM': 'HW', 'MB': 'AC', 'SB': 'SE', 'SS': 'SE', 'YC': 'CW'}

The seven high-priority BC species the 15-code variant does not carry, and the donor each one uses until the true species tables land. The output carries the donor code, not the input code.

fresh_fvsbc.treelist.FIA_CODES: tuple[str, ...] = ('119', '073', '202', '017', '263', '242', '108', '093', '019', '122', '375', '746', '747', '202', '376')

The parallel FIA numeric codes, FIAJSP. 202 appears twice, for FD and for OC.

fresh_fvsbc.treelist.FIELD_DESCRIPTORS: tuple[tuple[str | None, str], ...] = (('plot_id', 'I4'), (None, 'T1'), ('tree_num', 'I7'), ('tree_count', 'F6.0'), ('tree_history', 'I1'), ('species', 'A3'), ('diam', 'F5.1'), ('dg_incr', 'F4.1'), ('live_ht', 'F5.1'), ('trunc_ht', 'F5.1'), ('htg_incr', 'F5.1'), ('crown_ratio', 'I1'), ('damage1', 'I2'), ('severity1', 'I2'), ('damage2', 'I2'), ('severity2', 'I2'), ('damage3', 'I2'), ('severity3', 'I2'), ('tree_val', 'I1'), ('tree_rx', 'I1'), ('plot_slope', 'I2'), ('plot_aspect', 'I3'), ('plot_habitat', 'I3'), ('plot_topo_pos', 'I1'), ('plot_site_prep', 'I1'), ('tree_age', 'F3.0'))

(field, edit) pairs in order. A field of None is a positioning directive.

fresh_fvsbc.treelist.FIELD_NAMES: tuple[str, ...] = ('plot_id', 'tree_num', 'tree_count', 'tree_history', 'species', 'diam', 'dg_incr', 'live_ht', 'trunc_ht', 'htg_incr', 'crown_ratio', 'damage1', 'severity1', 'damage2', 'severity2', 'damage3', 'severity3', 'tree_val', 'tree_rx', 'plot_slope', 'plot_aspect', 'plot_habitat', 'plot_topo_pos', 'plot_site_prep', 'tree_age')

The fields, in the order the format declares them. The names come from the corresponding PyFVS template, and the contract page carries the same list beside the edit descriptors.

fresh_fvsbc.treelist.JSP_CODES: tuple[str, ...] = ('PW', 'LW', 'FD', 'BG', 'HW', 'CW', 'PL', 'SE', 'BL', 'PY', 'EP', 'AT', 'AC', 'OC', 'OH')

BC’s species list, JSP in canada/bc/blkdat.f, in order.

fresh_fvsbc.treelist.PLANTS_SYMBOLS: tuple[str, ...] = ('PIMO3', 'LAOC', 'PSME', 'ABGR', 'TSHE', 'THPL', 'PICO', 'PIEN', 'ABLA', 'PIPO', 'BEPA', 'POTR5', 'POBAT', 'SOFT', 'HARD')

The parallel PLANTS symbols, PLNJSP. SOFT and HARD are BC aggregates, not symbols.

fresh_fvsbc.treelist.READ_COLUMNS: dict[str, tuple[int, int]] = {'crown_ratio': (42, 42), 'damage1': (43, 44), 'damage2': (47, 48), 'damage3': (51, 52), 'dg_incr': (23, 26), 'diam': (18, 22), 'htg_incr': (37, 41), 'live_ht': (27, 31), 'plot_aspect': (59, 61), 'plot_habitat': (62, 64), 'plot_id': (1, 4), 'plot_site_prep': (66, 66), 'plot_slope': (57, 58), 'plot_topo_pos': (65, 65), 'severity1': (45, 46), 'severity2': (49, 50), 'severity3': (53, 54), 'species': (15, 17), 'tree_age': (67, 69), 'tree_count': (8, 13), 'tree_history': (14, 14), 'tree_num': (1, 7), 'tree_rx': (56, 56), 'tree_val': (55, 55), 'trunc_ht': (32, 36)}

The columns FVS reads each field from. tree_num overlaps plot_id.

fresh_fvsbc.treelist.RECORD_WIDTH: int = 69

The width of a record, in columns. It is 69, not the 73 that the declared widths sum to, because plot_id and tree_num share their first four columns.

fresh_fvsbc.treelist.TREE_LIST_END: str = '-999\n'

The line that ends a tree-data file. intree.f reads until a line contains -999.

fresh_fvsbc.treelist.WRITE_COLUMNS: dict[str, tuple[int, int]] = {'crown_ratio': (42, 42), 'damage1': (43, 44), 'damage2': (47, 48), 'damage3': (51, 52), 'dg_incr': (23, 26), 'diam': (18, 22), 'htg_incr': (37, 41), 'live_ht': (27, 31), 'plot_aspect': (59, 61), 'plot_habitat': (62, 64), 'plot_id': (1, 4), 'plot_site_prep': (66, 66), 'plot_slope': (57, 58), 'plot_topo_pos': (65, 65), 'severity1': (45, 46), 'severity2': (49, 50), 'severity3': (53, 54), 'species': (15, 17), 'tree_age': (67, 69), 'tree_count': (8, 13), 'tree_history': (14, 14), 'tree_num': (5, 7), 'tree_rx': (56, 56), 'tree_val': (55, 55), 'trunc_ht': (32, 36)}

The columns this writer places each field in. tree_num is three columns wide here.

exception fresh_fvsbc.treelist.AmbiguousSpeciesError[source]

Bases: SpeciesError

A species code that appears at more than one species position.

exception fresh_fvsbc.treelist.FieldWidthError[source]

Bases: ValueError

A value that does not fit the width its field has on the record.

exception fresh_fvsbc.treelist.SpeciesError[source]

Bases: ValueError

A species code that this writer cannot resolve to one BC species.

class fresh_fvsbc.treelist.TreeListFields[source]

Bases: TypedDict

The tree-list fields of one record, as a caller supplies them.

The values are the FVS side of the record. species is a code from any of BC’s three lists, and the writer resolves it. crown_ratio is the single digit the field holds; which scale it is in is nemora-ingest.rst’s open question rather than this writer’s.

exception fresh_fvsbc.treelist.UnknownSpeciesError[source]

Bases: SpeciesError

A species code that appears in none of BC’s three lists.

fresh_fvsbc.treelist.resolve_species(code: str) → str[source]

Resolve a species code from any of BC’s three lists to a JSP code.

Parameters:

code (str) – A JSP code such as PW, an FIA numeric code such as 119, or a PLANTS symbol such as PIMO3. Case and surrounding whitespace are ignored.

Returns:

str – The JSP code for the species.

Raises:
  • UnknownSpeciesError – If the code appears in none of the three lists and is not a donor alias. The donor aliases are DONOR_SPECIES; the caller receives the donor JSP code, not the input code.

  • AmbiguousSpeciesError – If the code appears at more than one position, which 202 does because BC gives it to both FD and OC. An ambiguous code is not resolved to a default.

fresh_fvsbc.treelist.write_tree_file(records: Sequence[TreeListFields]) → str[source]

Write a tree list as the file FVS reads, terminator and all.

FVS does not take a filename for tree data: metric/base/intree.f opens the file named after the keyword file and reads records until a line contains -999. A list without that line runs to the end of the file and is reported as a read error, so the terminator belongs with the writer rather than at a call site.

Parameters:

records (Sequence[TreeListFields]) – The trees, in the order to write them.

Returns:

str – The records, each on its own line, followed by the terminating line.

fresh_fvsbc.treelist.write_tree_list(records: Sequence[TreeListFields]) → str[source]

Write a tree list in BC’s format, one record per line.

Parameters:

records (Sequence[TreeListFields]) – The trees, in the order to write them.

Returns:

str – The records, each on its own line and each ending with a newline.

fresh_fvsbc.treelist.write_tree_record(fields: TreeListFields) → str[source]

Write one tree record in BC’s format.

Parameters:

fields (TreeListFields) – The tree-list fields of one tree.

Returns:

str – One fixed-width record, RECORD_WIDTH columns wide.

Raises:
  • FieldWidthError – If any value does not fit its field.

  • SpeciesError – If species is not one of BC’s codes, or resolves to more than one.

The BC Biogeoclimatic Ecosystem Classification (BEC) code, validated.

canada/bc/initre.f reads a BC stand’s BEC from the first two fields of ``STDINFO``, concatenates them, and passes the result to canada/bc/habtyp.f. HABTYP looks for a region token, a zone token, a two-letter subzone token, an optional subzone variant, a slash, and a site-series code. If any part is missing it calls its error path and leaves the BEC at the documented default, ICHmw2/01.

That default is the hazard this module closes. A caller who writes STDINFO=712,CHS131,... is not running an IDF or SBS stand; FVS is running ICHmw2/01 with a maximum basal area of 89 m2/ha. Nothing in the run says so. This module parses the two STDINFO fields before a run starts, refuses a code that would not resolve, and names the accepted grammar and the supported zones.

The source is the authority

The accepted regions are CAR, KAM, and NEL; the zones are ESSF, ICH, IDF, MS, PP, SBS, SBPS, CWH, CDF, BWBS, CMA, and MH; and the subzones are the two-letter list in HABTYP. The zone list is also the list sitset.f assigns default maximum basal area for. The five prototype zones are carried by the W2.5 extension; a build without that extension echoes a different BEC and the run refuses it.

The grammar is the working form the FVS source accepts, made explicit:

REGION ZONE SUBZONE[VARIANT]/SERIES

IDFdk3/01 is a zone IDF, subzone dk, variant 3, series 01. IDFxm/01 has no variant. The series is mandatory: CAR IDFdk3 alone does not resolve, because HABTYP requires the slash.

fresh_fvsbc.bec.REGIONS: tuple[str, ...] = ('CAR', 'KAM', 'NEL')

The region tokens HABTYP accepts, in its order.

fresh_fvsbc.bec.SUBZONES: tuple[str, ...] = ('DC', 'DK', 'DM', 'DW', 'MC', 'MH', 'MK', 'MM', 'MW', 'VK', 'WC', 'WK', 'WM', 'WW', 'XC', 'XH', 'XK', 'XM', 'DH', 'DV', 'MV', 'VC', 'WH', 'WV', 'XV', 'XW', 'XX', 'UN', 'DS', 'MS', 'VH', 'VM', 'WS')

The two-letter subzone tokens HABTYP accepts, without duplicates. The source lists WM twice; a set does not need the duplicate.

fresh_fvsbc.bec.SUPPORTED_ZONES: tuple[str, ...] = ('ESSF', 'ICH', 'IDF', 'MS', 'PP', 'SBS', 'SBPS', 'CWH', 'CDF', 'BWBS', 'CMA', 'MH')

The BEC zones the BC variant resolves, in HABTYP’s order.

fresh_fvsbc.bec.UNSUPPORTED_ZONES: tuple[str, ...] = ()

Retained for callers that imported the old refusal list. Empty after the W2.5 extension added the five prototype zones.

class fresh_fvsbc.bec.BecCode(region: str, zone: str, subzone: str, series: str)[source]

Bases: object

A validated BC BEC code.

Parameters:
  • region (str) – The region token: CAR, KAM, or NEL.

  • zone (str) – The BEC zone: ESSF, ICH, IDF, MS, PP, SBS, or SBPS.

  • subzone (str) – The lowercase subzone and optional variant, such as dk3 or xm.

  • series (str) – The lowercase site-series code, such as 01 or 01-yc.

Raises:

BecCodeError – If a field is outside the source’s lists. parse() is the normal way to build one, because it reads a caller’s code and reports what is wrong.

property pretty: str

Return the zone, subzone, slash, and series, such as IDFdk3/01.

Returns:

str – The second STDINFO field’s canonical form.

property fields: tuple[str, str]

Return the two STDINFO fields that carry this BEC.

Returns:

tuple of str – The region and the zone+subzone/series string, in that order.

classmethod from_fields(first: str, second: str) → BecCode[source]

Parse the first two STDINFO fields.

HABTYP concatenates the fields before it parses them, so this method does the same. The working form puts the region in the first field and the rest in the second: STDINFO CAR IDFdk3/01.

Parameters:
  • first (str) – The first STDINFO field, normally the region.

  • second (str) – The second STDINFO field, normally ZONE+SUBZONE/SERIES.

Returns:

BecCode – The validated code.

Raises:

BecCodeError – If the concatenated fields do not parse, or if the zone is one the variant does not represent.

classmethod parse(text: str) → BecCode[source]

Parse a BEC code and report why it would not resolve.

The parser mirrors HABTYP’s accepted grammar, but it is anchored: the region, zone, subzone, slash, and series must be the whole code. Whitespace is removed first, so CAR IDFdk3/01 and CARIDFdk3/01 are the same code.

Parameters:

text (str) – A code such as CAR IDFdk3/01, CARIDFdk3/01, or the legacy 712CHS131 that the package refuses.

Returns:

BecCode – The validated code, with the region and zone upper-case and the subzone and series lower-case.

Raises:

BecCodeError – If the code is malformed, if its zone is not represented by the BC variant, or if its series is missing.

exception fresh_fvsbc.bec.BecCodeError[source]

Bases: ValueError

A BEC code that FVS-BC would not resolve, or a set that does not carry one.

Build the provincial calibration dataset from the FAIB PSP archive.

The dataset is the input to the calibration workstream, not a calibration. It turns the public FAIB permanent-sample-plot files into:

  • one row per plot visit, with live basal area, trees per hectare, volume, and quadratic mean diameter;

  • one row per consecutive visit pair, with the interval’s dead basal area and trees per hectare;

  • one row per live tree at the start of an interval, with its end status (live, dead, or missing);

  • province-wide BEC coverage and basal-area envelope tables.

The tree-level tables carry site identifiers, so they stay in the caller’s output directory. The aggregate tables in notes/reference/calibration/ carry no site identifiers and are the ones safe to commit.

The archive is public

The FAIB PSP compilation is public data. This module does not download it: the caller names the directory that holds faib_header.csv, faib_sample_byvisit.csv, faib_plot_header.csv, and faib_tree_detail.csv. The manifest records each file’s name, size, row count, and SHA-256, so a run can be reproduced from the same snapshot.

What is in the weights

PHF_TREE is the per-hectare expansion factor and already includes TREE_WT and PLOT_WT for the FAIB compilation. This module still multiplies by TREE_WT defensively, because the data dictionary names both and a future extract may not fold them; the value is 1 for an ordinary tree. BA_TREE is the tree’s basal area in square metres, and VOL_MER is gross merchantable volume in cubic metres.

fresh_fvsbc.calibration.SPECIES_GROUPS: dict[str, str] = {'AC': 'HW', 'AT': 'HW', 'BG': 'BG', 'BL': 'BL', 'CW': 'HW', 'EP': 'HW', 'FD': 'FD', 'HW': 'HW', 'LW': 'LW', 'OC': 'HW', 'OH': 'HW', 'PL': 'PL', 'PW': 'PW', 'PY': 'PY', 'SE': 'SE', 'SW': 'SE', 'SX': 'SE'}

The species groups the mortality workstream uses. A code that is not here is kept as UNKNOWN rather than folded into another group silently.

exception fresh_fvsbc.calibration.CalibrationDataError[source]

Bases: ValueError

An archive or output that the dataset builder cannot use.

class fresh_fvsbc.calibration.CalibrationDataset(directory: Path, visits: DataFrame, intervals: DataFrame, tree_survival: DataFrame, coverage: DataFrame, envelope: DataFrame, species_envelope: DataFrame, species: DataFrame, manifest: Manifest)[source]

Bases: object

The tables a build returns, and where they were written.

Parameters:
  • directory (Path) – The output directory.

  • visits (pandas.DataFrame) – One row per plot visit.

  • intervals (pandas.DataFrame) – One row per consecutive visit pair.

  • tree_survival (pandas.DataFrame) – One row per live tree at the start of an interval.

  • coverage (pandas.DataFrame) – BEC coverage, including the zones the variant does not resolve.

  • envelope (pandas.DataFrame) – Basal-area and volume envelope by BEC.

  • species_envelope (pandas.DataFrame) – Basal-area envelope by BEC and species group.

  • species (pandas.DataFrame) – The PSP-to-FVS species crosswalk.

  • manifest (Mapping) – The input and output inventory.

class fresh_fvsbc.calibration.Manifest[source]

Bases: TypedDict

The input and output inventory a build writes.

inputs

Each input file’s size and SHA-256, by file name.

Type:

dict

rows

The row count of each input and output table.

Type:

dict

parameters

The interval bounds and any other build setting.

Type:

dict

supported_zones

The BEC zones the variant resolves.

Type:

list of str

unsupported_zones

The BEC zones the variant does not resolve.

Type:

list of str

source

The data source, named without a machine path.

Type:

str

archive

The archive directory’s name, not its full path.

Type:

str

fresh_fvsbc.calibration.build_dataset(psp_dir: str | Path, out_dir: str | Path, *, include_unsupported: bool = False, min_dbh: float = 0.0) → CalibrationDataset[source]

Build the calibration dataset and write it to out_dir.

Parameters:
  • psp_dir (str or Path) – The directory that holds the four FAIB PSP files.

  • out_dir (str or Path) – Where the tables and the manifest are written. The tree-level tables carry site identifiers; keep them outside the tracked tree.

  • include_unsupported (bool, optional) – Read every site, including the zones the current variant does not resolve. The default keeps the supported-zone dataset unchanged.

  • min_dbh (float, optional) – The common lower DBH limit, in cm. FAIB programs do not share one tree-detail limit: PSP subplots start at 2.0 cm while CMI/YSM/VRI/NFI tree detail starts at 4.0 cm. Set this explicitly for cross-program comparisons. Defaults to 0.0 for compatibility.

Returns:

CalibrationDataset – The tables, the crosswalk, and the manifest.

Raises:

CalibrationDataError – If an input file is missing, a required column is absent, or no supported-zone trees are present.

fresh_fvsbc.calibration.species_crosswalk(trees: DataFrame) → DataFrame[source]

Return the PSP-to-FVS species crosswalk observed in the tree table.

Parameters:

trees (pandas.DataFrame) – The normalised tree table, with a species column.

Returns:

pandas.DataFrame – One row per observed PSP code, with its FVS code when the code is one of BC’s 15, its calibration group, and a known flag.

fresh_fvsbc.calibration.species_envelope(tree_survival: DataFrame) → DataFrame[source]

Build the basal-area envelope by BEC and species group.

Parameters:

tree_survival (pandas.DataFrame) – The tree-survival table, with BEC, group, interval_id, ba, and phf.

Returns:

pandas.DataFrame – One row per BEC and group, with the plot count, BA p50/p90/p99/max, and mean TPH.

The calibration bundle contract.

A calibration bundle is a versioned JSON document that names the build it is valid for, the domain it covers, the parameters it applies, the evidence behind them, and the reviewer who signed it. It is data, not code: the package loads it, checks it, renders it into FVS keywords, and records its id in provenance. It does not fit the parameters.

Why the pins are part of the bundle

A parameter set is reproducible only against the build that produced it. The bundle names the PyFVS fork commit, the FVS source submodule commit, and the variant; the loader refuses a bundle whose pins do not match the package. A future pin is a new bundle, not an edit to an old one.

The domain

domain can name a region, a BEC, a species group, and a management regime. An empty list means “no restriction” for that field. A run asks CalibrationBundle.check_domain() with the actual BEC, species groups, and management, and the bundle refuses a run outside its domain.

The fields

parameters is a list of keyword records. Each has the FVS keyword, its raw fields, the unit, the index of the field a bound applies to, and an optional minimum and maximum. A value at a bound is a warning, not a pass: a parameter that sits at its bound is a sign the model structure or the data do not identify it.

fresh_fvsbc.bundle.CALIBRATION_KEYWORDS: frozenset[str] = frozenset({'BAIMULT', 'BAMAX', 'CRNMULT', 'DGSTDEV', 'FIXMORT', 'HTGMULT', 'MORTMSB', 'MORTMULT', 'SDICALC', 'SDIMAX'})

The keywords a bundle may render. They are the calibration levers the pinned variant supports; a bundle cannot invent a keyword the package does not know.

fresh_fvsbc.bundle.MANAGEMENT_REGIMES: frozenset[str] = frozenset({'managed', 'untreated'})

The management regimes a domain may name. managed means a treatment keyword is present; the bundle does not infer a specific treatment.

exception fresh_fvsbc.bundle.BundleBoundsError[source]

Bases: BundleError

A parameter outside the bounds the bundle declares.

class fresh_fvsbc.bundle.BundleDomain(regions: tuple[str, ...] = (), tsas: tuple[str, ...] = (), becs: tuple[str, ...] = (), species_groups: tuple[str, ...] = (), dbh_classes: tuple[tuple[float, float], ...] = (), management: tuple[str, ...] = (), qmd_min: float | None = None, ba_min: float | None = None)[source]

Bases: object

The domain a bundle covers.

An empty tuple means the field is unrestricted.

regions

The accepted BC regions.

Type:

tuple of str

tsas

The accepted TSA names, such as Quesnel TSA.

Type:

tuple of str

becs

The accepted BEC labels, such as IDFdk3 or a zone such as IDF.

Type:

tuple of str

species_groups

The accepted calibration groups.

Type:

tuple of str

dbh_classes

Inclusive DBH ranges in centimetres.

Type:

tuple of tuple of float

management

untreated, managed, or both.

Type:

tuple of str

qmd_min

The minimum initial quadratic mean diameter, in centimetres.

Type:

float, optional

ba_min

The minimum initial basal area, in square metres per hectare.

Type:

float, optional

exception fresh_fvsbc.bundle.BundleDomainError[source]

Bases: BundleError

A run outside the domain the bundle declares.

exception fresh_fvsbc.bundle.BundleError[source]

Bases: ValueError

A bundle that cannot be loaded or applied.

class fresh_fvsbc.bundle.BundleParameter(keyword: str, fields: tuple[str, ...], units: str, value_index: int, bounds: tuple[float, float] | None)[source]

Bases: object

One FVS keyword and the bounds on its numeric value.

Parameters:
  • keyword (str) – The FVS keyword, in the table’s spelling.

  • fields (tuple of str) – The raw parameter fields, in FVS’s order.

  • units (str) – The unit of the bounded value.

  • value_index (int) – The zero-based index of the field the bounds apply to.

  • bounds (tuple of float, optional) – The minimum and maximum, inclusive. None means no bound.

keyword_object() → Keyword[source]

Return the Keyword this parameter renders.

Returns:

Keyword – The keyword and its fields.

class fresh_fvsbc.bundle.CalibrationBundle(bundle_id: str, version: str, pins: Mapping[str, str], domain: BundleDomain, parameters: tuple[BundleParameter, ...], training: Mapping[str, object], review: Mapping[str, object], claim: str, warnings: tuple[str, ...] = (), ingrowth: IngrowthModel | None = None)[source]

Bases: object

A validated calibration bundle.

Parameters:
  • bundle_id (str) – The stable identifier, such as kc-pilot-2026-11-02.

  • version (str) – The bundle’s semantic version.

  • pins (Mapping) – The build the bundle is valid for: pyfvs_commit, fvs_source_commit, and variant.

  • domain (BundleDomain) – The accepted regions, BECs, species groups, DBH classes, and management.

  • parameters (tuple of BundleParameter) – The keywords the bundle renders.

  • training (Mapping) – The data checksum, split, objective, and convergence record.

  • review (Mapping) – The reviewer, date, and sign-off flag.

  • claim (str) – The claim boundary, in one sentence.

  • warnings (tuple of str) – Non-fatal findings, such as a parameter at a bound or an unsigned bundle.

  • ingrowth (IngrowthModel, optional) – The natural-regeneration model the bundle carries, if any.

ingrowth_keyword(*, bec: str, years: float, ba: float | None = None, qmd: float | None = None, cycles: int = 1, cycle_years: float | None = None, cycle: int = 1) → Keyword | None[source]

Return the bundle’s ESTAB block for a stand, if it has one.

Parameters:
  • bec (str) – The stand’s BEC label.

  • years (float) – The total interval length, in years.

  • ba (float or None, optional) – The stand’s initial BA, in m2/ha.

  • qmd (float or None, optional) – The stand’s initial QMD, in cm.

  • cycles (int, optional) – The number of establishment cycles.

  • cycle_years (float or None, optional) – The length of each cycle.

  • cycle (int, optional) – The first cycle number.

Returns:

Keyword or None – The block, or None when the bundle carries no ingrowth model.

render() → tuple[Keyword, ...][source]

Return the keywords the bundle applies, in contract order.

Returns:

tuple of Keyword – One keyword per parameter. The caller inserts them before the first PROCESS or STOP; the run does not add them itself.

record() → dict[str, str][source]

Return the two fields the provenance record carries.

Returns:

dict – calibration_id and calibration_version.

check_domain(bec: BecCode, species_groups: Sequence[str], management: str = 'untreated', dbh_values: Sequence[float] | None = None, qmd: float | None = None, ba: float | None = None, tsa: str | None = None) → None[source]

Refuse a run outside the bundle’s declared domain.

Parameters:
  • bec (BecCode) – The run’s validated BEC.

  • species_groups (Sequence of str) – The calibration groups present in the run’s tree list.

  • management (str, optional) – untreated or managed. Defaults to untreated.

  • dbh_values (Sequence of float, optional) – The DBHs present in the run. Required when the domain names DBH classes.

  • qmd (float, optional) – The initial quadratic mean diameter, in centimetres. Required when the domain names qmd_min.

  • ba (float, optional) – The initial basal area, in square metres per hectare. Required when the domain names ba_min.

  • tsa (str, optional) – The TSA name, such as Quesnel TSA. Required when the domain names tsas.

Raises:

BundleDomainError – If the BEC, region, species, management, or DBH is outside the domain.

classmethod from_record(record: Mapping[str, object]) → CalibrationBundle[source]

Validate a decoded JSON record and return the bundle.

Parameters:

record (Mapping) – The decoded bundle.

Returns:

CalibrationBundle – The validated bundle.

Raises:
  • BundleError – If a required field is missing or a value is outside its contract.

  • BundleBoundsError – If a parameter is outside its declared bounds.

fresh_fvsbc.bundle.load_bundle(path: str | Path) → CalibrationBundle[source]

Load and validate a bundle from a JSON file.

Parameters:

path (str or Path) – The bundle file.

Returns:

CalibrationBundle – The validated bundle.

Raises:

BundleError – If the file is missing, is not JSON, or is not a valid bundle.

The uncalibrated FVS-BC baseline backtest.

An interval is two consecutive PSP measurements of one plot. The baseline grows the first measurement’s tree list through FVS-BC, with no cut and no calibration, and compares the result with the second measurement. The comparison is the reference the calibration is judged against; it is not a calibration.

The sample is stratified by BEC, because a province-wide mean would hide the variants where the model fails. The tree list is built from the raw FAIB tree rows: PHF_TREE is the per-hectare expansion factor, and a tree whose species does not resolve to one of BC’s 15 is skipped and counted rather than mapped to a species it is not.

fresh_fvsbc.backtest.CROWN_BY_CLASS: dict[str, int] = {'C': 6, 'D': 8, 'I': 4, 'S': 2}

A crown-class letter in the FAIB tree table mapped to FVS’s 1-to-9 class.

fresh_fvsbc.backtest.SPECIES_ALIASES: dict[str, str] = {'BA': 'BG', 'DR': 'AT', 'HM': 'HW', 'MB': 'AC', 'SB': 'SE', 'SS': 'SE', 'SW': 'SE', 'SX': 'SE', 'W': 'OH', 'YC': 'CW'}

PSP species spellings that are aliases of one of BC’s 15. A code outside this map and outside the 15 is skipped rather than forced into a group. The donor aliases are the same layer the tree-list writer uses, so a baseline and a run of the same stand resolve Ba, Hm, Yc, Dr, and Mb the same way.

fresh_fvsbc.backtest.classes_for(records: list[TreeListFields], ba0: float) → dict[str, str][source]

Return the dominant species group, median DBH class, and BA class.

Parameters:
  • records (list of TreeListFields) – The start tree list.

  • ba0 (float) – The observed initial basal area, in m2/ha.

Returns:

dict – species_group, dbh_class, and ba_class.

fresh_fvsbc.backtest.compare_rows(interval: Mapping[str, object], observed_next: Mapping[str, float], predicted_start: Mapping[str, float], predicted_end: Mapping[str, float], envelope: Mapping[str, Mapping[str, float]]) → dict[str, object][source]

Build one comparison row from the observed and predicted summaries.

Parameters:
  • interval (Mapping) – One interval row, with its BEC, TSA, BA0, TPH0, and years.

  • observed_next (Mapping) – The second measurement: ba, tph, qmd, top_height, and volume.

  • predicted_start (Mapping) – The FVS metric summary at the start year.

  • predicted_end (Mapping) – The FVS metric summary at the end year.

  • envelope (Mapping) – The envelope mapping, from fresh_fvsbc.envelope.load_envelope().

Returns:

dict – The observed, predicted, bias, and flag fields for one interval.

fresh_fvsbc.backtest.keywords_for(interval: Mapping[str, object]) → tuple[Keyword, ...][source]

Build the uncalibrated keyword set for one interval.

Parameters:

interval (Mapping) – One row from the interval table.

Returns:

tuple of Keyword – A DATABASE request for the metric summary, the stand setup, one growth cycle of the interval’s length, and the terminator.

fresh_fvsbc.backtest.psp_tree_records(trees: DataFrame, interval: Mapping[str, object], *, tree_age: float = 60.0) → tuple[list[TreeListFields], int][source]

Build one interval’s start tree list from the raw FAIB rows.

Parameters:
  • trees (pandas.DataFrame) – The raw tree rows, with SITE_IDENTIFIER, CLSTR_ID, VISIT_NUMBER, PLOT, TREE_NO, SPECIES, DBH, HEIGHT, CR_CL, LV_D, and PHF_TREE.

  • interval (Mapping) – One row from the interval table, with SITE_IDENTIFIER, CLSTR_ID, PLOT, and visit0.

  • tree_age (float, optional) – The age at breast height to write. The PSP interval does not carry a single age; the default is a stated placeholder.

Returns:

tuple of (list of TreeListFields, int) – The records and the number of trees skipped because their species did not resolve to one of BC’s 15.

fresh_fvsbc.backtest.run_backtest(intervals: DataFrame, trees: DataFrame, visits: DataFrame, envelope: Mapping[str, Mapping[str, float]], directory: str | Path, *, per_bec: int = 1, tree_age: float = 60.0) → tuple[DataFrame, DataFrame, DataFrame][source]

Run the baseline sample and return the rows, the summary, and the skips.

Parameters:
  • intervals (pandas.DataFrame) – The interval table.

  • trees (pandas.DataFrame) – The raw tree rows for the sampled intervals.

  • visits (pandas.DataFrame) – The visit table, used for the second measurement’s QMD, top height, and volume.

  • envelope (Mapping) – The envelope from fresh_fvsbc.envelope.load_envelope().

  • directory (str or Path) – Where each run writes. One subdirectory per interval.

  • per_bec (int, optional) – The sample size per BEC. Defaults to one.

  • tree_age (float, optional) – The age placeholder written to the tree list. Defaults to 60.

Returns:

tuple of (pandas.DataFrame, pandas.DataFrame, pandas.DataFrame) – The comparison rows, the BEC summary, and one row per interval with the number of skipped trees.

fresh_fvsbc.backtest.select_intervals(intervals: DataFrame, per_bec: int = 1) → DataFrame[source]

Select a deterministic, BEC-stratified sample of intervals.

Parameters:
  • intervals (pandas.DataFrame) – The interval table from the calibration dataset. It needs BEC, TSA_DESC, SITE_IDENTIFIER, PLOT, and year0.

  • per_bec (int, optional) – The number of intervals to take from each BEC, spread across TSAs. Defaults to one.

Returns:

pandas.DataFrame – The sample, sorted by BEC and TSA.

fresh_fvsbc.backtest.summarise(rows: DataFrame, by: tuple[str, ...] = ('BEC',)) → DataFrame[source]

Aggregate the comparison rows by BEC.

Parameters:
  • rows (pandas.DataFrame) – The rows from compare_rows().

  • by (tuple of str, optional) – The columns to group by. Defaults to ("BEC",).

Returns:

pandas.DataFrame – Count, mean bias, and mean absolute error for BA, TPH, QMD, top height, and volume, by BEC.

fresh_fvsbc.backtest.summary_for_year(database: str | Path, year: int) → dict[str, float][source]

Return the metric summary row for a year.

Parameters:
  • database (str or Path) – The run’s SQLite database.

  • year (int) – The year to read. The last row is used when the year is absent.

Returns:

dict – ba, tph, qmd, top_height, and volume. volume is the metric merchantable volume (MCuM).

The empirical basal-area envelope, and the flag it produces.

The envelope is a property of the PSP sample, not a biological maximum. For each BEC the dataset carries the p99 and the observed maximum basal area. A run is flagged when its BA is above the p99 or above the maximum; it is never clipped, because a clipped number hides the disagreement the flag exists to show.

The envelope is built by fresh_fvsbc.calibration and published in notes/reference/calibration/bec_envelope.csv.

fresh_fvsbc.envelope.ENVELOPE_FLAGS: tuple[str, ...] = ('ok', 'above_p99', 'above_max', 'unknown')

the caller must decide what to do when the BEC is outside the envelope table.

Type:

The flag values, from best to worst. unknown is not a pass

fresh_fvsbc.envelope.envelope_flags(ba: float, bec: str, envelope: Mapping[str, Mapping[str, float]]) → str[source]

Return the envelope flag for one basal-area value.

Parameters:
  • ba (float) – The basal area, in m2/ha.

  • bec (str) – The BEC label, such as IDFdk3.

  • envelope (Mapping) – The mapping from load_envelope().

Returns:

str – ok, above_p99, above_max, or unknown. A BEC outside the table is unknown rather than ok.

fresh_fvsbc.envelope.load_envelope(path: str | Path) → dict[str, dict[str, float]][source]

Load the envelope table into a mapping by BEC.

Parameters:

path (str or Path) – The bec_envelope.csv file.

Returns:

dict – Each BEC mapped to {"ba_p99": ..., "ba_max": ...}.

Raises:

ValueError – If the file lacks the two required columns.

The FVS-BC keyword-set validator.

docs/reference/keywords.rst is the contract. This module holds FVS’s keyword table as it is declared in base/keywds.f, validates a keyword set against it, and gives the set a hash.

What the table is

TABLE is a CHARACTER*8 array of 150 slots, declared in base/keywds.f for the base model; BC adds nothing to it. Six of the slots are blank, and a blank slot is not a keyword, so the table holds 144 named keywords.

STOP is not in the table, and is still a keyword

base/keyrdr.f reads each record, upcases its first eight characters, and compares them with STOP before it consults the table; a match ends the file with code 3. So STOP is a keyword that keywds.f does not hold, and it is in SPECIAL_KEYWORDS rather than in the table. The run orchestration found this the first time it wrote a whole key file: every real key file ends with STOP, and a validator that refused it refused all of them.

Matching is FVS’s own. base/fndkey.f compares the caller’s word to the table with FORTRAN’s .EQ. on character strings, which is an exact, case-sensitive, blank-padded comparison. A validator that accepted database would therefore accept something FVS does not, so this one does not either; it rejects the name and points at the table’s spelling when there is one close.

The misspelling

Slot 30 holds xSALVAGE, with a leading x, which is a spelling defect in FVS itself. FVS matches what the table holds, so xSALVAGE is the correct spelling and SALVAGE is not a keyword at all. The validator warns when a set contains xSALVAGE, because a caller who reached for that name is working around a defect they should know about, and it names xSALVAGE when a caller writes SALVAGE instead.

What this module does not do

Contract rule 4, that a set never silently gains a keyword the caller did not write, is not enforced here. The one exception the contract allows is the tree-record format, which is a property of how a run is set up rather than of validation, and it is handled where the run is.

fresh_fvsbc.keywords.BLANK_SLOTS: tuple[int, ...] = (114, 118, 120, 148, 149, 150)

The 1-based slot numbers that hold no keyword.

fresh_fvsbc.keywords.BLOCK_BODIES: dict[str, frozenset[str]] = {'ESTAB': frozenset({'ADDTREES', 'AUTALLY', 'BUDWORM', 'BURNPREP', 'ESRANSD', 'ESTAB', 'EZCRUISE', 'HABGROUP', 'HTADJ', 'INGROW', 'MECHPREP', 'MINPLOTS', 'NATURAL', 'NOAUTALY', 'NOINGROW', 'NOSPROUT', 'OUTPUT', 'PASSALL', 'PLANT', 'PLOTINFO', 'RESETAGE', 'SPECMULT', 'SPROUT', 'STOCKADJ', 'TALLY', 'TALLYONE', 'TALLYTWO', 'THRSHOLD'}), 'FMIN': frozenset({'BURNREPT', 'CANCALC', 'CANFPROF', 'CARBCALC', 'CARBCUT', 'CARBREPT', 'CFIMCALC', 'DEFULMOD', 'DROUGHT', 'DUFFPROD', 'DWDCVOUT', 'DWDVLOUT', 'FIRECALC', 'FLAMEADJ', 'FMODLIST', 'FMORTMLT', 'FUELDCAY', 'FUELFOTO', 'FUELINIT', 'FUELMODL', 'FUELMOVE', 'FUELMULT', 'FUELOUT', 'FUELPOOL', 'FUELREPT', 'FUELSOFT', 'FUELTRET', 'LANDOUT', 'MOISTURE', 'MOREOUT', 'MORTCLAS', 'MORTREPT', 'PILEBURN', 'POTFIRE', 'POTFMOIS', 'POTFPAB', 'POTFSEAS', 'POTFTEMP', 'POTFWIND', 'SALVAGE', 'SALVSP', 'SIMFIRE', 'SNAGBRK', 'SNAGCLAS', 'SNAGDCAY', 'SNAGFALL', 'SNAGINIT', 'SNAGOUT', 'SNAGPBN', 'SNAGPSFT', 'SNAGSUM', 'SOILHEAT', 'STATFUEL', 'SVIMAGES', 'USECFIM'}), 'MISTOE': frozenset({'DMAUTO', 'DMBCA', 'DMBCI', 'DMCAP', 'DMCLUMP', 'DMCRTHRD', 'DMDEATH', 'DMETUNE', 'DMFLOWER', 'DMITUNE', 'DMKTUNE', 'DMLIGHT', 'DMOPAQ', 'DMOPQMLT', 'DMSED', 'DMSTUNE', 'DMTABLE', 'MISTABLE', 'MISTGMOD', 'MISTHMOD', 'MISTMORT', 'MISTMULT', 'MISTOFF', 'MISTPINF', 'MISTPREF', 'MISTPRT', 'NEWSPRED'}), 'RDIN': frozenset({'BBCLEAR', 'BBOUT', 'BBTYPE1', 'BBTYPE2', 'BBTYPE3', 'BBTYPE4', 'BORATE', 'CARRY', 'COMMENT', 'DNSCALC', 'INFCOLO', 'INFKILL', 'INFMULT', 'INFSIMS', 'INOCLIFE', 'INOCSPAN', 'OPEN', 'PLOTINF', 'PLREAD', 'PSTUMP', 'RRCOMP', 'RRDOUT', 'RRECHO', 'RRHOSTS', 'RRINIT', 'RRJUMP', 'RRMINK', 'RRTREIN', 'RRTYPE', 'RSEED', 'SAREA', 'SDIRMULT', 'SMCOUT', 'SPORE', 'SPREAD', 'STREAD', 'TDISTN', 'TIMEDEAD', 'TTDMULT', 'WINDTHR'})}

Each keyword block, and the keywords its reader accepts. The body is rendered with the same writer as any other keyword, so a parameter lands in FVS’s ten-column field rather than being hand-spaced — a hand-spaced field is read as a different number and FVS reports nothing.

fresh_fvsbc.keywords.BLOCK_KEYWORDS: frozenset[str] = frozenset({'COMMENT', 'COMPUTE', 'DATABASE', 'ESTAB', 'FMIN', 'IF', 'MISTOE', 'RDIN'})

Every keyword that takes a block of some kind.

fresh_fvsbc.keywords.BODY_KEYWORDS: frozenset[str] = frozenset({'ADDTREES', 'AUTALLY', 'BBCLEAR', 'BBOUT', 'BBTYPE1', 'BBTYPE2', 'BBTYPE3', 'BBTYPE4', 'BORATE', 'BUDWORM', 'BURNPREP', 'BURNREPT', 'CANCALC', 'CANFPROF', 'CARBCALC', 'CARBCUT', 'CARBREPT', 'CARRY', 'CFIMCALC', 'COMMENT', 'DEFULMOD', 'DMAUTO', 'DMBCA', 'DMBCI', 'DMCAP', 'DMCLUMP', 'DMCRTHRD', 'DMDEATH', 'DMETUNE', 'DMFLOWER', 'DMITUNE', 'DMKTUNE', 'DMLIGHT', 'DMOPAQ', 'DMOPQMLT', 'DMSED', 'DMSTUNE', 'DMTABLE', 'DNSCALC', 'DROUGHT', 'DUFFPROD', 'DWDCVOUT', 'DWDVLOUT', 'ESRANSD', 'ESTAB', 'EZCRUISE', 'FIRECALC', 'FLAMEADJ', 'FMODLIST', 'FMORTMLT', 'FUELDCAY', 'FUELFOTO', 'FUELINIT', 'FUELMODL', 'FUELMOVE', 'FUELMULT', 'FUELOUT', 'FUELPOOL', 'FUELREPT', 'FUELSOFT', 'FUELTRET', 'HABGROUP', 'HTADJ', 'INFCOLO', 'INFKILL', 'INFMULT', 'INFSIMS', 'INGROW', 'INOCLIFE', 'INOCSPAN', 'LANDOUT', 'MECHPREP', 'MINPLOTS', 'MISTABLE', 'MISTGMOD', 'MISTHMOD', 'MISTMORT', 'MISTMULT', 'MISTOFF', 'MISTPINF', 'MISTPREF', 'MISTPRT', 'MOISTURE', 'MOREOUT', 'MORTCLAS', 'MORTREPT', 'NATURAL', 'NEWSPRED', 'NOAUTALY', 'NOINGROW', 'NOSPROUT', 'OPEN', 'OUTPUT', 'PASSALL', 'PILEBURN', 'PLANT', 'PLOTINF', 'PLOTINFO', 'PLREAD', 'POTFIRE', 'POTFMOIS', 'POTFPAB', 'POTFSEAS', 'POTFTEMP', 'POTFWIND', 'PSTUMP', 'RESETAGE', 'RRCOMP', 'RRDOUT', 'RRECHO', 'RRHOSTS', 'RRINIT', 'RRJUMP', 'RRMINK', 'RRTREIN', 'RRTYPE', 'RSEED', 'SALVAGE', 'SALVSP', 'SAREA', 'SDIRMULT', 'SIMFIRE', 'SMCOUT', 'SNAGBRK', 'SNAGCLAS', 'SNAGDCAY', 'SNAGFALL', 'SNAGINIT', 'SNAGOUT', 'SNAGPBN', 'SNAGPSFT', 'SNAGSUM', 'SOILHEAT', 'SPECMULT', 'SPORE', 'SPREAD', 'SPROUT', 'STATFUEL', 'STOCKADJ', 'STREAD', 'SVIMAGES', 'TALLY', 'TALLYONE', 'TALLYTWO', 'TDISTN', 'THRSHOLD', 'TIMEDEAD', 'TTDMULT', 'USECFIM', 'WINDTHR'})

Every keyword that is only valid inside a keyword block.

fresh_fvsbc.keywords.ESTAB_KEYWORDS: frozenset[str] = frozenset({'ADDTREES', 'AUTALLY', 'BUDWORM', 'BURNPREP', 'ESRANSD', 'ESTAB', 'EZCRUISE', 'HABGROUP', 'HTADJ', 'INGROW', 'MECHPREP', 'MINPLOTS', 'NATURAL', 'NOAUTALY', 'NOINGROW', 'NOSPROUT', 'OUTPUT', 'PASSALL', 'PLANT', 'PLOTINFO', 'RESETAGE', 'SPECMULT', 'SPROUT', 'STOCKADJ', 'TALLY', 'TALLYONE', 'TALLYTWO', 'THRSHOLD'})

The regeneration keywords ESTAB reads, from estb/esin.f’s own table. ESTAB appears in it because the block can schedule another establishment; it is also a top-level keyword.

fresh_fvsbc.keywords.FIELD_WIDTH: int = 10

The width of a parameter field. base/keyrdr.f reads KARD(I)=RECORD(J:J+9) with J stepping by ten from eleven, so every parameter is a ten-column field of the keyword’s own record and a longer one cannot be carried.

fresh_fvsbc.keywords.FIRE_MODEL_ORDER_WARNING: str = "an FMIN block that asks for FUELOUT or POTFIRE must precede INVYEAR: both capture the inventory year when they are read, and FVS treats a positive value as 'suppress this report', so the fuels and potfire tables would be silently not written"

The warning the validator gives for a fire-model block that follows INVYEAR.

fresh_fvsbc.keywords.FMIN_KEYWORDS: frozenset[str] = frozenset({'BURNREPT', 'CANCALC', 'CANFPROF', 'CARBCALC', 'CARBCUT', 'CARBREPT', 'CFIMCALC', 'DEFULMOD', 'DROUGHT', 'DUFFPROD', 'DWDCVOUT', 'DWDVLOUT', 'FIRECALC', 'FLAMEADJ', 'FMODLIST', 'FMORTMLT', 'FUELDCAY', 'FUELFOTO', 'FUELINIT', 'FUELMODL', 'FUELMOVE', 'FUELMULT', 'FUELOUT', 'FUELPOOL', 'FUELREPT', 'FUELSOFT', 'FUELTRET', 'LANDOUT', 'MOISTURE', 'MOREOUT', 'MORTCLAS', 'MORTREPT', 'PILEBURN', 'POTFIRE', 'POTFMOIS', 'POTFPAB', 'POTFSEAS', 'POTFTEMP', 'POTFWIND', 'SALVAGE', 'SALVSP', 'SIMFIRE', 'SNAGBRK', 'SNAGCLAS', 'SNAGDCAY', 'SNAGFALL', 'SNAGINIT', 'SNAGOUT', 'SNAGPBN', 'SNAGPSFT', 'SNAGSUM', 'SOILHEAT', 'STATFUEL', 'SVIMAGES', 'USECFIM'})

The fire-model keywords FMIN reads, from metric/fire/base/fmin.f’s own table. They are not top-level keywords: FVS accepts them only between FMIN and its END, so the validator refuses one that stands alone rather than writing a record FVS would ignore.

fresh_fvsbc.keywords.KEYWORDS: frozenset[str] = frozenset({'ADDFILE', 'AGPLABEL', 'ALSOTRY', 'ANIN', 'ATRTLIST', 'BAIMULT', 'BAMAX', 'BFDEFECT', 'BFFDLN', 'BFVOLEQU', 'BFVOLUME', 'BMIN', 'BRUST', 'CALBSTAT', 'CCADJ', 'CFVOLEQU', 'CHEAPO', 'CLIMATE', 'CLOSE', 'COMPRESS', 'COMPUTE', 'COVER', 'CRNMULT', 'CUTEFF', 'CUTLIST', 'CWEQN', 'CYCLEAT', 'DATABASE', 'DATASCRN', 'DEBUG', 'DEFECT', 'DELOTAB', 'DESIGN', 'DFB', 'DFTM', 'DGSTDEV', 'ECHO', 'ECHOSUM', 'ECON', 'ENDFILE', 'ENDIF', 'ESTAB', 'FERTILIZ', 'FIAVBC', 'FIXCW', 'FIXDG', 'FIXHTG', 'FIXMORT', 'FMIN', 'FVSSTAND', 'GROWTH', 'HTGMULT', 'HTGSTOP', 'IF', 'INVYEAR', 'LOCATE', 'MANAGED', 'MCDEFECT', 'MCFDLN', 'MGMTID', 'MINHARV', 'MISTOE', 'MODTYPE', 'MORTMSB', 'MORTMULT', 'MPB', 'NOAUTOES', 'NOCALIB', 'NODEBUG', 'NOECHO', 'NOHTDREG', 'NOSCREEN', 'NOSUM', 'NOTREES', 'NOTRIPLE', 'NUMCYCLE', 'NUMTRIP', 'OPEN', 'ORGANON', 'POINTREF', 'PRMFROST', 'PROCESS', 'PRUNE', 'PTGROUP', 'RANNSEED', 'RDIN', 'READCORD', 'READCORH', 'READCORR', 'REGDMULT', 'REGHMULT', 'RESETAGE', 'REUSCORD', 'REUSCORH', 'REUSCORR', 'REWIND', 'RRIN', 'SCREEN', 'SDICALC', 'SDIMAX', 'SERLCORR', 'SETPTHIN', 'SETSITE', 'SITECODE', 'SPCODES', 'SPECPREF', 'SPGROUP', 'SPLABEL', 'SPLEAVE', 'STANDCN', 'STATS', 'STDIDENT', 'STDINFO', 'STRCLASS', 'SVS', 'TCONDMLT', 'TFIXAREA', 'THEN', 'THINABA', 'THINATA', 'THINAUTO', 'THINBBA', 'THINBTA', 'THINCC', 'THINDBH', 'THINHT', 'THINMIST', 'THINPRSC', 'THINPT', 'THINQFA', 'THINRDEN', 'THINRDSL', 'THINSDI', 'TIMEINT', 'TOPKILL', 'TREEDATA', 'TREEFMT', 'TREELIST', 'TREESZCP', 'VOLEQNUM', 'VOLUME', 'WSBW', 'YARDLOSS', 'xSALVAGE'})

The named keywords, as a set for lookup.

fresh_fvsbc.keywords.KEYWORD_BLOCK_KEYWORDS: frozenset[str] = frozenset({'ESTAB', 'FMIN', 'MISTOE', 'RDIN'})

The block keywords whose body holds other keywords and is closed by END.

fresh_fvsbc.keywords.KEYWORD_TABLE: tuple[str | None, ...] = ('PROCESS', 'TIMEINT', 'FIXCW', 'TREEDATA', 'TREEFMT', 'MPB', 'DFTM', 'WSBW', 'CWEQN', 'DESIGN', 'NUMCYCLE', 'TFIXAREA', 'GROWTH', 'STDINFO', 'STDIDENT', 'INVYEAR', 'TREELIST', 'REWIND', 'NOSUM', 'DEBUG', 'ECHOSUM', 'ADDFILE', 'THINAUTO', 'THINBTA', 'THINATA', 'THINBBA', 'THINABA', 'THINPRSC', 'THINDBH', 'xSALVAGE', 'SPLABEL', 'AGPLABEL', 'COMPUTE', 'FERTILIZ', 'THINHT', 'STATS', 'TOPKILL', 'HTGSTOP', 'MCFDLN', 'BFFDLN', 'MCDEFECT', 'BFDEFECT', 'VOLUME', 'BFVOLUME', 'REGDMULT', 'COVER', 'ESTAB', 'MINHARV', 'SPECPREF', 'SPCODES', 'NODEBUG', 'CUTEFF', 'NOTRIPLE', 'READCORD', 'REUSCORD', 'NOCALIB', 'DGSTDEV', 'BAIMULT', 'MORTMULT', 'NOHTDREG', 'RANNSEED', 'HTGMULT', 'CHEAPO', 'NUMTRIP', 'ENDFILE', 'BAMAX', 'READCORH', 'REUSCORH', 'MGMTID', 'REGHMULT', 'TCONDMLT', 'NOAUTOES', 'READCORR', 'REUSCORR', 'BRUST', 'IF', 'SCREEN', 'COMPRESS', 'THEN', 'ALSOTRY', 'ENDIF', 'NOTREES', 'CALBSTAT', 'OPEN', 'CLOSE', 'NOSCREEN', 'RRIN', 'FIXMORT', 'SDIMAX', 'DELOTAB', 'SERLCORR', 'CUTLIST', 'RESETAGE', 'SITECODE', 'MISTOE', 'CRNMULT', 'CFVOLEQU', 'BFVOLEQU', 'ANIN', 'DFB', 'RDIN', 'MANAGED', 'YARDLOSS', 'FMIN', 'STRCLASS', 'MODTYPE', 'FVSSTAND', 'PRUNE', 'SVS', 'FIXDG', 'FIXHTG', 'THINSDI', 'LOCATE', None, 'THINCC', 'ECON', 'DATABASE', None, 'DEFECT', None, 'STANDCN', 'THINMIST', 'TREESZCP', 'THINRDEN', 'SPGROUP', 'BMIN', 'DATASCRN', 'SETPTHIN', 'THINPT', 'VOLEQNUM', 'POINTREF', 'ECHO', 'NOECHO', 'CYCLEAT', 'ATRTLIST', 'THINRDSL', 'MORTMSB', 'SETSITE', 'CLIMATE', 'SDICALC', 'THINQFA', 'PTGROUP', 'ORGANON', 'SPLEAVE', 'CCADJ', 'PRMFROST', 'FIAVBC', None, None, None)

FVS’s keyword table, TABLE in base/keywds.f, in slot order. None is a blank slot, and a blank slot is not a keyword. Names are stored as CHARACTER*8 compares them, without the trailing blanks that pads them.

fresh_fvsbc.keywords.LINES_KEYWORDS: frozenset[str] = frozenset({'COMMENT', 'COMPUTE', 'DATABASE'})

The keywords whose content is a block of text lines ended by END. A caller supplies the lines, and the writer supplies the terminator, because a block without one swallows the rest of the file.

fresh_fvsbc.keywords.MISSPELLED_KEYWORD: str = 'xSALVAGE'

The keyword whose spelling is defective in FVS, at slot 30.

fresh_fvsbc.keywords.MISTOE_KEYWORDS: frozenset[str] = frozenset({'DMAUTO', 'DMBCA', 'DMBCI', 'DMCAP', 'DMCLUMP', 'DMCRTHRD', 'DMDEATH', 'DMETUNE', 'DMFLOWER', 'DMITUNE', 'DMKTUNE', 'DMLIGHT', 'DMOPAQ', 'DMOPQMLT', 'DMSED', 'DMSTUNE', 'DMTABLE', 'MISTABLE', 'MISTGMOD', 'MISTHMOD', 'MISTMORT', 'MISTMULT', 'MISTOFF', 'MISTPINF', 'MISTPREF', 'MISTPRT', 'NEWSPRED'})

The dwarf-mistletoe keywords MISTOE reads, from newmist/misin.f’s own table.

fresh_fvsbc.keywords.RDIN_KEYWORDS: frozenset[str] = frozenset({'BBCLEAR', 'BBOUT', 'BBTYPE1', 'BBTYPE2', 'BBTYPE3', 'BBTYPE4', 'BORATE', 'CARRY', 'COMMENT', 'DNSCALC', 'INFCOLO', 'INFKILL', 'INFMULT', 'INFSIMS', 'INOCLIFE', 'INOCSPAN', 'OPEN', 'PLOTINF', 'PLREAD', 'PSTUMP', 'RRCOMP', 'RRDOUT', 'RRECHO', 'RRHOSTS', 'RRINIT', 'RRJUMP', 'RRMINK', 'RRTREIN', 'RRTYPE', 'RSEED', 'SAREA', 'SDIRMULT', 'SMCOUT', 'SPORE', 'SPREAD', 'STREAD', 'TDISTN', 'TIMEDEAD', 'TTDMULT', 'WINDTHR'})

The root-disease keywords RDIN reads, from rd/rdinie.f’s own table. --RDv3-- is a separator in that table rather than a keyword.

fresh_fvsbc.keywords.RECORD_KEYWORDS: frozenset[str] = frozenset({'STDIDENT', 'TREEFMT'})

The keywords whose content is a record of its own rather than a field of the keyword’s record. canada/bc/initre.f reads the record after STDIDENT as the stand id and title, and intree.f reads the two after TREEFMT as the format.

fresh_fvsbc.keywords.SPECIAL_KEYWORDS: frozenset[str] = frozenset({'COMMENT', 'STOP'})

The keywords FVS matches before it consults the table. base/keyrdr.f compares the record’s first eight characters with STOP and returns code 3, which ends the keyword file, so STOP is a keyword that base/keywds.f does not hold. A validator that refused it would refuse every real key file, which is what the run orchestration found the first time it wrote one.

fresh_fvsbc.keywords.STAND_KEYWORDS: frozenset[str] = frozenset({'STANDSQL', 'STDIDENT', 'STDINFO'})

The keywords that start a stand’s setup. A DATABASE block must precede the first of them: FVS refuses it later with FVS16 ERROR: KEYWORD ENTERED IS USED IN WRONG CONTEXT, which is what the official case avoids by putting the block first.

fresh_fvsbc.keywords.SUPPLEMENTAL_KEYWORDS: frozenset[str] = frozenset({'DEFULMOD', 'FUELMODL', 'THINQFA'})

IF opens a conditional whose body is a sequence of keywords and whose terminator is ENDIF. That is a nested structure this model does not have, so it is refused by name rather than written wrongly. Keywords that read one extra record after their own. THINQFA is the one in this variant: its own record carries the year and six parameters, and the record that follows names the units of its residual-density target. cuts.f reads PRMS(1..6) from the six post-year fields, which leaves QFATAR = INT(PRMS(7)) for the supplemental record’s value; the keyword guide says the same.

Type:

The keywords whose block holds other keywords rather than lines

class fresh_fvsbc.keywords.Keyword(name: str, parameters: tuple[str, ...] = (), lines: tuple[str, ...] = (), nested: tuple[Keyword, ...] = (), otherwise: tuple[Keyword, ...] = (), supplemental: tuple[str, ...] = ())[source]

Bases: object

One keyword and its parameters, in the order FVS reads them.

Parameters:
  • name (str) – The keyword, spelled as FVS’s table spells it.

  • parameters (tuple of str, optional) – The values that follow the keyword. FVS reads them as separate tokens, so a parameter may contain spaces.

  • lines (tuple of str, optional) – The body of a block keyword, one record per entry. Only LINES_KEYWORDS take lines, and the writer adds the END that closes them.

  • nested (tuple of Keyword, optional) – The body of a conditional, which holds keywords rather than lines. Only IF takes one, and the writer adds the ENDIF that closes it.

  • otherwise (tuple of Keyword, optional) – The ELSE branch of a conditional.

  • supplemental (tuple of str, optional) – The record FVS reads after this keyword’s own. Only SUPPLEMENTAL_KEYWORDS take one, and the writer does not close it: the record stands alone on its line.

exception fresh_fvsbc.keywords.KeywordOrderError[source]

Bases: ValueError

A keyword FVS refuses in the position the set gives it.

class fresh_fvsbc.keywords.KeywordSet(keywords: tuple[Keyword, ...] = ())[source]

Bases: object

An ordered keyword set, validated, carrying the hash of the text it renders to.

Parameters:

keywords (Sequence[Keyword]) – The keywords, in the order FVS will read them.

Raises:

UnknownKeywordError – If any keyword’s name is not in FVS’s table.

property digest: str

Return the hash of the text this set renders to, naming the algorithm.

render() → str[source]

Return the text that FVS would read for this set.

Returns:

str – The rendered set, with one token per line.

exception fresh_fvsbc.keywords.KeywordWidthError[source]

Bases: ValueError

A parameter that FVS’s ten-column field cannot carry.

exception fresh_fvsbc.keywords.UnknownKeywordError[source]

Bases: ValueError

A keyword that FVS’s table does not hold.

exception fresh_fvsbc.keywords.UnsupportedKeywordError[source]

Bases: ValueError

A keyword whose content is a block, which this module does not write.

fresh_fvsbc.keywords.keyword_set_hash(keywords: Sequence[Keyword]) → str[source]

Return the hash of the text a keyword set renders to.

The hash is taken over the rendered text rather than over a structure, so that it identifies the exact bytes a run was given, which is what makes it useful as provenance. The algorithm is named in the result, so a reader does not have to know it.

Parameters:

keywords (Sequence[Keyword]) – The keywords, in order. Order is part of the identity, because FVS processes a set in order.

Returns:

str – "sha256:" followed by the hexadecimal digest.

fresh_fvsbc.keywords.render_keyword(keyword: Keyword) → str[source]

Render one keyword as the record FVS reads for it.

FVS reads a keyword from the first eight columns of a record and its parameters from ten-column fields of the same record, KARD(I)=RECORD(J:J+9) with J stepping by ten from eleven. So a parameter is a field, and the field is padded rather than separated by a delimiter. Two keywords are the exception and carry their content on the records that follow them: STDIDENT and TREEFMT.

Parameters:

keyword (Keyword) – The keyword and its parameters.

Returns:

str – The keyword’s record, ending with a newline, or the keyword and the records that follow it.

Raises:

KeywordWidthError – If a parameter is wider than the field FVS reads it from.

fresh_fvsbc.keywords.render_keywords(keywords: Sequence[Keyword]) → str[source]

Render a keyword set as the text that FVS would read.

Parameters:

keywords (Sequence[Keyword]) – The keywords, in order.

Returns:

str – The rendered set.

fresh_fvsbc.keywords.validate_keyword(keyword: Keyword) → list[str][source]

Validate one keyword against FVS’s table.

Parameters:

keyword (Keyword) – The keyword and its parameters.

Returns:

list of str – The warnings this keyword raises. An empty list means there are none.

Raises:

UnknownKeywordError – If the name is not in the table. The message names the keyword, and names the table’s spelling when the name is a near miss.

fresh_fvsbc.keywords.validate_keywords(keywords: Sequence[Keyword]) → list[str][source]

Validate a whole keyword set, in order.

Parameters:

keywords (Sequence[Keyword]) – The keywords, in the order FVS will read them.

Returns:

list of str – Every warning the set raises, in the order the keywords were checked.

Raises:

UnknownKeywordError – On the first keyword whose name is not in the table. Validation stops there rather than reporting the rest, because a set with an unknown keyword cannot be run.

The nemora adapter.

docs/reference/nemora-ingest.rst is the contract. It takes a synthetic stem population shaped like nemora’s output and returns the tree-list fields that fresh_fvsbc.treelist writes.

Three things the adapter cannot take from the population

nemora emits no species and no tree age, and FVS-BC needs both, so the caller supplies them. The adapter fails when either is missing rather than choosing one, which is what the contract requires; a defaulted species would quietly model the wrong tree.

The plot-level fields — slope, aspect, habitat, topographic position, and site preparation — are properties of a run rather than of a stem population, so the contract takes them from the keyword set. This adapter writes the values it is given for them, and zero when it is given none, because a tree record has to carry something in those columns and the keyword set supplies the run’s own.

The crown ratio is a conversion, not a copy

The two sides are not the same shape. nemora emits a fraction, and the tree-list field is a crown class, 1 to 9, which FVS expands to a percent crown ratio by n*10-5 — see fresh_fvsbc.treelist and the contract for the source. So class 7 means 65 percent.

The adapter takes the class whose expanded percentage is nearest the fraction’s, and refuses a fraction it cannot represent. FVS’s own arithmetic sets the reachable band: class 9 reaches 85 percent and class 10 would be needed above it, which the field’s single digit cannot hold. So a fraction at or above 0.9 is refused rather than rounded down, because rounding it down would silently understate a crown by up to five percent.

fresh_fvsbc.nemora.CROWN_CLASS_MIN: int = 1

The reachable crown classes. FVS treats a value below 10 as a class and expands it by n*10-5.

exception fresh_fvsbc.nemora.CrownRatioError[source]

Bases: PopulationError

A crown ratio that the one-digit tree-list field cannot carry.

exception fresh_fvsbc.nemora.MissingAssignmentError[source]

Bases: PopulationError

An input the caller must supply, which nemora does not emit.

class fresh_fvsbc.nemora.PlotFields[source]

Bases: TypedDict

The plot-level fields, which belong to a run rather than to a stem population.

exception fresh_fvsbc.nemora.PopulationError[source]

Bases: ValueError

A population the adapter cannot turn into a tree list.

fresh_fvsbc.nemora.crown_class(crown_ratio: float) → int[source]

Return the crown class nearest a fraction.

Parameters:

crown_ratio (float) – A crown ratio as a fraction, which is the shape nemora emits.

Returns:

int – The crown class whose expanded percentage is nearest, by crown_percent().

Raises:

CrownRatioError – If the fraction is outside 0 to 1, or if the nearest class falls outside 1 to 9. The second is the case the contract calls a value the one-digit field cannot represent: a fraction at or above 0.9 would need class 10, and rounding it down would understate a crown by up to five percent, so it is refused rather than approximated.

fresh_fvsbc.nemora.crown_percent(crown_class_value: int) → int[source]

Return the percent crown ratio FVS expands a class to.

Parameters:

crown_class_value (int) – A crown class, 1 to 9.

Returns:

int – n*10-5, which is what base/intree.f computes for a value below 10.

Raises:

CrownRatioError – If the class is outside 1 to 9.

fresh_fvsbc.nemora.to_tree_list(population: Sequence[Mapping[str, object]], *, species: str | None = None, tree_age: float | None = None, plot_fields: PlotFields | None = None) → list[TreeListFields][source]

Turn a synthetic stem population into tree-list fields.

Parameters:
  • population (Sequence of Mapping) – The stems, shaped like nemora’s output. Only stand_id, dbh_cm, height_m, and crown_ratio are read; the spatial fields have no home in an FVS tree list, and the attribute fields are not columns FVS-BC reads.

  • species (str, optional) – The species code, from any of BC’s three lists. Required: nemora emits none, and the adapter fails rather than defaulting.

  • tree_age (float, optional) – The age at breast height, in years. Required, for the same reason.

  • plot_fields (PlotFields, optional) – The plot-level fields, which belong to a run. Each defaults to zero.

Returns:

list of TreeListFields – One record per stem, ready for fresh_fvsbc.treelist.write_tree_record().

Raises:
  • MissingAssignmentError – If species or tree_age is not supplied.

  • PopulationError – If a record is missing a field the adapter reads, or carries a non-numeric one.

  • CrownRatioError – If a crown ratio cannot be represented by the tree-list field.

  • SpeciesError – If the species code resolves to no BC species, or to more than one.

The FVS-BC output reader.

docs/reference/outputs.rst is the contract. This module reads the run’s SQLite database back, exposes the tables it has tested, and separates the two kinds of row that FVS_Error holds.

What the run actually gives, and why this reader is shaped this way

The contract says the database is the primary output and names 44 FVS_ tables. Which of them a run writes is the keyword set’s business: a DATABASE block names the tables, and a table it does not name is absent. An earlier revision of this docstring said a run produced FVS_Cases and FVS_Error and no more, because PyFVS occupied the block; both claims were wrong, and the run that reads thirteen tables disproved them.

Some tables need more than a DATABASE option. FMIN is a block of options — FUELOUT, POTFIRE, CARBREPT and SIMFIRE live inside it, not in the keyword table — and it is how a caller reaches the fuels, potfire, carbon and consumption tables. SIMFIRE schedules a fire event, which is what makes a consumption row exist at all. notes/reference/table-reachability.md records which of the rest can be written at all, and why the others cannot.

The summary is also returned in memory by FVS.summary, and the run writes a fixed-width .sum report beside the database.

FVS_Error is not an error channel only

It holds FVS warnings as well as errors. A run whose site data is outside the model’s range writes FVS03 WARNING: FOREST CODE INDICATES THE GEOGRAPHIC LOCATION IS OUTSIDE THE RANGE OF THE MODEL, which is not a failure. So the contract’s rule that a non-empty FVS_Error means a failed run is not the rule a reader can apply; the rule is that the rows of severity ERROR make a run failed, and this module separates the two by reading the severity FVS writes into each message.

fresh_fvsbc.outputs.CASES_COLUMNS: tuple[str, ...] = ('CaseID', 'Stand_CN', 'StandID', 'MgmtID', 'RunTitle', 'KeywordFile', 'SamplingWt', 'Variant', 'Version', 'RV', 'Groups', 'RunDateTime')

The columns of FVS_Cases, from dbsqlite/dbscase.f.

fresh_fvsbc.outputs.DATABASE_TABLES: tuple[str, ...] = ('FVS_ATRTList_Metric', 'FVS_BurnReport_Metric', 'FVS_CalibStats', 'FVS_CanProfile', 'FVS_Carbon_Metric', 'FVS_Cases', 'FVS_Climate_Metric', 'FVS_Compute', 'FVS_Consumption_Metric', 'FVS_CutList_Metric', 'FVS_DM_Spp_Sum_Metric', 'FVS_DM_Stnd_Sum_Metric', 'FVS_DM_Sz_Sum_Metric', 'FVS_DM_TreeList_Metric', 'FVS_Down_Wood_Cov_Metric', 'FVS_Down_Wood_Vol_Metric', 'FVS_EconHarvestValue_Metric', 'FVS_EconSummary_Metric', 'FVS_Error', 'FVS_FIAVBC_ATRTList', 'FVS_FIAVBC_CutList', 'FVS_FIAVBC_Summary', 'FVS_FIAVBC_TreeList', 'FVS_Fuels_Metric', 'FVS_Hrv_Carbon', 'FVS_InvReference', 'FVS_Mortality_Metric', 'FVS_PotFire_Cond_Metric', 'FVS_PotFire_Metric', 'FVS_RD_Beetle_Metric', 'FVS_RD_Det_Metric', 'FVS_RD_Sum_Metric', 'FVS_Regen_HabType', 'FVS_Regen_Ingrow_Metric', 'FVS_Regen_SitePrep', 'FVS_Regen_Sprouts_Metric', 'FVS_Regen_Tally_Metric', 'FVS_SnagDet_Metric', 'FVS_SnagSum_Metric', 'FVS_Stats_Species_Metric', 'FVS_Stats_Stand', 'FVS_StrClass_Metric', 'FVS_Summary_Metric', 'FVS_TreeList_Metric')

Every FVS_-prefixed table the database component can create, per the contract.

fresh_fvsbc.outputs.ERROR_COLUMNS: tuple[str, ...] = ('CaseID', 'StandID', 'Message')

right shape rather than one a caller has to special-case.

fresh_fvsbc.outputs.IDENTITY_COLUMN: str = 'CaseID'

The column that joins a table back to FVS_Cases.

fresh_fvsbc.outputs.ORACLE_TABLES: tuple[str, ...] = ('FVS_Down_Wood_Vol_Metric', 'FVS_SnagDet_Metric', 'FVS_SnagSum_Metric', 'FVS_Summary_Metric')

from a table outside this set must not present it as verified.

fresh_fvsbc.outputs.REFERENCE_VERIFIED: frozenset[str] = frozenset({'FVS_ATRTList_Metric', 'FVS_CanProfile', 'FVS_Carbon_Metric', 'FVS_Compute', 'FVS_Consumption_Metric', 'FVS_CutList_Metric', 'FVS_DM_Spp_Sum_Metric', 'FVS_DM_Stnd_Sum_Metric', 'FVS_DM_Sz_Sum_Metric', 'FVS_DM_TreeList_Metric', 'FVS_Down_Wood_Cov_Metric', 'FVS_Down_Wood_Vol_Metric', 'FVS_Fuels_Metric', 'FVS_Hrv_Carbon', 'FVS_InvReference', 'FVS_Mortality_Metric', 'FVS_PotFire_Cond_Metric', 'FVS_PotFire_Metric', 'FVS_RD_Det_Metric', 'FVS_RD_Sum_Metric', 'FVS_Regen_Tally_Metric', 'FVS_SnagDet_Metric', 'FVS_SnagSum_Metric', 'FVS_Stats_Species_Metric', 'FVS_Stats_Stand', 'FVS_StrClass_Metric', 'FVS_Summary_Metric', 'FVS_TreeList_Metric'})

Every exposed table checked against some independent source, either the official dump or a standalone FVSbc. A caller showing a number from a table outside this set must not present it as verified.

fresh_fvsbc.outputs.STANDALONE_VERIFIED: tuple[str, ...] = ('FVS_ATRTList_Metric', 'FVS_CanProfile', 'FVS_Carbon_Metric', 'FVS_Compute', 'FVS_Consumption_Metric', 'FVS_CutList_Metric', 'FVS_DM_Spp_Sum_Metric', 'FVS_DM_Stnd_Sum_Metric', 'FVS_DM_Sz_Sum_Metric', 'FVS_DM_TreeList_Metric', 'FVS_Down_Wood_Cov_Metric', 'FVS_Down_Wood_Vol_Metric', 'FVS_Fuels_Metric', 'FVS_Hrv_Carbon', 'FVS_InvReference', 'FVS_Mortality_Metric', 'FVS_PotFire_Cond_Metric', 'FVS_PotFire_Metric', 'FVS_RD_Det_Metric', 'FVS_RD_Sum_Metric', 'FVS_Regen_Tally_Metric', 'FVS_SnagDet_Metric', 'FVS_SnagSum_Metric', 'FVS_Stats_Species_Metric', 'FVS_Stats_Stand', 'FVS_StrClass_Metric', 'FVS_Summary_Metric', 'FVS_TreeList_Metric')

The exposed tables compared against a standalone ``FVSbc`` built from the same source, and found to agree value for value. That is a second independent source, and a broader one than the official dump: it covers every table a run of either build writes, not only the four the dump carries. scripts/oracle_compare.py produces the comparison and notes/reference/oracle-coverage.md records the verdict for each table.

FVS_Regen_SitePrep is deliberately absent: its site-preparation allocation differs, for a reason that is not this package’s. FVS_Error is absent because FVS writes it only when it logs something, so the two builds cannot be made to agree on its presence.

exception fresh_fvsbc.outputs.MissingOutputError[source]

Bases: OutputError

A run produced no database, which contract rule 1 reports as a failure.

exception fresh_fvsbc.outputs.OutputError[source]

Bases: Exception

A problem with a run’s output database.

exception fresh_fvsbc.outputs.RunFailedError[source]

Bases: OutputError

A run whose output holds one or more messages of severity ERROR.

fresh_fvsbc.outputs.Severity

The severity FVS writes into a message, or UNKNOWN for a message that names none.

alias of Literal[‘ERROR’, ‘WARNING’, ‘UNKNOWN’]

exception fresh_fvsbc.outputs.UnknownTableError[source]

Bases: OutputError

A table this reader does not expose, or one this database does not hold.

fresh_fvsbc.outputs.check_run(db_path: str | Path) → DataFrame[source]

Report a run as failed when its output holds an error, and return its warnings.

The contract says a run whose FVS_Error is non-empty is failed. In practice that table also holds warnings, so this function fails a run on the rows of severity ERROR and hands the warnings back rather than discarding them.

Parameters:

db_path (str or Path) – The database the run wrote.

Returns:

pandas.DataFrame – The warnings, which do not make a run failed.

Raises:

RunFailedError – If any message is of severity ERROR. The message names each one, because a failure that does not say what failed is not a report.

fresh_fvsbc.outputs.errors_in(table: DataFrame) → DataFrame[source]

Return the rows of severity ERROR.

Parameters:

table (pandas.DataFrame) – A table from FVS_Error.

Returns:

pandas.DataFrame – The errors, in the order they were read.

fresh_fvsbc.outputs.read_cases(db_path: str | Path) → DataFrame[source]

Read FVS_Cases, the table that identifies the run.

Parameters:

db_path (str or Path) – The database the run wrote.

Returns:

pandas.DataFrame – One row per case.

fresh_fvsbc.outputs.read_errors(db_path: str | Path) → DataFrame[source]

Read FVS_Error, which holds messages of more than one severity.

FVS writes the table only when it has something to log. A run that raises no warning and no error has no such table, and that is a clean run rather than a failure to read: this returns an empty frame with the table’s columns instead of raising. An earlier revision treated the absent table as a failure, on the stated premise that the extension writes it on every run; a 999-tree single-plot case disproved that premise, and it reported a good run as failed.

Parameters:

db_path (str or Path) – The database the run wrote.

Returns:

pandas.DataFrame – Every row, errors and warnings together. Use errors_in() and warnings_in() to separate them. Empty when the run logged nothing.

fresh_fvsbc.outputs.read_table(db_path: str | Path, table: str) → DataFrame[source]

Read one table from a run’s output database.

Parameters:
  • db_path (str or Path) – The database the run wrote.

  • table (str) – The table to read. It must be one this reader exposes.

Returns:

pandas.DataFrame – One row per record, with the database’s own column names.

Raises:
  • UnknownTableError – If the table is not exposed, or the database does not hold it.

  • MissingOutputError – If the database is not there. Contract rule 1: a run that produced no database is a reported failure, not a silent fall back.

fresh_fvsbc.outputs.severity_of(message: str) → Literal['ERROR', 'WARNING', 'UNKNOWN'][source]

Return the severity FVS wrote into a message.

FVS writes the message code and then the severity, as in FVS16 ERROR:  .... The first severity in the message is the one that counts, so a severity word appearing later, inside the text, does not change the answer.

Parameters:

message (str) – One message from FVS_Error.

Returns:

Severity – "ERROR", "WARNING", or "UNKNOWN" when the message names neither.

fresh_fvsbc.outputs.warnings_in(table: DataFrame) → DataFrame[source]

Return the rows of severity WARNING.

Parameters:

table (pandas.DataFrame) – A table from FVS_Error.

Returns:

pandas.DataFrame – The warnings, in the order they were read.

The provenance record.

docs/reference/provenance.rst is the contract. A record names the two pinned inputs, the variant and the build’s variant list, a hash of the keyword set and one of the tree list, and the case id and run datetime from FVS_Cases.

The build’s variant list is exposed at run time

The contract listed as unverified whether PyFVS exposes the built variant list. It does: pyfvs.list_variants() returns a mapping of variant name to the module it would import and a status, and in this project’s build it returns {'BC': {'lib': 'pyfvs.fvsbc', 'status': 'OK'}}.

The status is the useful part, because the contract records the list so that a mistaken build is visible, and a variant whose status is not OK is exactly that. built_variants() turns the report into the record’s list and refuses a build that reported anything but OK, rather than recording a failure as a fact.

Why the hashes are not file hashes

The contract is explicit that the two hashes are of a canonical serialization rather than of a file, because a file hash moves with a trailing newline or a column width and would then report a difference that is not one. The tree-list hash is therefore taken over the records’ values, so rewriting the same trees in a different width gives the same hash. The keyword hash is fresh_fvsbc.keywords.keyword_set_hash(), which is taken over the text a set renders to; that text is fully determined by the set, with no incidental whitespace, so it is canonical in the same sense.

fresh_fvsbc.provenance.FVS_SOURCE_COMMIT: str = 'ef5fa847'

The pinned FVS source submodule commit, from ADR 0004, read back from the submodule.

fresh_fvsbc.provenance.PYFVS_COMMIT: str = 'e95172b'

The pinned PyFVS fork commit, from ADR 0004, read back from the clone rather than quoted.

exception fresh_fvsbc.provenance.BuildError[source]

Bases: ProvenanceError

A build whose reported variant list holds a variant that did not import.

class fresh_fvsbc.provenance.Provenance(variant: str, keyword_set_hash: str, treelist_hash: str, case_id: str, run_datetime: str, bec: str = '', qmd: float | None = None, qmd_min: float | None = None, ba: float | None = None, ba_min: float | None = None, tsa: str | None = None, calibration_id: str = '', fresh_fvsbc_version: str = '0.21.4', pyfvs_commit: str = 'e95172b', fvs_source_commit: str = 'ef5fa847', variant_list: tuple[str, ...] = (), random_seed: int | None = None)[source]

Bases: object

What a run records about itself, per the contract.

Parameters:
  • variant (str) – The variant that ran, which for this project is bc.

  • keyword_set_hash (str) – The hash of the keyword set, from fresh_fvsbc.keywords.

  • treelist_hash (str) – The hash of the tree list, from treelist_hash().

  • case_id (str) – The CaseID from FVS_Cases.

  • run_datetime (str) – The RunDateTime from FVS_Cases.

  • bec (str, optional) – The validated BEC the run used, such as CAR IDFdk3/01. Empty only when a record is built without a run, as some contract tests do.

  • qmd (float, optional) – The expansion-weighted initial QMD, in centimetres.

  • qmd_min (float, optional) – The bundle’s minimum QMD, when it declares one.

  • ba (float, optional) – The expansion-weighted initial basal area, in square metres per hectare.

  • ba_min (float, optional) – The bundle’s minimum BA, when it declares one.

  • tsa (str, optional) – The TSA name the run was checked against, when one was supplied.

  • calibration_id (str, optional) – The id of the calibration bundle the run used, when it used one.

  • fresh_fvsbc_version (str, optional) – The version of this package that ran. Defaults to the installed one.

  • pyfvs_commit (str, optional) – The pinned PyFVS fork commit.

  • fvs_source_commit (str, optional) – The pinned FVS source submodule commit.

  • variant_list (tuple of str, optional) – The build’s variant list, from built_variants().

  • random_seed (int, optional) – The seed, when the run used one.

as_record() → dict[str, object][source]

Return the record as a JSON-ready mapping, in the contract’s field order.

Returns:

dict – One key per field the contract lists. It carries no credential and no path, which contract rule 4 requires of a record that may be cited in a tracked file.

to_json() → str[source]

Return the record as JSON, with a trailing newline.

Returns:

str – The record, formatted for a reader rather than for a machine.

write(path: str | Path) → Path[source]

Write the record beside a run’s outputs, as JSON.

Parameters:

path (str or Path) – Where to write it. The contract puts it beside the outputs.

Returns:

Path – The path written.

exception fresh_fvsbc.provenance.ProvenanceError[source]

Bases: Exception

A run whose provenance cannot be recorded, which the contract calls a failure.

fresh_fvsbc.provenance.built_variants(report: Mapping[str, Mapping[str, str]]) → tuple[str, ...][source]

Turn PyFVS’s variant report into the record’s variant list.

Parameters:

report (Mapping) – What pyfvs.list_variants() returns: each variant name mapped to its module and its status.

Returns:

tuple of str – The variant names, sorted, so two records of the same build read alike.

Raises:

BuildError – If any variant’s status is not OK. The contract records this list so that a mistaken build is visible, and a variant that did not import is the case it is there to catch.

fresh_fvsbc.provenance.from_cases(row: Mapping[str, object]) → tuple[str, str][source]

Return the case id and run datetime that a FVS_Cases row carries.

Parameters:

row (Mapping) – One row from FVS_Cases, as fresh_fvsbc.outputs.read_cases() returns it.

Returns:

tuple of str – The CaseID and the RunDateTime.

Raises:

ProvenanceError – If the row carries neither. The contract makes provenance mandatory rather than optional, so a run that cannot supply these has not recorded its provenance.

fresh_fvsbc.provenance.treelist_hash(records: Sequence[Mapping[str, object]]) → str[source]

Return the hash of a tree list’s values.

The hash is taken over the values rather than over the written text, so the same trees hash the same way at a different column width. That is what the contract asks for, and it is what keeps a change of record format from reading as a change of input.

Parameters:

records (Sequence of Mapping) – The tree list, in order, because order is part of what a run was given. A sequence of fresh_fvsbc.treelist.TreeListFields is what a caller usually has.

Returns:

str – "sha256:" followed by the hexadecimal digest.

The run orchestration.

This is where the five contracts meet. A run writes a keyword file, writes the tree list beside it, drives BC through PyFVS’s step API, reads the outputs back, and records its provenance. The reference pages govern each part: Tree-list input contract the records, Keyword-set contract the set, Output contract the read-back, Provenance contract the record, and nemora ingestion contract the adapter that feeds it.

Three facts about the run are not choices, and each of them is a way to get a run quietly wrong

  • The tree-data file is named after the keyword file, in lowercase. PyFVS computes its treelist path as keywords_path.with_suffix('.tre') and refuses to start when that file is absent, so a list written as .TRE passes FVS and fails PyFVS.

  • The output database has a fixed name. dbsqlite/dbsblkd.f has DATA DSNOUT/"FVSOut.db", so the database lands in the working directory under that name whatever the keyword file is called. Two runs in one directory overwrite each other’s database.

  • The tree-record format must be the variant’s. PyFVS injects its own TREEFMT when it is given a keyword set, using a default format rather than BC’s. A run here passes a keyword file, so nothing is injected and BC’s format is the only one a run can get.

Because the database’s name is fixed, and because FVS keeps its state in the process, a run drives the projection in a child process (ADR 0010). The child owns the working directory and the Fortran state; this process shares nothing between runs, so two runs in one process are independent and a run is thread-safe. The child writes the summary to summary.csv and the build’s variant list to variants.json, and the parent reads them. The summary is still FVS’s own frame rather than a re-derivation, and it is inspectable on disk.

What the orchestration writes, and what it refuses

The caller writes the stand: STDIDENT, STDINFO, DESIGN, NUMCYCLE, SITECODE, and whatever else the case needs. The orchestration writes the tree handoff, which is TREEFMT with BC’s format and TREEDATA with a blank parameter, placed before the first PROCESS or STOP so that FVS reads the trees before it starts the projection. A set that already carries either keyword is refused rather than quietly overridden, so nothing the caller wrote is replaced.

That insertion is a reading of the keyword contract’s rule 4, which allows one exception, “the tree-record format, which it must control”. Without TREEDATA FVS never opens the tree file, so the format and the instruction to use it are one handoff. The reading is recorded on Keyword-set contract rather than left to be inferred from this module.

The keyword hash covers what FVS read

Provenance is taken after the handoff is inserted, because a hash of the set the caller wrote would identify a set no run ever saw.

fresh_fvsbc.run.DATABASE_NAME: str = 'FVSOut.db'

The database’s name, which dbsblkd.f fixes and no caller can change.

fresh_fvsbc.run.KEYWORD_FILE_NAME: str = 'run.key'

The keyword file’s name inside a run’s directory. FVS and PyFVS both derive other names from it.

fresh_fvsbc.run.PROVENANCE_FILE_NAME: str = 'provenance.json'

The provenance record’s name, which the provenance contract puts beside the outputs.

fresh_fvsbc.run.SUMMARY_FILE_NAME: str = 'summary.csv'

the summary FVS produced, and the build’s variant list.

Type:

What the child writes for the parent

fresh_fvsbc.run.TREEFMT_LINES: tuple[str, str] = ('(I4,T1,I7,F6.0,I1,A3,F5.1,F4.1,2F5.1,F5.1,I1,3(I2,I2),2I1,I2,2I3,', '2I1,F3.0)')

BC’s format, split into the two lines a TREEFMT keyword carries.

exception fresh_fvsbc.run.ExtensionUnavailableError[source]

Bases: RunError

PyFVS is not importable, or the variant it was asked for did not load.

exception fresh_fvsbc.run.KeywordConflictError[source]

Bases: RunError

A keyword set that carries a keyword the orchestration writes itself.

exception fresh_fvsbc.run.RunError[source]

Bases: Exception

A run that could not be set up, driven, or read.

class fresh_fvsbc.run.RunResult(provenance: Provenance, cases: DataFrame, warnings: DataFrame, summary: DataFrame, directory: Path, keyword_file: Path, database: Path)[source]

Bases: object

What a run returns: its tables, its provenance, and where it left them.

Parameters:
  • provenance (Provenance) – The record, as the provenance contract defines it.

  • cases (pandas.DataFrame) – FVS_Cases, which identifies the run.

  • warnings (pandas.DataFrame) – The rows of FVS_Error whose severity is WARNING. A row of severity ERROR raises instead, so a result never carries one.

  • summary (pandas.DataFrame) – The summary the run produced in memory, which the output contract names as the route to that table through this API.

  • directory (Path) – The run’s directory, which holds the keyword file, the tree list, the database, the reports, the provenance record, and the two files the child process wrote: summary.csv and variants.json.

  • keyword_file (Path) – The keyword file written.

  • database (Path) – The database read.

fresh_fvsbc.run.database_path(directory: str | Path) → Path[source]

Return the path of the database a run in directory writes.

Parameters:

directory (str or Path) – The run’s directory.

Returns:

Path – directory/FVSOut.db, the default DSNOUT that dbsblkd.f initialises. A keyword set carrying a DATABASE block with a DSNOUT option names another, and this function does not read it; the route this module runs carries no such block.

fresh_fvsbc.run.run_case(records: Sequence[TreeListFields], keywords: Sequence[Keyword], *, directory: str | Path, variant: str = 'bc', key_name: str = 'run.key', calibration: CalibrationBundle | None = None, tsa: str | None = None) → RunResult[source]

Run one case: write the inputs, drive BC, read the outputs, and record the provenance.

The caller describes the stand and the run in keywords and supplies the trees in records. The orchestration writes the tree handoff, the keyword file, and the tree list, drives one projection in directory, and reads the database back. A run that reports an error row raises rather than returning a result that holds one.

Parameters:
  • records (Sequence of TreeListFields) – The trees, in the order to write them. fresh_fvsbc.nemora.to_tree_list() builds these from a synthetic population.

  • keywords (Sequence of Keyword) – The keyword set, in the order FVS will read it. It must not carry TREEFMT or TREEDATA.

  • directory (str or Path) – Where the run writes. It is created if it does not exist, and the run owns it: the database’s name is fixed, so two runs in one directory would overwrite each other.

  • variant (str, optional) – The variant to run. Defaults to bc, which is the only variant this project wraps.

  • key_name (str, optional) – The keyword file’s name inside directory. The tree file takes the same base name.

  • calibration (CalibrationBundle, optional) – A bundle the run uses. The caller renders the bundle into keywords; the run checks the domain and refuses a set that does not carry the bundle’s keywords. The bundle id is recorded in provenance.

  • tsa (str, optional) – The TSA name, when a bundle domain declares one. It is recorded in provenance and checked against the bundle.

Returns:

RunResult – The tables, the provenance, and the paths.

Raises:
  • BecCodeError – If the set carries no STDINFO, its first two fields are not a BEC the variant resolves, or the run echoes a different BEC because the installed build does not carry the requested code.

  • BundleError – If the calibration bundle’s domain does not cover the run, or the set does not carry the keywords the bundle renders.

  • KeywordConflictError – If the keyword set carries a keyword the orchestration writes.

  • UnknownKeywordError – If the set carries a keyword FVS’s table does not hold.

  • ExtensionUnavailableError – If PyFVS is not importable, or the variant did not load.

  • RunFailedError – If the run holds a row of severity ERROR.

  • MissingOutputError – If the run wrote no database.

  • RunError – If the projection ends badly, or the run recorded no case.

fresh_fvsbc.run.run_population(population: Sequence[Mapping[str, object]], *, directory: str | Path, species: str | None = None, tree_age: float | None = None, keywords: Sequence[Keyword] = (), variant: str = 'bc', plot_fields: PlotFields | None = None, key_name: str = 'run.key') → RunResult[source]

Run one case built from a synthetic stem population.

This is fresh_fvsbc.nemora.to_tree_list() followed by run_case(), and it exists because that pair is the project’s own path from a population to a run. It adds no behaviour of its own, and the adapter’s refusals are its refusals.

Parameters:
  • population (Sequence of Mapping) – The stems, shaped like nemora’s output.

  • directory (str or Path) – Where the run writes.

  • species (str, optional) – The species code. Required, because nemora emits none.

  • tree_age (float, optional) – The age at breast height, in years. Required, for the same reason.

  • keywords (Sequence of Keyword, optional) – The keyword set. The orchestration writes the tree handoff.

  • variant (str, optional) – The variant to run. Defaults to bc.

  • plot_fields (PlotFields, optional) – The plot-level fields, which belong to a run.

  • key_name (str, optional) – The keyword file’s name inside directory.

Returns:

RunResult – The tables, the provenance, and the paths.

Raises:
  • MissingAssignmentError – If species or tree_age is not supplied.

  • PopulationError – If a record is missing a field the adapter reads, or carries a non-numeric one.

  • CrownRatioError – If a crown ratio cannot be represented by the tree-list field.

  • SpeciesError – If the species code resolves to no BC species, or to more than one.

  • RunError – If the run cannot be set up, driven, or read.

fresh_fvsbc.run.tree_file_name(key_path: str | Path) → Path[source]

Return the tree-data path PyFVS expects beside a keyword file.

Parameters:

key_path (str or Path) – The keyword file’s path.

Returns:

Path – The same path with a lowercase .tre suffix. FVS would also accept .TRE, but PyFVS checks for .tre and refuses to start without it, so the lowercase name is the only one that works through this route.

fresh_fvsbc.run.treelist_format_lines(format_string: str = '(I4,T1,I7,F6.0,I1,A3,F5.1,F4.1,2F5.1,F5.1,I1,3(I2,I2),2I1,I2,2I3,2I1,F3.0)') → tuple[str, str][source]

Split a tree-record format into the two lines FVS reads it from.

TREEFMT takes its format from the two lines that follow it, and intree.f joins them without padding, so the split may fall inside a FORTRAN group. It falls where the variant’s own keyword files put it.

Parameters:

format_string (str, optional) – The format to split. Defaults to BC’s.

Returns:

tuple of str – The first and second line.

Raises:

RunError – If the format does not carry the split point. A format that cannot be split cannot be handed over, and guessing a split would produce a format FVS misreads.

fresh_fvsbc.run.with_tree_handoff(keywords: Sequence[Keyword]) → tuple[Keyword, ...][source]

Return a keyword set with the tree handoff inserted, in the caller’s order.

The handoff goes before the first PROCESS or STOP, because FVS reads the trees when it reaches TREEDATA and starts the projection at PROCESS. A set that carries neither ends with the handoff.

Parameters:

keywords (Sequence of Keyword) – The caller’s set, which describes the stand and the run.

Returns:

tuple of Keyword – The caller’s keywords in order, with TREEFMT and TREEDATA inserted.

Raises:

KeywordConflictError – If the set already carries either keyword. Overriding it would replace something the caller wrote, which the keyword contract forbids.

The command-line entry point.

This is the orchestration with a shell in front of it. It parses a population file and a keyword set, calls fresh_fvsbc.run.run_population(), and turns the result into an exit code. It adds no modelling, no defaults for the stand, and no keyword the caller did not write.

The exit codes separate the two ways a caller can be unhappy, because they call for different actions:

  • 0 the run completed and wrote its outputs;

  • 1 the run was attempted and failed, which is worth reporting;

  • 2 the input was wrong before any run, which is worth fixing.

argparse already exits 2 for a usage error, so the third code is the convention rather than an invention.

The command’s one limit is that a parameter cannot contain a comma, because the comma separates the fields of a keyword. The API has no such limit, and a caller who needs one uses the API.

fresh_fvsbc.cli.EXIT_BAD_INPUT: int = 2

The input was wrong before any run.

fresh_fvsbc.cli.EXIT_FAILED_RUN: int = 1

The run was attempted and failed.

fresh_fvsbc.cli.EXIT_OK: int = 0

The run completed and wrote its outputs.

exception fresh_fvsbc.cli.CommandInputError[source]

Bases: ValueError

Input the command cannot use, which is refused before a run is attempted.

It is raised while the entries are parsed, inside the command’s own try, so the message reaches the caller rather than being replaced by argparse’s generic one for a failed type=.

fresh_fvsbc.cli.main(argv: Sequence[str] | None = None) → int[source]

Run one case from the command line.

Parameters:

argv (sequence of str or None, optional) – Arguments to parse. None reads sys.argv.

Returns:

int – 0 for a completed run, 1 for a run that failed, and 2 for input the command cannot use.

Notes

fresh-fvsbc web ... is dispatched to fresh_fvsbc.web.app.main() before the run parser sees the arguments, so the optional service never changes how a run is invoked.