From e9378ad4a675d8f27de4a1daf0422724f0a1e35d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Str=C3=B6mer?= Date: Fri, 25 Oct 2024 18:03:52 +0200 Subject: [PATCH] feat: each Virtual now keeps a reference to its template; CoreTemplate now implements a proper summary extraction (and Base.show) --- src/core.jl | 2 + src/core/virtual.jl | 1 + src/parser.jl | 7 -- src/templates/definition.jl | 20 ++++++ src/templates/parse.jl | 8 +++ src/templates/templates.jl | 131 +++++++++--------------------------- src/templates/utils.jl | 45 +++++++++++++ 7 files changed, 107 insertions(+), 107 deletions(-) create mode 100644 src/templates/definition.jl create mode 100644 src/templates/utils.jl diff --git a/src/core.jl b/src/core.jl index 9f0db29..b008f6a 100644 --- a/src/core.jl +++ b/src/core.jl @@ -93,6 +93,8 @@ macro profile(arg1, arg2=nothing, arg3=nothing) end end +include("templates/definition.jl") # this is used in `virtual.jl` and needs to be included first + include("core/carrier.jl") include("core/expression.jl") # this needs to come before the core components using it include("core/connection.jl") diff --git a/src/core/virtual.jl b/src/core/virtual.jl index 4ecb419..25644fe 100644 --- a/src/core/virtual.jl +++ b/src/core/virtual.jl @@ -18,6 +18,7 @@ might expect (and want) to be able to interact with "my_storage_foo". Since the # [Internal] ======================================================================================================= _parameters = Dict{String, Any}() _finalizers::Vector{Base.Callable} = Base.Callable[] + _template::CoreTemplate # [External] ======================================================================================================= # - diff --git a/src/parser.jl b/src/parser.jl index e632306..50964b3 100644 --- a/src/parser.jl +++ b/src/parser.jl @@ -203,13 +203,6 @@ function _flatten_model!(model::JuMP.Model, description::Dict{String, Any}) _is_valid_template_name(type) || @error "Invalid type of `Template` (check documentation)" type - # Remember its name and type properly, before that is lost due to flattening, by constructing a Virtual. - _iesopt(model).model.components[cname] = Virtual(; model=model, name=cname, type=type) - - # Properly tag the new Virtual. - !haskey(_iesopt(model).model.tags, type) && (_iesopt(model).model.tags[type] = Vector{String}()) - push!(_iesopt(model).model.tags[type], cname) - # Try parsing it. new_components = _parse_noncore!(model, description, cname) toflatten = vcat(toflatten, new_components) diff --git a/src/templates/definition.jl b/src/templates/definition.jl new file mode 100644 index 0000000..59265cd --- /dev/null +++ b/src/templates/definition.jl @@ -0,0 +1,20 @@ +""" + CoreTemplate + +A struct to represent an IESopt.jl "Core Template". +""" +@kwdef struct CoreTemplate + model::JuMP.Model + name::String + path::String + raw::String + yaml::Dict{String, Any} = Dict{String, Any}() + + """A dictionary of functions that can be called by the template, options are `:validate`, `:prepare`, `:finalize`.""" + functions::Dict{Symbol, Function} = Dict{Symbol, Function}() + + """Type of this `CoreTemplate`: `:container` (if `"components"` exists), `:component` (if `"component"` exists).""" + type::Ref{Symbol} = Ref(:none) + + _status::Ref{Symbol} +end diff --git a/src/templates/parse.jl b/src/templates/parse.jl index ce98080..beffde3 100644 --- a/src/templates/parse.jl +++ b/src/templates/parse.jl @@ -326,6 +326,14 @@ function _parse_noncore!(model::JuMP.Model, description::Dict{String, Any}, cnam type = pop!(description[cname], "type") template = _require_template(model, type) + + # Remember its name and type properly, before that is lost due to flattening, by constructing a Virtual. + _iesopt(model).model.components[cname] = Virtual(; model, name=cname, type, _template=template) + + # Properly tag the new Virtual. + !haskey(_iesopt(model).model.tags, type) && (_iesopt(model).model.tags[type] = Vector{String}()) + push!(_iesopt(model).model.tags[type], cname) + # if !haskey(_iesopt(model).input.noncore[:templates], type) # valid_templates = [ # path for diff --git a/src/templates/templates.jl b/src/templates/templates.jl index 84a3deb..627edb6 100644 --- a/src/templates/templates.jl +++ b/src/templates/templates.jl @@ -1,120 +1,51 @@ -""" - CoreTemplate -A struct to represent an IESopt.jl "Core Template". -""" -@kwdef struct CoreTemplate - model::JuMP.Model - name::String - path::String - raw::String - yaml::Dict{String, Any} = Dict{String, Any}() +include("utils.jl") - """A dictionary of functions that can be called by the template, options are `:validate`, `:prepare`, `:finalize`.""" - functions::Dict{Symbol, Function} = Dict{Symbol, Function}() - - """Type of this `CoreTemplate`: `:container` (if `"components"` exists), `:component` (if `"component"` exists).""" - type::Ref{Symbol} = Ref(:none) - - _status::Ref{Symbol} -end - -function _get_parameter_safe(p::String, parameters::Dict{String, Any}, default::Any = nothing) - haskey(parameters, p) || @critical "Trying to access (`get`) undefined parameter in `CoreTemplate`" parameter = p - return isnothing(default) ? parameters[p] : something(parameters[p], default) -end - -function _set_parameter_safe(p::String, v::Any, parameters::Dict{String, Any}) - haskey(parameters, p) || @critical "Trying to access (`set`) undefined parameter in `CoreTemplate`" parameter = p - parameters[p] = v - return nothing -end - -function _get_timeseries_safe(p_or_cf::String, parameters::Dict{String, Any}, model::JuMP.Model) - if !occursin("@", p_or_cf) - p_or_cf = _get_parameter_safe(p_or_cf, parameters)::String - end - - # Now we know, that `p_or_cf` is a "col@file" selector string. - column, file = string.(split(p_or_cf, "@")) +include("functions/functions.jl") +include("load.jl") +include("parse.jl") - return _getfromcsv(model, file, column) -end +function Base.show(io::IO, template::CoreTemplate) + info = _analyse(template) -function _set_timeseries_safe(p_or_cf::String, v::Any, parameters::Dict{String, Any}, model::JuMP.Model) - if !occursin("@", p_or_cf) - p_or_cf = _get_parameter_safe(p_or_cf, parameters)::String - end + beautify(value::Any) = value + beautify(value::Vector) = isempty(value) ? "-" : (length(value) <= 4 ? join(value, ", ") : "$(value[1]), $(value[2]), ..., $(value[end])") - # Now we know, that `p_or_cf` is a "col@file" selector string. - column, file = string.(split(p_or_cf, "@")) + str_show = ":: IESopt.Template ::" - # Check if this file exists. - if haskey(_iesopt(model).input.files, file) - # This works for overwriting existing columns, as well as adding new ones. - _iesopt(model).input.files[file][!, column] .= v - else - _iesopt(model).input.files[file] = DataFrames.DataFrame(column => v) + ks = collect(keys(info)) + for k in ks[1:(end - 1)] + v = info[k] + (k == "docs") && (v = collect(keys(info[k]))) + (k == "parameters") && (v = [p for p in keys(info[k]) if !startswith(p, "_")]) + str_show *= "\nā”œ $k: $(beautify(v))" end + k = ks[end] + v = (k in ["docs", "parameters"]) ? collect(keys(info[k])) : info[k] + str_show *= "\nā”” $k: $(beautify(v))" - return nothing -end - -include("functions/functions.jl") -include("load.jl") -include("parse.jl") - -_is_template(filename::String) = endswith(filename, ".iesopt.template.yaml") -_get_template_name(filename::String) = string(rsplit(basename(filename), "."; limit=4)[1]) -_is_component(template::CoreTemplate) = template.type[] == :component -_is_container(template::CoreTemplate) = template.type[] == :container - -function Base.show(io::IO, template::CoreTemplate) - str_show = "IESopt.CoreTemplate: $(template.name)" return print(io, str_show) end -function analyse(template::CoreTemplate) +function _analyse(template::CoreTemplate) old_status = template._status[] template = _require_template(template.model, template.name) template._status[] = old_status - child_types::Vector{String} = sort!(collect(Set(if haskey(template.yaml, "component") - [template.yaml["component"]["type"]] - else - [v["type"] for v in values(template.yaml["components"])] - end))) - - child_templates = [t for t in child_types if t āˆ‰ ["Connection", "Decision", "Node", "Profile", "Unit"]] - child_corecomponents = [t for t in child_types if t in ["Connection", "Decision", "Node", "Profile", "Unit"]] + internal = (template.type[] == :container) ? values(template.yaml["components"]) : [template.yaml["component"]] + child_types = sort!(collect(Set(comp["type"] for comp in internal)))::Vector{String} + instances = get(_iesopt(template.model).model.tags, template.name, String[]) - docs = "" - for line in eachline(IOBuffer(template.raw)) - startswith(line, "#") || break - length(line) >= 3 || continue - docs = "$(docs)\n$(line[3:end])" - end - - if isempty(docs) - @warn "Encountered empty docstring for `CoreTemplate`" template = template.name - else - docs = docs[2:end] # remove the leading `\n` - startswith(docs, "# ") || - @warn "`CoreTemplate` docstring should start with main header (`# Your Title`)" template = template.name - for section in ["Parameters", "Components", "Usage"] - occursin("## $(section)\n", docs) || - @warn "`CoreTemplate` is missing mandatory section in docstring" template = template.name section - end - end + docs = get(template.yaml, "docs", Dict{String, Any}()) + isempty(docs) && @warn "Template is missing `docs` entry" template = template.name - return ( - name=template.name, - was_prepared=old_status == :yaml, - docs=Markdown.parse(docs), - functions=keys(get(template.yaml, "functions", Dict{String, Any}())), - parameters=get(template.yaml, "parameters", Dict{String, Any}()), - child_templates=child_templates, - child_corecomponents=child_corecomponents, + return OrderedDict( + "type" => (template.type[] == :container) ? template.name : "$(template.name) <: $(child_types[1])", + "instances" => instances, + "docs" => docs, + "parameters" => get(template.yaml, "parameters", Dict{String, Any}()), + "child types" => child_types, + "functions" => collect(keys(get(template.yaml, "functions", Dict{String, Any}()))) ) end diff --git a/src/templates/utils.jl b/src/templates/utils.jl new file mode 100644 index 0000000..db03f63 --- /dev/null +++ b/src/templates/utils.jl @@ -0,0 +1,45 @@ +function _get_parameter_safe(p::String, parameters::Dict{String, Any}, default::Any = nothing) + haskey(parameters, p) || @critical "Trying to access (`get`) undefined parameter in `CoreTemplate`" parameter = p + return isnothing(default) ? parameters[p] : something(parameters[p], default) +end + +function _set_parameter_safe(p::String, v::Any, parameters::Dict{String, Any}) + haskey(parameters, p) || @critical "Trying to access (`set`) undefined parameter in `CoreTemplate`" parameter = p + parameters[p] = v + return nothing +end + +function _get_timeseries_safe(p_or_cf::String, parameters::Dict{String, Any}, model::JuMP.Model) + if !occursin("@", p_or_cf) + p_or_cf = _get_parameter_safe(p_or_cf, parameters)::String + end + + # Now we know, that `p_or_cf` is a "col@file" selector string. + column, file = string.(split(p_or_cf, "@")) + + return _getfromcsv(model, file, column) +end + +function _set_timeseries_safe(p_or_cf::String, v::Any, parameters::Dict{String, Any}, model::JuMP.Model) + if !occursin("@", p_or_cf) + p_or_cf = _get_parameter_safe(p_or_cf, parameters)::String + end + + # Now we know, that `p_or_cf` is a "col@file" selector string. + column, file = string.(split(p_or_cf, "@")) + + # Check if this file exists. + if haskey(_iesopt(model).input.files, file) + # This works for overwriting existing columns, as well as adding new ones. + _iesopt(model).input.files[file][!, column] .= v + else + _iesopt(model).input.files[file] = DataFrames.DataFrame(column => v) + end + + return nothing +end + +_is_template(filename::String) = endswith(filename, ".iesopt.template.yaml") +_get_template_name(filename::String) = string(rsplit(basename(filename), "."; limit=4)[1]) +_is_component(template::CoreTemplate) = template.type[] == :component +_is_container(template::CoreTemplate) = template.type[] == :container