PlantSimEngine.jlPlantSimEngine.jl

Implement A Hard Dependency​#

Use a hard dependency when one model must decide when or how often another model runs. For example, an energy-balance calculation may evaluate gas exchange at several trial temperatures before accepting a solution.

When a model only needs another model's result, use an ordinary input instead. Coupling models explains the difference, and Control Advanced Execution covers scenario configuration.

Declare the model you need to call​#

The teaching model below acts as a controller: it chooses when to run another model. It selects one leaf, tries two prescribed temperatures, and finally accepts a third. This shows how to run trials; it does not solve an energy-balance equation.

Its declaration asks for temperature-reading models on the current plant's leaves. These definitions are extracted from examples/ToyAdvancedControl.jl:

julia
PlantSimEngine.dep(::ToySelectiveCallControllerModel) = (
    readers=Call(Many(
        scale=:Leaf,
        process=:toy_environment_reader,
        within=Subtree(),
    )),
)

Call declares which models the controller needs. The controller chooses when to run them. Asking for a process on the current plant's leaves lets you reuse it without knowing the names a future simulation will give those model applications.

Run trials and accept one result​#

Here is the actual controller calculation:

julia
function PlantSimEngine.run!(
    model::ToySelectiveCallControllerModel,
    status,
    environment,
    constants,
    context,
)
    targets = call_targets(context, :readers)
    status.target_count = length(targets)
    selected = only(call_targets(
        context,
        :readers;
        objects=(ObjectId(model.selected_object),),
    ))

    for temperature in model.trial_temperatures
        run_call!(
            selected;
            sampled_environment=(T=temperature,),
            publish=false,
        )
    end
    status.trial_temperature_seen = selected.status.temperature_seen

    run_call!(
        selected;
        sampled_environment=(T=model.accepted_temperature,),
        publish=true,
    )
    status.accepted_temperature_seen = selected.status.temperature_seen
    return nothing
end

call_targets lists the models and objects that match the Call declaration. Each match is called a target. Here the controller chooses the target for one leaf. Each trial uses publish=false, so its result is not saved as an accepted output sample. The final call uses publish=true to save the accepted result for output history and time-based connections.

For a real solver, you must decide how to calculate each trial and when a solution is close enough. You must also handle any values changed by a rejected trial: publish=false does not restore them automatically.

Compose a small scenario​#

julia
using Test, PlantSimEngine
using PlantSimEngine.Examples

controller = ToySelectiveCallControllerModel(
    (28.0, 31.0), 22.0; selected_object=:sun_leaf,
)
environment = ToySpatialEnvironment(
    Dict(:sun => (T=26.0,), :shade => (T=18.0,));
    step_seconds=3600.0,
)
model = CompositeModel(
    Object(:plant; scale=:Plant),
    Object(:sun_leaf; scale=:Leaf, parent=:plant, geometry=(cell=:sun,)),
    Object(:shade_leaf; scale=:Leaf, parent=:plant, geometry=(cell=:shade,));
    applications=(
        ModelSpec(
            ToyEnvironmentReaderModel();
            name=:reader, on=Many(scale=:Leaf),
            environment=Environment(backend=environment),
        ),
        ModelSpec(controller; name=:controller, on=One(scale=:Plant)),
    ),
)
simulation = run!(model; outputs=:all)
accepted = final_state(simulation, :plant)
@test accepted.trial_temperature_seen == 31.0
@test accepted.accepted_temperature_seen == 22.0
(
    last_trial=accepted.trial_temperature_seen,
    accepted=accepted.accepted_temperature_seen,
)
(last_trial = 31.0, accepted = 22.0)

The controller's dep declaration describes the models it calls by default. To choose different models or objects in a simulation, set ModelSpec(...; calls=...). Use Diagnostics.explain_calls(model) to check which models and objects were selected.

Choose the simplest call operation​#

Your algorithm needs…Use
To run all selected models and objectsrun_call!(context, :readers)
To read one called model's parameters or typecall_model(context, :reader)
To choose objects, read their current values, or give them different trial valuescall_targets(context, :readers), then call the selected target

call_model requires exactly one match. If you have already prepared the environmental values that all selected objects should use, pass them as sampled_environment=value to run_call!.

When objects are added, removed, or moved to a different parent through the PlantSimEngine functions, the selection is updated. Use the call functions above so your controller uses that updated selection.