Keyword-set contract
How fresh-fvsbc represents, validates, and hands a keyword set to FVS-BC.
Read at: forest-modeling/ForestVegetationSimulator, branch pyfvs-features, commit ef5fa84.
The vocabulary
FVS matches keywords against a fixed table. The base table is TABLE in base/keywds.f, declared in blocks of 20 and 150 slots long. It is not a per-variant list: the variant hooks into it.
The named entries, in slot order, are:
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 <blank> THINCC ECON DATABASE <blank> DEFECT <blank>
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 <blank> <blank> <blank>
Two caveats, both verified:
One entry is misspelled upstream:
xSALVAGE, with a leadingx. A caller that writesSALVAGEis writing a keyword the table does not hold.The table has blank slots. A blank slot is not a keyword.
Matching is FVS’s own.
base/fndkey.fcompares the caller’s word with the table using FORTRAN’s.EQ.on character strings, so the comparison is exact and case sensitive. A validator that accepteddatabasewould be accepting something FVS does not.
How a keyword and its parameters are laid out
A keyword is not a name followed by tokens. base/keyrdr.f reads the keyword from the first eight
columns of a record and then reads each parameter from a ten-column field of that same record:
KEYWRD=RECORD(1:8)
J=1
DO 25 I=1,NF
J=J+10
KARD(I)=RECORD(J:J+9)
So the first field is columns 11 to 20, the second 21 to 30, and so on, and a parameter wider than
ten columns cannot be carried. A blank field is a field, which is why FVS’s own files write
TIMEINT with an empty first field before the value.
Some keywords are the exception, because they read records of their own rather than fields:
STDIDENTtakes the record after it as the stand id and title (canada/bc/initre.f).TREEFMTtakes the two records after it as the format (metric/base/intree.f).THINQFAtakes one supplemental record, whose first column names the units of its residual-density target:0basal area per acre,1trees per acre,2stand density index. It is written as a bare record with no terminator, immediately after the keyword, and the caller supplies it after a semicolon:--keyword THINQFA=1,0,24,0,1.4,2,100;1.The reason is in the source rather than the guide:
cuts.freadsPRMS(1..6)from the six fields after the year, which leavesQFATAR = INT(PRMS(7))for the supplemental record’s value. A set without the record is refused, not written, because FVS would read the next keyword as the units record and the run would be wrong without saying so.COMMENT,COMPUTEandDATABASEtake a block of text lines ended byEND. A caller supplies the lines and the writer supplies the terminator, because a block without one swallows the rest of the file. A ``DATABASE`` block must precede the stand setup: FVS refuses it later withFVS16 ERROR: KEYWORD ENTERED IS USED IN WRONG CONTEXT, and the official case puts it first. The validator refuses a set that puts it afterSTDIDENT,STDINFOorSTANDSQL.``FMIN`` is a block of *keywords*, not of lines, and it is how a caller asks for the snag, down-wood, fuel, potfire, carbon and consumption tables. Its body is written by the same writer as any other keyword, so a parameter lands in FVS’s ten-column field:
Keyword("FMIN", nested=( Keyword("SNAGCLAS", ("0.0", "10.0", "20.0", "40.0")), Keyword("SNAGOUT", ("1",)), Keyword("CARBREPT", ("2",)), ))
from the shell:
$ fresh-fvsbc population.json --directory run \ --block "FMIN|SNAGCLAS=0.0,10.0,20.0,40.0|SNAGOUT=1|CARBREPT=2"
The fields are not spaced by hand. A hand-spaced line is read as different numbers and FVS reports nothing:
SNAGCLAS 0.0 10.0 20.0 40.0reaches FVS as class 10.0, class 220.0, class 30.0and class 42537.5, because each value has to sit in its own ten-column field. An earlier revision took raw lines here and a caller could write exactly that; the block now takes keywords and the writer spaces them.The block must precede ``INVYEAR`` when it asks for ``FUELOUT`` or ``POTFIRE``. Both capture the inventory year when they are read, and
fmdout.fandfmpofl.ftreat any positive value as “suppress this report”. Read afterINVYEAR, they switch their own reports off and the fuels and potfire tables are silently not written — the run succeeds and the tables are simply absent. The validator warns, and the warning names the two keywords.The body is checked against
fmin.f’s own table — fifty-five keywords — and one written outside the block is refused, because FVS would ignore it.FUELMODLandDEFULMODread one supplemental record after their own, and the rest read one record. Each table is written only when theDATABASEblock names the matching option —SnagOuDB,SnagSuDB,DWDVLDB,DWDCVDB,FuelsOut,PotfirDB,CARBREDBorBURNREDB— and the option alone is never enough for carbon or consumption:CARBREDBneedsCARBREPT, andBURNREDBneeds an actual fire, whichSIMFIREschedules as activity 2506.The burn report has no working path.
FVS_BurnReport_Metricis written only insideIF (IFMBRB .LT. 0), andIFMBRBis set to9999at initialisation and to the inventory year byBURNREPT— never negative unlessINVYEARis given an invalid negative year, which shifts the whole simulation. It is not exposed.``ATRTLIST`` and ``STRCLASS`` are requests, and ``ATRTLIST`` needs a treatment. The attribute list is written after a cutting operation — its writer returns unless it is called from cuts.f — so a run with no removal writes no rows even though the table exists. STRCLASS asks for the structural stage of each stratum.
Some tables need a request as well as an option.
FVS_TreeList_MetricneedsTREELIDBin theDATABASEblock and aTREELISTkeyword;FVS_CutList_MetricneedsCUTLIDBandCUTLIST, and an actual removal;FVS_Stats_Species_MetricneedsINVSTATSandSTATS. The option alone is accepted and writes nothing, which is the same trapCARBREDBsets.The mortality table needs ``MORTREPT``, which sets ``IFMMRB`` to -1;
MORTREDBalone is accepted and writes nothing. That is the opposite of the burn report, whoseBURNREPTsetsIFMBRBto the inventory year, which the gate rejects — two options that look identical and behave oppositely.A mistletoe rating enters through the damage columns: damage codes 30 to 34 with the severity as the Hawksworth rating, per
misdam.f. The adapter writes zero there, so an infected stand needs a caller who supplies the damage fields.``DESIGN``’s fourth field is the inventory point count.
FVS_Stats_Standis the distribution among sample points and is written only when that count is above one; a run that leaves it at 1 writes the species statistics and a message saying the distribution cannot be computed.``ESTAB``, ``MISTOE``, ``RDIN`` and ``FMIN`` are blocks of *keywords*, not of lines. Each body is written by the same writer as any other keyword, so a parameter lands in FVS’s ten-column field, and each is checked against its own reader’s table —
esin.f,misin.f,rdinie.fandfmin.f:Keyword("RDIN", nested=(Keyword("RRINIT"), Keyword("RRDOUT"))) Keyword("ESTAB", nested=(Keyword("PLANT", ("0", "FD", "3680", "90")),)) Keyword("MISTOE", nested=(Keyword("MISTPINF", ("0", "3", "0.5")), Keyword("MISTPRT", ("1",))))
A keyword from one of those tables written outside its block is refused, because FVS would ignore it. A name that is also a top-level keyword —
ESTAB,COMMENT— stays valid at the top level.A hand-spaced line here is read as different numbers and FVS reports nothing, which is why these are keywords now:
MISTPINF 0 1 0.5reachesMISINas speciesPWand proportion0.50, neither of which was written.ESTAB’sNATURALandPLANTlines still need stems per acre:esin.frefuses the line — and the whole set with it — when that field is zero.RDINneedsRRINIT(orRRTREIN) or it refuses the whole set at itsENDwith “NEITHER TREELIST OR MANUAL INITIALIZATION SPECIFIED”, andRRDOUTadds the detail table. The dwarf-mistletoe tables need an infection as well as the block: the rating enters through the damage columns, code 30 with the Hawksworth rating as the severity. ``FVS_CalibStats`` is not exposed: the table is created but carries nothing unless the run is calibrated, and this package does not calibrate.IFtakes a block of keywords rather than lines: the condition record,THEN, the body, andENDIF, with an optionalELSEbranch. The official case’s form is the one the run test checks. ``ALSOTRY`` is not modelled, because no fixture here uses it.
STOP is a keyword that the table does not hold. base/keyrdr.f compares each record’s first
eight characters with STOP before it consults the table, and a match ends the file with code 3, so
STOP is accepted and every real key file ends with it. The first version of the validator refused
it, and refused every real key file with it; the run orchestration found that the first time it wrote
one.
BC-specific input
The BC variant adds no keyword to base/keywds.f. It reaches site and BEC input through the base keywords, in canada/bc/initre.f:
SITECODEis handled at option 94 of initre.f, from line 3347. The comment states that the site species comes from the firstSITECODEcard, and it names field 3 as the species field.SETSITEis in the table at slot 139.
BEC values arrive in the first two fields of ``STDINFO``. Option 14 of
canada/bc/initre.f concatenates those two fields and calls HABTYP; option 138,
SETSITE, does the same when it is given a habitat type. canada/bc/habtyp.f
searches the concatenation for a region token, a zone token, a two-letter subzone
with an optional variant, a slash, and a site series. If any part is missing it
calls its error path and leaves the BEC at the documented default,
ICHmw2/01.
The working form is:
STDINFO CAR IDFdk3/01 60 200 35 16
The first two fields carry the code: the region, then the zone, subzone, and site
series. The series is mandatory. CAR IDFdk3 alone does not resolve, because
there is no slash.
canada/bc/BCPLOT.F77 declares the structured value that BECSET resolves:
TYPE BEC_DESCRIPTION
SEQUENCE
CHARACTER (LEN=3) :: Region !KAM - longest strings
CHARACTER (LEN=4) :: Zone !ESSF - are shown here
CHARACTER (LEN=3) :: SubZone !mw2
CHARACTER (LEN=5) :: Series !01-YC
CHARACTER (LEN=7) :: FullName !ICHmw2
CHARACTER (LEN=15) :: PrettyName !IDFmw1/01-YC
END TYPE
What the package does. STDINFO is validated before a run starts.
fresh_fvsbc.bec.BecCode parses the first two fields and refuses a code that
would not resolve, naming the accepted grammar, the supported zones, and the silent
default. It refuses STDINFO=712,CHS131,... for the same reason: that string is
not a BEC, and FVS would run ICHmw2/01. The supported zones are ESSF,
ICH, IDF, MS, PP, SBS, and SBPS; they are also the zones
canada/bc/sitset.f assigns default maximum basal area for. CWH, CDF,
BWBS, CMA, and MH are not represented by the variant, and a code in one
of them is refused by name rather than defaulted.
BECINFO is a stale comment. It appears nowhere in the keyword table and nowhere
in the code except two comments in canada/bc/becset.f. BECSET is called from
canada/bc/sitset.f, not from the keyword reader. SITECODE supplies the site
species; it does not supply the BEC.
Contract rules
fresh-fvsbc represents a keyword set as an ordered sequence of keywords, each with its parameters. Order is significant, because FVS processes the file in order.
fresh-fvsbc validates each keyword against the table above, and it rejects an unknown keyword by name rather than passing it through. A pass-through would let a typo reach Fortran, which reports it poorly.
The validator warns on
xSALVAGE. The defect is FVS’s rather than the caller’s, so the warning’s job is to say which spelling the table holds. A caller who writesSALVAGEwithout thexis writing a keyword the table does not hold, so that name is refused, and the refusal namesxSALVAGE.fresh-fvsbc does not silently add a keyword the caller did not write, with one exception: the tree-list handoff, which it must control. See Tree-list input contract and the hazard below. The handoff is
TREEFMTwith BC’s format andTREEDATAwith a blank parameter, which are written by the run and placed before the firstPROCESSorSTOP. The rule’s exception names the tree-record format;TREEDATAis part of the same handoff because without it FVS never opens the tree file, so the format and the instruction to use it cannot be separated. A set that already carries either keyword is refused rather than overridden. This reading was recorded when the run orchestration was written, because a rule read generously should say so.A keyword set carries a hash, and the hash is part of the run’s provenance. See Provenance contract. The hash is taken over the set’s rendered text — the bytes a run was given — rather than over a structure, so that it identifies exactly what FVS read, and it is written with its algorithm named, as
sha256:followed by the digest. Order is part of the identity, because FVS processes a set in order.A BC run carries a
STDINFOwith a BEC in its first two fields, and the package validates it before the run starts. An invalid code, or a set with noSTDINFO, is refused rather than left on FVS’s silentICHmw2/01default. The accepted grammar and the supported zones are infresh_fvsbc.bec.BecCode.
The open hazard
PyFVS injects a TREEFMT keyword when the caller’s set lacks one, using its own default format rather
than the BC variant’s. fresh-fvsbc must not inherit that behaviour silently. Either it writes the BC
TREEFMT itself, or it writes the format the build actually expects and records why.
Resolved in P2.2. The injection is conditional, and avoiding it needs no change to PyFVS, so contract
rule 4 stands: fresh-fvsbc passes BC’s format through FVS(treelist_format=...) whenever it supplies trees
as data. The injection’s condition, and how the two formats differ, are in Tree-list input contract and are not
repeated here.