diff --git a/src/Abstract_model_structs.jl b/src/Abstract_model_structs.jl index 9cef7b087..59405e935 100644 --- a/src/Abstract_model_structs.jl +++ b/src/Abstract_model_structs.jl @@ -25,4 +25,47 @@ model_(m::AbstractModel) = m get_models(m::AbstractModel) = [model_(m)] # Get the models of an AbstractModel # Note: it is returning a vector of models, because in this case the user provided a single model instead of a vector of. get_status(m::AbstractModel) = nothing -get_mapped_variables(m::AbstractModel) = Pair{Symbol,String}[] \ No newline at end of file +get_mapped_variables(m::AbstractModel) = Pair{Symbol,String}[] + + +#using Dates +struct TimestepRange + lower_bound::Period + upper_bound::Period +end + +# Default, no specified range, meaning the model either doesn't depend on time or uses the simulation's default (eg smallest) timestep +TimestepRange() = TimestepRange(Second(0), Second(0)) +# Only a single timestep type possible +TimestepRange(p::Period) = TimestepRange(p, p) + +""" + timestep_range_(tsr::TimestepRange) + +Return the model's valid range for timesteps (which corresponds to the simulation base timestep in the default case). +""" +function timestep_range_(model::AbstractModel) + return TimestepRange() +end + +""" + timestep_valid(tsr::TimestepRange) + +Checks whether a TimestepRange +""" +timestep_valid(tsr::TimestepRange) = tsr.lower_bound <= tsr.upper_bound + +function model_timestep_range_compatible_with_timestep(tsr::TimestepRange, p::Period) + if !timestep_valid(tsr) + return false + end + + # 0 means any timestep is valid, no timestep constraints + if tsr.upper_bound == Seconds(0) + return true + end + + return p >= tsr.lower_bound && p <= tsr.lower_bound +end + +# TODO should i set all timestep ranges to default and hope the modeler gets it right or should i force them to write something ? \ No newline at end of file diff --git a/src/PlantSimEngine.jl b/src/PlantSimEngine.jl index ebf59dcb9..b31d98471 100644 --- a/src/PlantSimEngine.jl +++ b/src/PlantSimEngine.jl @@ -26,6 +26,7 @@ import Statistics import SHA: sha1 using PlantMeteo +using PlantMeteo.Dates # UninitializedVar + PreviousTimeStep: include("variables_wrappers.jl") @@ -65,6 +66,9 @@ include("dependencies/printing.jl") include("dependencies/dependencies.jl") include("dependencies/get_model_in_dependency_graph.jl") +# Timesteps. : +include("timestep/timestep_mapping.jl") + # MTG compatibility: include("mtg/GraphSimulation.jl") include("mtg/mapping/getters.jl") @@ -103,6 +107,7 @@ include("examples_import.jl") export PreviousTimeStep export AbstractModel export ModelList, MultiScaleModel +export Orchestrator export RMSE, NRMSE, EF, dr export Status, TimeStepTable, status export init_status! diff --git a/src/mtg/GraphSimulation.jl b/src/mtg/GraphSimulation.jl index 933762409..001a7f3f4 100644 --- a/src/mtg/GraphSimulation.jl +++ b/src/mtg/GraphSimulation.jl @@ -14,6 +14,7 @@ A type that holds all information for a simulation over a graph. - `var_need_init`: a dictionary indicating if a variable needs to be initialized - `dependency_graph`: the dependency graph of the models applied to the graph - `models`: a dictionary of models +- `Orchestrator : the structure that handles timestep peculiarities - `outputs`: a dictionary of outputs """ struct GraphSimulation{T,S,U,O,V} @@ -24,12 +25,12 @@ struct GraphSimulation{T,S,U,O,V} var_need_init::Dict{String,V} dependency_graph::DependencyGraph models::Dict{String,U} + orchestrator::Orchestrator outputs::Dict{String,O} - outputs_index::Dict{String, Int} end -function GraphSimulation(graph, mapping; nsteps=1, outputs=nothing, type_promotion=nothing, check=true, verbose=false) - GraphSimulation(init_simulation(graph, mapping; nsteps=nsteps, outputs=outputs, type_promotion=type_promotion, check=check, verbose=verbose)...) +function GraphSimulation(graph, mapping; nsteps=1, outputs=nothing, type_promotion=nothing, check=true, verbose=false, orchestrator=Orchestrator()) + GraphSimulation(init_simulation(graph, mapping; nsteps=nsteps, outputs=outputs, type_promotion=type_promotion, check=check, verbose=verbose)..., orchestrator) end dep(g::GraphSimulation) = g.dependency_graph diff --git a/src/mtg/initialisation.jl b/src/mtg/initialisation.jl index 57aef81cf..95fb6e5c9 100644 --- a/src/mtg/initialisation.jl +++ b/src/mtg/initialisation.jl @@ -305,7 +305,7 @@ The value is not a reference to the one in the attribute of the MTG, but a copy a value in a Dict. If you need a reference, you can use a `Ref` for your variable in the MTG directly, and it will be automatically passed as is. """ -function init_simulation(mtg, mapping; nsteps=1, outputs=nothing, type_promotion=nothing, check=true, verbose=false) +function init_simulation(mtg, mapping; nsteps=1, outputs=nothing, type_promotion=nothing, check=true, verbose=false, orchestrator=Orchestrator()) # Ensure the user called the model generation function to handle vectors passed into a status # before we keep going diff --git a/src/run.jl b/src/run.jl index d33124738..03974654b 100644 --- a/src/run.jl +++ b/src/run.jl @@ -365,17 +365,21 @@ function run!( meteo=nothing, constants=PlantMeteo.Constants(), extra=nothing; + orchestrator::Orchestrator=nothing, nsteps=nothing, tracked_outputs=nothing, check=true, - executor=ThreadedEx() + executor=ThreadedEx(), + default_timestep::Int, + model_timesteps::Dict{T, Int} where {T} + ) isnothing(nsteps) && (nsteps = get_nsteps(meteo)) meteo_adjusted = adjust_weather_timesteps_to_given_length(nsteps, meteo) # NOTE : replace_mapping_status_vectors_with_generated_models is assumed to have already run if used # otherwise there might be vector length conflicts with timesteps - sim = GraphSimulation(object, mapping, nsteps=nsteps, check=check, outputs=tracked_outputs) + sim = GraphSimulation(object, mapping, nsteps=nsteps, check=check, outputs=tracked_outputs, default_timestep=default_timestep, model_timesteps=model_timesteps) run!( sim, meteo_adjusted, @@ -455,6 +459,18 @@ function run_node_multiscale!( return nothing end + model_timestep = object.model_timesteps[typeof(node.value)] + + if model_timestep != object.default_timestep + # do accumulation + + + # run if necessary + if i % model_timestep != 0 + return nothing + end + end + node_statuses = status(object)[node.scale] # Get the status of the nodes at the current scale models_at_scale = models[node.scale] diff --git a/src/timestep/timestep_mapping.jl b/src/timestep/timestep_mapping.jl new file mode 100644 index 000000000..e30fc97c5 --- /dev/null +++ b/src/timestep/timestep_mapping.jl @@ -0,0 +1,42 @@ + +# Those names all suck, need to change them +# Some of them are probably not ideal for new users, too + +# Some types can also be constrained a lot more, probably + +struct TimestepMapper#{V} + variable_from#::V + timestep_from::Int + mapping_function +end + +struct SimulationTimestepHandler#{W,V} + model_timesteps::Dict{Any, Int} # where {W <: AbstractModel} # if a model isn't in there, then it follows the default, todo check if the given timestep respects the model's range + timestep_variable_mapping::Dict{Any, TimestepMapper} #where {V} +end + +SimulationTimestepHandler() = SimulationTimestepHandler(Dict(), Dict()) #Dict{W, Int}(), Dict{V, TimestepMapper}()) where {W, V} + +mutable struct Orchestrator + # This is actually a general simulation parameter, not-scale specific + # todo change to Period + default_timestep::Int64 + + # This needs to be per-scale : if a model is used at two different scales, + # and the same variable of that model maps to some other timestep to two *different* variables + # then I believe we can only rely on the different scale to disambiguate + non_default_timestep_data_per_scale::Dict{String, SimulationTimestepHandler} + + function Orchestrator(default::Int64, per_scale::Dict{String, SimulationTimestepHandler}) + @assert default >= 0 "The default_timestep should be greater than or equal to 0." + return new(default, per_scale) + end +end + +# TODO have a default constructor take in a meteo or something, and set up the default timestep automagically to be the finest weather timestep +# Other options are possible +Orchestrator() = Orchestrator(1, Dict{String, SimulationTimestepHandler}()) + + +#o = Orchestrator() +#oo = Orchestrator(1, Dict{String, SimulationTimestepHandler}()) \ No newline at end of file diff --git a/test/test-simulation.jl b/test/test-simulation.jl index 4ff83a64d..c44393080 100644 --- a/test/test-simulation.jl +++ b/test/test-simulation.jl @@ -292,4 +292,153 @@ end; end end end -end \ No newline at end of file +end + + + + +using PlantSimEngine +# Include the example dummy processes: +using PlantSimEngine.Examples +using Test, Aqua +using Tables, DataFrames, CSV +using MultiScaleTreeGraph +using PlantMeteo, Statistics +using Documenter # for doctests + +using PlantMeteo.Dates +include("helper-functions.jl") + + + +# These models might be worth exposing in the future ? +PlantSimEngine.@process "basic_current_timestep" verbose = false + +struct HelperCurrentTimestepModel <: AbstractBasic_Current_TimestepModel +end + +PlantSimEngine.inputs_(::HelperCurrentTimestepModel) = (next_timestep=1,) +PlantSimEngine.outputs_(m::HelperCurrentTimestepModel) = (current_timestep=1,) + +function PlantSimEngine.run!(m::HelperCurrentTimestepModel, models, status, meteo, constants=nothing, extra=nothing) + status.current_timestep = status.next_timestep + end + + PlantSimEngine.ObjectDependencyTrait(::Type{<:HelperCurrentTimestepModel}) = PlantSimEngine.IsObjectDependent() + PlantSimEngine.TimeStepDependencyTrait(::Type{<:HelperCurrentTimestepModel}) = PlantSimEngine.IsTimeStepDependent() + +PlantSimEngine.timestep_range_(m::HelperCurrentTimestepModel) = Day(1) + + + PlantSimEngine.@process "basic_next_timestep" verbose = false + struct HelperNextTimestepModel <: AbstractBasic_Next_TimestepModel + end + + PlantSimEngine.inputs_(::HelperNextTimestepModel) = (current_timestep=1,) + PlantSimEngine.outputs_(m::HelperNextTimestepModel) = (next_timestep=1,) + + function PlantSimEngine.run!(m::HelperNextTimestepModel, models, status, meteo, constants=nothing, extra=nothing) + status.next_timestep = status.current_timestep + 1 + end + +PlantSimEngine.timestep_range_(m::HelperNextTimestepModel) = Day(1) + + + + + +PlantSimEngine.@process "ToyDay" verbose = false + +struct MyToyDayModel <: AbstractToydayModel end + +PlantSimEngine.inputs_(m::MyToyDayModel) = (a=1,) +PlantSimEngine.outputs_(m::MyToyDayModel) = (daily_temperature=-Inf,) + +function PlantSimEngine.run!(m::MyToyDayModel, models, status, meteo, constants=nothing, extra=nothing) + status.daily_temperature = meteo.T +end + +PlantSimEngine.@process "ToyWeek" verbose = false + +struct MyToyWeekModel <: AbstractToyweekModel + temperature_threshold::Float64 +end + +MyToyWeekModel() = MyToyWeekModel(30.0) +function PlantSimEngine.inputs_(::MyToyWeekModel) + (weekly_max_temperature=-Inf,) +end +PlantSimEngine.outputs_(m::MyToyWeekModel) = (hot = false,) + +function PlantSimEngine.run!(m::MyToyWeekModel, models, status, meteo, constants=nothing, extra=nothing) + status.hot = status.weekly_max_temperature > m.temperature_threshold +end + +PlantSimEngine.timestep_range_(m::MyToyWeekModel) = Week(1) + + + +PlantSimEngine.@process "DWConnector" verbose = false + +struct MyDwconnectorModel <: AbstractDwconnectorModel + T_daily::Array{Float64} +end + +MyDwconnectorModel() = MyDwconnectorModel(Array{Float64}(undef, 7)) + +function PlantSimEngine.inputs_(::MyDwconnectorModel) + (daily_temperature=-Inf, current_timestep=1,) +end +PlantSimEngine.outputs_(m::MyDwconnectorModel) = (weekly_max_temperature = 0.0,) + +function PlantSimEngine.run!(m::MyDwconnectorModel, models, status, meteo, constants=nothing, extra=nothing) + m.T_daily[1 + (status.current_timestep % 7)] = status.daily_temperature + + if(status.current_timestep % 7 == 1) + status.weekly_max_temperature = sum(m.T_daily)/7.0 + else + status.weekly_max_temperature = 0 + end +end + + PlantSimEngine.timestep_range_(m::MyDwconnectorModel) = Day(1) + + + + + +meteo_day = read_weather(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), duration=Day) + +m = Dict("Default" => ( + MyToyDayModel(), + MyToyWeekModel(), + MyDwconnectorModel(), + HelperNextTimestepModel(), + MultiScaleModel( + model=HelperCurrentTimestepModel(), + mapped_variables=[PreviousTimeStep(:next_timestep),], + ), + Status(a=1,))) + +to_initialize(m) + +models_timestep = Dict(MyToyDayModel=>1, MyDwconnectorModel => 1, MyToyWeekModel =>7, HelperNextTimestepModel => 1, HelperCurrentTimestepModel => 1) + +mtg = Node(MultiScaleTreeGraph.NodeMTG("/", "Default", 1, 1)) + +out = run!(mtg, m, meteo_day, default_timestep=1, model_timesteps=models_timestep) + +@testset "Test varying timestep" begin + + + @test + @test + +end + + + # NOTE : replace_mapping_status_vectors_with_generated_models is assumed to have already run if used + # otherwise there might be vector length conflicts with timesteps + sim = @enter PlantSimEngine.GraphSimulation(mtg, m, nsteps=nothing, check=true, outputs=nothing, default_timestep=1, model_timesteps=models_timestep) + +using PlantSimEngine