Some models also change their environment. A canopy energy-balance model, for example, may try several temperatures before deciding which temperature other models should use. A model that manages these trials is called a controller.
This teaching example uses prescribed temperatures to explain that sequence. It does not implement a physical canopy solver. Start with Modify The Environment for the scenario perspective and Implement A Hard Dependency for model calls.
A controller declares both the model it calls and the environmental variables it may update. Here environment_outputs_ names temperature T. These are the actual declarations in examples/ToySpatialEnvironment.jl:
PlantSimEngine.inputs_(::ToyEnvironmentControllerModel) = NamedTuple()
PlantSimEngine.dep(::ToyEnvironmentControllerModel) = (
reader=Call(One(process=:toy_environment_reader)),
)
function PlantSimEngine.outputs_(model::ToyEnvironmentControllerModel)
initial = zero(model.accepted_temperature)
return (
trial_temperature_seen=initial,
accepted_temperature_seen=initial,
)
end
PlantSimEngine.environment_outputs_(model::ToyEnvironmentControllerModel) = (
T=zero(model.accepted_temperature),
)The variables in outputs_ record the temperatures seen during the calculation. The variable in environment_outputs_ is the temperature the controller may change in the shared environment.
The controller's implementation makes the sequence explicit:
function PlantSimEngine.run!(
model::ToyEnvironmentControllerModel,
status,
environment,
constants,
context,
)
trial_environment = (T=model.trial_temperature,)
trial_target = only(run_call!(
context,
:reader;
environment=trial_environment,
publish=false,
))
status.trial_temperature_seen = trial_target.status.temperature_seen
accepted_environment = (T=model.accepted_temperature,)
commit_environment!(context, accepted_environment)
accepted_target = only(run_call!(
context,
:reader;
environment=accepted_environment,
publish=true,
))
status.accepted_temperature_seen =
accepted_target.status.temperature_seen
return nothing
endThe first call gives the reader a trial temperature. Its result is not saved as an accepted output sample. commit_environment! then stores the accepted temperature in the shared environment. The final reader call saves the result calculated at that temperature.
For a scientific controller, you must choose how to calculate trials and when to accept a solution. You must also handle any values changed by a rejected trial: publish=false does not restore them automatically.
An environment provider supplies data such as temperature to the models. This example stores temperature in a named canopy cell. The reader can read that temperature. The controller also gets sink=:cells, which allows it to write accepted temperatures into the provider's cells.
using Test, PlantSimEngine
using PlantSimEngine.Examples
environment = ToySpatialEnvironment(
Dict(:canopy => (T=20.0,)); step_seconds=3600.0,
)
controller = ToyEnvironmentControllerModel(30.0, 22.0)
model = CompositeModel(
Object(:leaf; scale=:Leaf, geometry=(cell=:canopy,));
applications=(
ModelSpec(
ToyEnvironmentReaderModel();
name=:reader, on=One(scale=:Leaf),
environment=Environment(backend=environment),
),
ModelSpec(
controller;
name=:controller, on=One(scale=:Leaf),
environment=Environment(backend=environment, sink=:cells),
),
),
)
simulation = run!(model; outputs=:all)
state = final_state(simulation)
@test state.trial_temperature_seen == 30.0
@test state.accepted_temperature_seen == 22.0
@test environment.cells[:canopy].T == 22.0
(
trial=state.trial_temperature_seen,
accepted=state.accepted_temperature_seen,
committed_temperature=environment.cells[:canopy].T,
)The temperature starts at 20, the trial uses 30, and the accepted value is 22. Use Diagnostics.explain_environment_bindings(model) to check where models read and write environmental data. Diagnostics.explain_outputs(simulation) shows the saved results.
When accepting a solution, supply a value for every variable declared in environment_outputs_. If another controller calls this one as a trial with publish=false, none of its nested calls can save accepted output samples or change the shared environment.
To add a new kind of spatial environment provider, see Environment Backend Extensions. When writing a process model, you can normally use an existing provider through Environment and the call and commit functions shown here.