Woodstock Format: What ws3 Reads
ws3 imports an essential subset of the Woodstock model input data format. The
boundary of that subset used to be invisible: keywords outside it are not
rejected, they are ignored. A dataset can declare an OPTIMIZE section, or
use *ACTIONSERIES, and import without complaint — producing a model that is
quietly not the model that was written. No error, wrong answer.
This page is generated from the contract shipped with the package
(ws3.woodstock), so it cannot drift from what the importers actually
do.
The contract catalogues 198 keywords across 17 sections. ws3 reads 25 of those keywords. Of the sections, 7 are implemented, 1 is partial, 4 are stubs that import nothing, and 5 have no importer at all.
The format is open ended by design
A Woodstock model instance declares its own themes, in its own order, with its own stratification variable codes within each theme. There is no fixed allocation of meaning to theme positions, and no theme count that is standard.
The LANDSCAPE section is the authoritative source for all of it: the
cardinality and order of the themes in the theme vector, the values each theme
is allowed to take, the *AGGREGATE theme values, and the constants. The
number of themes declared there determines the length of every development-type
key in the model.
Datasets that look structurally alike usually do so because one author reused their own conventions, not because the format imposes anything. Do not infer theme semantics from a sample of models.
*THEME declaration lines carry a descriptive name after the keyword, and
that description is the only statement in a dataset of what a theme position
means. ws3.forest.ForestModel.import_landscape_section() preserves it
as __description__ on each entry of ForestModel._themes; where a dataset
omits it, ws3 has nothing to go on and says so rather than inventing a meaning.
The generated theme names (theme0, theme1, …) are positional labels
and carry no meaning of their own.
Sections
stub is called out separately from no importer because the failure mode
differs in a way that matters. A stub method exists and returns successfully, so
a caller has every reason to believe the section was read. It was not.
Section |
File |
Importer |
Status |
|---|---|---|---|
Areas |
|
|
implemented |
Constants |
|
|
implemented |
Landscape |
|
|
implemented |
Outputs |
|
|
implemented |
Schedule |
|
|
implemented |
Transitions |
|
|
implemented |
Yields |
|
|
implemented |
Actions |
|
|
partial |
Control |
|
|
stub — imports nothing |
Graphics |
|
|
stub — imports nothing |
Lifespan |
|
|
stub — imports nothing |
Optimize |
|
|
stub — imports nothing |
Allocation |
|
— |
no importer |
LpSchedule |
|
— |
no importer |
Queue |
|
— |
no importer |
Regimes |
|
— |
no importer |
Reports |
|
— |
no importer |
Keywords ws3 reads
Every other catalogued keyword is ignored on import.
Keyword |
Sections it appears in |
|---|---|
|
Areas |
|
Actions |
|
Landscape, Actions, Allocation |
|
Transitions |
|
Constants. Outputs. Optimize. Yields |
|
— |
|
Actions, Regimes |
|
Outputs |
|
Actions |
|
Transitions, Queue, Outputs |
|
— |
|
Landscape |
|
Yields |
|
Yields |
|
Yields |
|
Transitions, Outputs |
|
Transitions, Outputs |
|
Yields, Actions, Regimes, Transitions, Outputs, Queue |
|
Transitions |
|
Outputs, Reports |
|
Outputs |
|
Areas, Transitions, Regimes |
|
Transitions |
|
Yields, Graphics, Reports, Optimize |
|
Outputs, Transitions, Yields, FOREACH loops |
Deliberate divergences from Woodstock
These are recorded in the contract rather than treated as defects.
- Time unit: periods versus years
Woodstock measures stand age and action timing in periods. ws3 measures them in years. ForestModel.import_areas_section takes convert_periods_to_years and multiplies imported ages by the period length. Any keyword documented as taking a number of periods, _LOCK most visibly, is therefore not directly comparable between the two.
- Theme indexing: one-based versus zero-based
Woodstock counts themes from one and writes _THn accordingly. ws3 stores themes zero-indexed, so _THn refers to ws3 theme n-1.
Linting a dataset
ws3.woodstock.lint_dataset() reports what ws3 will not read from a
dataset. It reads the section files directly: nothing is imported, no model is
built, and nothing is modified. It is advisory, and it is not required in order
to import a model — but running it before you trust an import is cheap.
from ws3.woodstock import lint_dataset, format_findings
findings = lint_dataset('examples/data/woodstock_model_files_tsa24_clipped',
'tsa24_clipped')
print(format_findings(findings))
Run against the tsa24_clipped dataset shipped with the examples, that
reports:
error: .../tsa24_clipped.lif: the LIFESPAN section is present but not imported:
import_lifespan_section is a stub and imports nothing. Everything in this
file is ignored.
error: .../tsa24_clipped.opt: the OPTIMIZE section is present but not imported:
import_optimize_section is a stub and imports nothing. Everything in this
file is ignored.
error: .../tsa24_clipped.que: the QUEUE section is present but not imported:
ws3 has no importer for it. Everything in this file is ignored.
error: .../tsa24_clipped.rep: the REPORTS section is present but not imported:
ws3 has no importer for it. Everything in this file is ignored.
error: .../tsa24_clipped.run: the CONTROL section is present but not imported:
import_control_section is a stub and imports nothing. Everything in this
file is ignored.
5 section(s) not imported, 0 keyword(s) ignored.
Five files in that dataset are read by nobody. That is not a defect report — the model built from it has carried real work — but it is the difference between knowing that and assuming otherwise.
Findings are ordered by severity, then file, then line. Each
ws3.woodstock.Finding carries the severity, section, path, line and
keyword separately, so the report can be rendered any way you like:
errors = [f for f in findings if f.severity == 'error']
for f in errors:
print(f.section, f.path)
To check only part of a dataset, pass the section identifiers:
findings = lint_dataset(path, name, sections_to_check=['Actions', 'Yields'])
An empty result means ws3 imports everything present in that dataset.
API
The Woodstock input data format contract, and what ws3 does with it.
The Woodstock format is deliberately open ended. A model instance declares its own set of themes, in its own order, with its own stratification variable codes within each theme, and the LANDSCAPE section is the authoritative source for all of it. Much of ws3’s internal complexity follows from that flexibility.
ws3 implements an essential subset of the format – enough to have carried real
projects for years – but the boundary of that subset was previously undocumented
and unenforced. Keywords outside it are not rejected; they are ignored. A
dataset can declare an OPTIMIZE section, or use *ACTIONSERIES, and import
without complaint, producing a model that is quietly not the model that was
written. No error, wrong answer.
This module exists to make that boundary visible:
contract()loads the machine-readable keyword contract shipped as package data.lint_dataset()reports which parts of a dataset ws3 will not read.
Linting is advisory and reads nothing but the files. It never mutates a model, and it is not required in order to import one.
Two ws3 divergences from Woodstock are recorded in the contract rather than treated as defects:
Time unit. Woodstock measures stand age and action timing in periods; ws3
measures them in years. Any keyword documented as taking a number of periods –
_LOCK most visibly – is therefore not directly comparable between the two.
Theme indexing. Woodstock counts themes from one and writes _THn
accordingly; ws3 stores themes zero-indexed, so _THn is ws3 theme n-1.
- ws3.woodstock.CONTRACT_PATH = PosixPath('/home/runner/work/ws3/ws3/ws3/data/woodstock_format.yaml')
Location of the contract, shipped as package data.
- ws3.woodstock.DIVERGENCES: dict[str, str] = {'theme_indexing': 'Woodstock counts themes from one and writes _THn accordingly. ws3 stores themes zero-indexed, so _THn refers to ws3 theme n-1.', 'time_unit': 'Woodstock measures stand age and action timing in periods. ws3 measures them in years. ForestModel.import_areas_section takes convert_periods_to_years and multiplies imported ages by the period length. Any keyword documented as taking a number of periods, _LOCK most visibly, is therefore not directly comparable between the two.'}
Deliberate departures from Woodstock semantics, recorded so that they are not mistaken for defects.
- class ws3.woodstock.Finding(severity: str, section: str, message: str, path: str | None = None, line: int | None = None, keyword: str | None = None)[source]
Bases:
objectOne thing ws3 will not read from a dataset.
- Parameters:
severity –
errorwhen data is silently dropped,warningwhen a keyword is unrecognised,infofor advisory notes.section – Section identifier, e.g.
Yields.path – File the finding refers to, if any.
line – 1-based line number, if the finding is line-specific.
keyword – Keyword involved, if the finding is keyword-specific.
message – What ws3 will do, stated plainly.
- keyword: str | None = None
- line: int | None = None
- message: str
- path: str | None = None
- section: str
- severity: str
- ws3.woodstock.SECTION_SUPPORT: dict[str, tuple[str | None, str]] = {'Actions': ('import_actions_section', 'partial'), 'Allocation': (None, 'none'), 'Areas': ('import_areas_section', 'implemented'), 'Constants': ('import_constants_section', 'implemented'), 'Control': ('import_control_section', 'stub'), 'Graphics': ('import_graphics_section', 'stub'), 'Landscape': ('import_landscape_section', 'implemented'), 'Lifespan': ('import_lifespan_section', 'stub'), 'LpSchedule': (None, 'none'), 'Optimize': ('import_optimize_section', 'stub'), 'Outputs': ('import_outputs_section', 'implemented'), 'Queue': (None, 'none'), 'Regimes': (None, 'none'), 'Reports': (None, 'none'), 'Schedule': ('import_schedule_section', 'implemented'), 'Transitions': ('import_transitions_section', 'implemented'), 'Yields': ('import_yields_section', 'implemented')}
What ws3 does with each section: the importer, and whether it does anything.
stubis called out separately fromnonebecause the failure mode differs in a way that matters. A stub method exists and can be called successfully, so a caller has every reason to believe the section was read. It was not.
- ws3.woodstock.SUPPORTED_KEYWORDS: frozenset[str] = frozenset({'*A', '*ACTION', '*AGGREGATE', '*CASE', '*GROUP', '*LEVEL', '*OPERABLE', '*OUTPUT', '*PARTIAL', '*SOURCE', '*TARGET', '*THEME', '*Y', '*YC', '*YT', '@AGE', '@YLD', '_AGE', '_APPEND', '_AREA', '_INVENT', '_LOCK', '_REPLACE', '_SUM', '_TH'})
Keywords ws3 reads.
Maintained here, by hand, rather than derived by searching the source for literal tokens. That approach does not work:
import_outputs_sectionrecognises_AREAand_INVENTthrough a genericstartswith('_')check, so there is no literal to find, and a token search reported two working keywords as unsupported. Anything added to an importer must be added here too;tests.test_woodstockguards the parts that can be checked.
- ws3.woodstock.canonical(token: str) str[source]
Reduce a keyword token to its contract spelling.
Indexed keywords appear in real files with a number (
_TH1) but are catalogued under a placeholder (_THn), and the contract stores the placeholder stripped. Normalising both sides means a keyword has one spelling everywhere, which is what makes the support set checkable.
- ws3.woodstock.contract() Any[source]
Load the Woodstock format contract.
Cached, because it is read-only reference data and parsing it repeatedly inside a lint loop would be wasteful.
- Raises:
FileNotFoundError – If the package data is missing, which means an incomplete installation rather than a user error.
- ws3.woodstock.format_findings(findings: Iterable[Finding]) str[source]
Render findings as a readable report.
- ws3.woodstock.lint_dataset(model_path: str, model_name: str, sections_to_check: Iterable[str] | None = None) list[Finding][source]
Report what ws3 will not read from a Woodstock dataset.
Reads the section files directly. Nothing is imported, no model is built, and nothing is modified – so this is safe to run before deciding whether to trust an import.
Findings are ordered by severity, then by file, then by line.
- Parameters:
model_path – Directory holding the section files.
model_name – Base file name shared by the section files.
sections_to_check – Restrict to these section identifiers. Defaults to every section in the contract.
- Returns:
Findings, empty when ws3 reads everything present.
- ws3.woodstock.section_support(name: str) tuple[str | None, str][source]
The importer for a section and whether it does anything.
- Returns:
(importer_name, status)where status is one ofimplemented,partial,stubornone.
- ws3.woodstock.sections() Any[source]
Section identifiers mapped to their contract entry, e.g. file extension.
- ws3.woodstock.supported_keywords() frozenset[str][source]
Keywords ws3 reads. See
SUPPORTED_KEYWORDS.