Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 27 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,24 @@ model-scale simulations.

## Installation

In Julia package mode:

```julia
add PlantSimEngine
```

Then:
The examples on this development branch use `CompositeModel`. Registered
releases through 0.14.1 use the previous mapping API; use their matching
[stable documentation](https://VirtualPlantLab.github.io/PlantSimEngine.jl/stable).
To run the examples below, install the development version in a project:

```julia
using Pkg
Pkg.activate("my_simulation")
Pkg.add(["PlantMeteo", "DataFrames"])
Pkg.add(url="https://github.com/VirtualPlantLab/PlantSimEngine.jl", rev="main")
using PlantSimEngine
```

When following a pull-request preview, use its branch or commit instead of
`"main"`. Keep `Project.toml` and `Manifest.toml` with your experiment to record
the package revisions. The [installation guide](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/prerequisites/installing_plantsimengine.html)
also installs the plotting tools used by the tutorials.

## Quickstart

This example runs three existing toy models on one model object:
Expand All @@ -62,12 +68,19 @@ This example runs three existing toy models on one model object:
2. `ToyLAIModel` consumes cumulative thermal time and computes LAI.
3. `Beer` consumes LAI and meteorology to compute absorbed PAR.

Run the full weather year to see canopy growth and senescence. The bundled
file contains daily radiation totals in MJ m⁻² d⁻¹ under historical `_f`
column names; convert them to mean fluxes in W m⁻² for `Beer` when reading.

```julia
using PlantSimEngine, PlantMeteo, Dates, DataFrames
using PlantSimEngine.Examples

meteo_day = read_weather(
joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv");
joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"),
:Ri_SW_f => (x -> x .* 1e6 ./ 86_400) => :Ri_SW_f,
:Ri_PAR_f => (x -> x .* 1e6 ./ 86_400) => :Ri_PAR_f,
:Ri_NIR_f => (x -> x .* 1e6 ./ 86_400) => :Ri_NIR_f;
duration=Dates.Day,
)

Expand All @@ -78,7 +91,7 @@ model = CompositeModel(
environment=meteo_day,
)

sim = run!(model; steps=30, outputs=:all)
sim = run!(model; steps=length(meteo_day), outputs=:all)
out = collect_outputs(sim; sink=DataFrame)
first(out, 6)
```
Expand Down Expand Up @@ -208,11 +221,11 @@ Diagnostics.explain_execution_plan(model)

- [Stable documentation](https://VirtualPlantLab.github.io/PlantSimEngine.jl/stable)
- [Development documentation](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev)
- [CompositeModel/object quickstart](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/composite_model/quickstart/)
- [Implement a model](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/journeys/modelers/basic_model/)
- [AI agent skill](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/agent_skill/)
- [CompositeModel/object migration guide](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/migration_composite_model/)
- [Public API reference](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/API/API_public/)
- [Run and couple models](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/journeys/users/one_object.html)
- [Implement a model](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/journeys/modelers/basic_model.html)
- [AI agent skill](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/agent_skill.html)
- [CompositeModel/object migration guide](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/migration_composite_model.html)
- [Public API reference](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/API/API_public.html)

## Projects That Use PlantSimEngine

Expand Down
11 changes: 11 additions & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
[deps]
Bonito = "824d6782-a2ef-11e9-3a09-e5662e0c26f8"
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589"
MultiScaleTreeGraph = "dd4a991b-8a45-4075-bede-262ee62d5583"
PlantMeteo = "4630fe09-e0fb-4da5-a846-781cb73437b6"
PlantSimEngine = "9a576370-710b-4269-adf9-4f603a9c6423"

[compat]
Bonito = "5.2"
Documenter = "1.11"
LiveServer = "1"
julia = "1.11"

[sources]
PlantSimEngine = {path = ".."}
21 changes: 21 additions & 0 deletions docs/bonito_rendering.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module BonitoRendering

using Bonito, Documenter

const Writer = Base.get_extension(Bonito, :BonitoDocumenterExt)
isnothing(Writer) && error("Bonito's Documenter extension must be loaded")

# Documenter wraps @eval Markdown results in a MarkdownAST.Document node.
# Bonito 5.2 falls back to plain text for that root, dropping code blocks,
# tables and their formatting. Dispatch its children through the normal writer.
# Leave an upstream implementation in place when Bonito adds this method.
const document_method = which(
Writer.domify, (Writer.DCtx, Writer.MA.Node, Writer.MA.Document),
)
if last(Base.unwrap_unionall(document_method.sig).parameters) === Any
function Writer.domify(ctx::Writer.DCtx, node::Writer.MA.Node, ::Writer.MA.Document)
return Writer.domify_children(ctx, node)
end
end

end
14 changes: 14 additions & 0 deletions docs/branding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# PlantSimEngine logo

The logo combines a branching plant with three connection points. The leaves and roots represent the soil–plant–atmosphere systems modeled with PlantSimEngine; the connected nodes represent reusable processes and their coupling. The plant is deliberately species-neutral.

The transparent PNG in `src/assets/logo.png` is used for the documentation header, landing page, and favicon. It was generated with Codex's built-in image-generation tool on 2026-09-06. It is a raster original, not an SVG. Keep the original alpha channel when preparing other sizes.

## Generation prompt

Use case: logo-design.
Asset type: Primary standalone logo mark for PlantSimEngine.jl, an open-source Julia scientific framework for coupling modular process models to simulate plants, their growth, and exchanges with soil and atmosphere.
Create ONE finished, distinctive professional symbol, on a genuinely transparent background with an alpha channel. No lettering.
Concept: a living branching plant that is also a small connected model graph. A clean upright gently curved central stem, three to four botanical leaf shapes on branches at different heights, an understated small branching root base, and exactly three restrained circular connection nodes integrated into the stem/branch junctions. The branching stem itself provides the graph edges; avoid a separate network floating around the plant. Convey modularity, exchange and growth. Species-neutral, not palm-specific.
Style: exceptionally clear flat vector-like brand mark, elegant scientific identity, generous negative space, coherent moderately bold smooth strokes, crisp geometric curves, a recognizable silhouette at 32 pixels. A slight botanical asymmetry makes it alive. Leaves should look organic and distinct, with no internal vein detail. Compact roughly circular/rounded-triangular composition, not a tall skinny sprout. Palette limited to deep teal #156F64, fresh green #64AF66 and a small warm gold #DEA748 accent on one node. Colors should read on both white and dark navy backgrounds.
Composition: icon alone centered on a square 1024x1024 transparent canvas; icon fills about 80 percent of frame with balanced clear margin. No text, no wordmark, no surrounding circle/badge, no gear, no literal motor, no circuit-board clutter, no gradients, no shadows, no 3D, no mockup, no decorative background, no watermark. Deliver the actual logo asset, not a presentation sheet.
124 changes: 124 additions & 0 deletions docs/check_static_export.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""
Finish and validate the Bonito documentation export.

Bonito 5.2 emits site-root-relative URLs even for nested Markdown pages. Give
each page an explicit site base, keep fragment links on their original page,
and adapt the theme's outline lookup. This also fixes asset URLs serialized
inside Bonito applications, which cannot be repaired by rewriting HTML alone.
"""
function finish_static_export(build_dir=joinpath(@__DIR__, "build"))
source_dir = joinpath(@__DIR__, "src")
pages = [
replace(relpath(joinpath(root, file), source_dir), r"\.md$" => ".html")
for (root, _, files) in walkdir(source_dir) for file in files
if endswith(file, ".md")
]

# Preserve Bonito's scroll-aware outline after qualifying fragment links.
scripts = [
joinpath(root, file)
for (root, _, files) in walkdir(joinpath(build_dir, "bonito")) for file in files
if occursin(r"^docs\d+\.js$", file)
]
length(scripts) == 1 || error("Expected one Bonito documentation theme script")
script = only(scripts)
javascript = read(script, String)
old_lookup = "a.getAttribute(\"href\").slice(1)"
count(old_lookup, javascript) == 1 || error(
"Bonito's outline code changed; review the nested-page export adjustment",
)
old_version_link = "return '<a href=\"../' + v + '/\">' + esc(v) + \"</a>\";"
count(old_version_link, javascript) == 1 || error(
"Bonito's version navigation changed; review the preview export adjustment",
)
new_version_link = "return '<a href=\"' + new URL(v + '/', window.__PSE_VERSIONS_ROOT__).href + '\">' + esc(v) + \"</a>\";"
# Exported package assets may be read-only. Write a site-owned script and
# leave the copied upstream asset intact.
patched_script = joinpath(build_dir, "assets", "docs-navigation.js")
write(patched_script, replace(javascript, old_lookup => "a.hash.slice(1)", old_version_link => new_version_link))
original_script_url = replace(relpath(script, build_dir), '\\' => '/')

for page in pages
path = joinpath(build_dir, page)
isfile(path) || error("Missing exported documentation page: $page")
html = read(path, String)
occursin("<base ", html) && error(
"Bonito now emits a base URL; review the nested-page export adjustment",
)
base = replace(relpath(build_dir, dirname(path)), '\\' => '/') * "/"
page_url = replace(page, '\\' => '/')
html = replace(html, original_script_url => "assets/docs-navigation.js")
html = replace(html, "href=\"#" => "href=\"$page_url#")

html = normalize_local_export_links(html, path, build_dir)
# Version metadata lives above dev/stable, and two levels above a PR preview.
version_loader = raw"""<script>
window.__PSE_VERSIONS_ROOT__ = new URL(
/\/previews\/PR\d+\/$/.test(new URL(document.baseURI).pathname) ? '../../' : '../',
document.baseURI
).href;
if (new URL(document.baseURI).pathname !== '/') {
document.write('<script src="' + window.__PSE_VERSIONS_ROOT__ + 'versions.js"><\/script>');
}
</script>"""
versions_tag = r"<script\b[^>]*\bsrc=\"\.\./versions\.js\"[^>]*></script>"
count(versions_tag, html) == 1 || error("Expected Bonito version metadata script in $page")
html = replace(html, versions_tag => version_loader)
extras = "<base href=\"$base\">" *
"<link rel=\"icon\" type=\"image/png\" href=\"assets/logo.png\">"
count("<head>", html) == 1 || error("Expected one HTML head in $page")
html = replace(html, "<head>" => "<head>" * extras; count=1)
html = replace(html, "</body>" => "<link rel=\"stylesheet\" href=\"assets/brand.css\"></body>"; count=1)
write(path, html)
end
check_static_export(build_dir; pages)
return nothing
end

function normalize_local_export_links(html, page_path, build_dir)
# Raw embeds and some Documenter contents links remain page-relative.
return replace(html, r"(?:href|src)=\"[^\"]*\"" => matched -> begin
attribute, reference = split(matched, "=\""; limit=2)
reference = chop(reference; tail=1)
occursin(r"^(?:[A-Za-z][A-Za-z0-9+.-]*:|/|#)", reference) && return matched
target = first(split(first(split(reference, '#')), '?'))
isempty(target) && return matched
ispath(joinpath(build_dir, target)) && return matched
resolved = normpath(joinpath(dirname(page_path), target))
isfile(resolved) || return matched
relative = replace(relpath(resolved, build_dir), '\\' => '/')
suffix = reference[nextind(reference, lastindex(target)):end]
"$attribute=\"$relative$suffix\""
end)
end

function check_static_export(build_dir=joinpath(@__DIR__, "build"); pages)
binary_files = Set{String}()
for page in pages
html = read(joinpath(build_dir, page), String)
occursin(r"Bonito\.init_session\([^;]*,\s*false\);"s, html) ||
error("Missing expected Bonito export bootstrap: $page")
markup = replace(html, r"<script\b[^>]*>.*?</script>"s => matched -> first(split(matched, '>'; limit=2)) * ">")
references = [m.captures[1] for m in eachmatch(r"(?:href|src)=\"([^\"]+)\"", markup)]
for matched in eachmatch(r"Bonito\.fetch_binary\([\"']([^\"']+)[\"']\)", html)
reference = matched.captures[1]
push!(references, reference)
push!(binary_files, reference)
end
for reference in references
startswith(reference, "//") && continue
occursin(r"^[A-Za-z][A-Za-z0-9+.-]*:", reference) && continue
target = first(split(first(split(reference, '#')), '?'))
isempty(target) && continue
target = replace(target, "%20" => " ", "&amp;" => "&")
# The base itself deliberately leads from this page to the site root.
target == replace(relpath(build_dir, dirname(joinpath(build_dir, page))), '\\' => '/') * "/" && continue
ispath(joinpath(build_dir, target)) || error(
"Missing local export target $reference in $page",
)
end
end
isempty(binary_files) && error("Static export contains no Bonito session data")
@info "Validated static documentation export" pages=length(pages) states=length(binary_files)
return nothing
end
Loading
Loading