Project data — _data#
A project usually has data that many documents use: section properties, the design schedule, analysis results. Declare each file once, by name, and every document in the project can use it by that name. Documents never contain paths to project data.
The project root#
The directory of the project config (the nearest *.ymprint.yml above the document; see
Configuration) is the project root. A
document with no project config above it is its own root: its directory is used.
Declaring data sources#
# project.ymprint.yml (at the project root)
_data:
sections: 1-reference/sections.yml
materials: 1-reference/materials.yml
design: 4-design/design.yml
frame: 3-analysis/results/frame.json
Every document under the project root can now use sections, design and so on:
as Jinja variables in text:
{{ design.B01.section }};as Python globals in
_pyand_hcblocks:sections[design["B01"]["section"]]["Zx"];as block values with
$design.
# 5-validation/beams/calc.yml — no paths
Beam B01:
- "Section: {{ design.B01.section }}"
- _hc: |
Z_x = sections[design["B01"]["section"]]["Zx"]
Renaming the 4-design/ folder means editing one line in the project config.
Long form#
_data:
frame:
path: 3-analysis/results/frame.json
loader: json # default: chosen from the file extension
Option |
Meaning |
|---|---|
|
The file, relative to the file that declares the entry. |
|
The loader to read it with. Built in: |
|
The document(s) or script(s) that produce this data, instead of a person writing it. See Produced data. |
In a document#
A document can declare its own _data in its front matter. Its entries are added to the
project’s, and replace a project entry of the same name.
_data:
local_loads: loads.yml # relative to this document
A _vars name that is also a data source name is an error: rename one of them.
Variables passed in at render time (for example by a before_render hook) take
precedence over data sources.
How data is loaded#
Lazily. A source is read the first time the document uses it. A source that a document never uses is never read, so a missing file only matters to the documents that use it.
Once per process. A file is parsed once and reused until it changes on disk.
A fresh copy per render. Changing data in a
_pyblock only changes this render’s copy; the next document sees the file as it is.
Live mode watches the data files a document reads (as well as its images and PDF
backgrounds), so editing design.yml re-renders the document that uses it.
Produced data — from:#
Some data isn’t written by a person: an analysis computes forces, a calc computes
capacities. Its _data entry says so with from:, which names the file that produces it.
from: is always written on a _data entry, never in the producer. It can name a
document (.yml) or a Python script (.py), and the entry looks the same either way. What
differs is how the producer hands its values over:
A document declares an
_exportmapping. YMPrint evaluates it after the document renders and writes the file.A script calls
publish(name, value)fromymprint.project. The script writes the file itself, when that line runs.
# project.ymprint.yml
_data:
loads: loads.yml # written by a person
forces:
from: analysis/frame.py # produced by a script
capacities:
from: calcs/beams.yml # produced by a document
Readers can’t tell the difference. A document uses {{ forces.B01.Mf }} or
capacities["B01"] and a script calls data("forces"), whatever produced them.
A script as producer#
# analysis/frame.py
from ymprint.project import data, publish
loads = data("loads")
forces = {m: {"Mf": v["w"] * v["L"] ** 2 / 8} for m, v in loads.items()}
publish("forces", forces) # the name of the _data entry, not a path
publish takes the name of the data source. It writes value as JSON to wherever that
source’s output is stored (see Where produced data is stored),
so the script never contains a path. The script must be named in that entry’s from:.
Otherwise publish refuses:
DataSourceError: frame.py cannot publish 'loads': its `_data` entry lists its producers
in `from:` (currently: none).
The script writes its data every time it runs, whether under ym build or on its own with
python analysis/frame.py. If a script is named in from: but never calls publish for
that name, nothing is written, and documents that read the source fail with
has not been produced yet.
One script can produce several sources. Name it in each entry’s from: and call publish
once for each:
_data:
forces: {from: analysis/frame.py}
reactions: {from: analysis/frame.py}
publish("forces", forces)
publish("reactions", {m: v["w"] * v["L"] / 2 for m, v in loads.items()})
A document as producer#
# calcs/beams.yml
_export:
B01: {Mr: $Mr_B01}
B02: {Mr: $Mr_B02}
Beam capacities:
- _hc: |
Mr_B01 = phi * Z_x * f_y
...
The document never names capacities. It declares what it produces, and every _data
entry whose from: names calcs/beams.yml receives that whole _export mapping. A
document rendered once per parameter set produces one entry
per set, keyed by the set.
Exports are written on full renders (ym build, ym convert, Ymprint.render_all), but
not by ym live, --var or --only renders. A document named in from: without an
_export is an error. See Document exports for the details.
Side by side#
Document ( |
Script ( |
|
|---|---|---|
Hands over its values with |
an |
|
Who writes the file |
YMPrint, after the document renders |
the script, when |
Producing several sources |
every source naming it gets the same |
one |
Written when |
full renders: |
every run: |
Rendered per parameter set |
one entry per set, keyed by the set |
— |
Named in |
error: the document has no |
readers fail: has not been produced yet |
Several producers#
from: can list several producers, documents and scripts mixed. Each producer’s output is
stored separately, and readers see the union of their mappings:
_data:
capacities:
from:
- calcs/beams.yml # _export: {B01: ..., B02: ...}
- analysis/joists.py # publish("capacities", {"J01": ...})
- "Joist J01: Mr = {{ capacities.J01.Mr }}, beam B01: Mr = {{ capacities.B01.Mr }}"
Each producer must produce a mapping, and no two producers may give the same key: every
value has exactly one author. A source with several producers can’t have a path, and
ym.path() doesn’t work for it, because it is read from several files.
Where produced data is stored#
The |
Each producer writes to |
|---|---|
no |
|
a |
that path, e.g. to keep the file somewhere visible |
_data:
forces:
path: build/data/forces.json
from: analysis/frame.py
build/
├── data/forces.json # forces (has a path)
└── .ym/exports/
├── capacities/calcs__beams.yml.json # capacities, from the document
├── capacities/analysis__joists.py.json # capacities, from the script
└── reactions/analysis__frame.py.json # reactions
The build directory can be changed with _build: {dir: ...}.
These files belong to YMPrint: read them by name with _data, not by path.
from: and ym build#
from: is the only place the producer relationship is stated, and it is all
ym build needs. Every producer it names becomes a build step. When
a step reads a source, the build runs that source’s producers first:
✓ analysis/frame.py 0.01 s (never built)
✓ calcs/beams.yml 0.31 s (never built)
✓ analysis/joists.py 0.00 s (never built)
✓ analysis/check.py 0.02 s (never built)
✓ report/report.yml 0.03 s (never built)
Built 5 of 5 steps in 0.4 s
Outside ym build, reading a produced source before its producer has run is an error that
names the producer:
Data source 'forces' has not been produced yet: frame.py has not written it
(…/build/data/forces.json). Render …/analysis/frame.py first, or run `ym build`.
One path rule#
Every relative path resolves against the directory of the file that declares it:
Declared in |
Resolves against |
|---|---|
|
that config file’s directory |
|
the document’s directory |
A page background in a config file |
that config file’s directory |
A page background in a document |
the document’s directory |
So a document renders the same whether ym is launched from the project root, from the
document’s folder, or from anywhere else. (A background can still say relative-to: cwd
to resolve against the current directory.)
The ym variable#
_py and _hc code runs in the directory ym was launched from, like any Python. Code
that needs project paths uses the reserved variable ym:
Name |
Value |
|---|---|
|
|
|
|
|
The resolved path of data source |
|
The current parameter set, for documents rendered once per parameter set |
- _py: |
import pandas as pd
loads = pd.read_csv(ym.path("load_schedule"))
Custom loaders#
Register a loader for other file types. A plugin package can do this when it is installed; see Plugins.
from ymprint import register_loader
register_loader("toml", lambda path: tomllib.loads(path.read_text()), extensions=[".toml"])
A loader is a function loader(path) -> data. A units-aware loader, for example, could turn
fy: 350 MPa into a quantity when the file is read.