Skip to content

Commit

Permalink
feat: each Virtual now keeps a reference to its template; CoreTemplat…
Browse files Browse the repository at this point in the history
…e now implements a proper summary extraction (and Base.show)
  • Loading branch information
Stefan Strömer committed Oct 25, 2024
1 parent 7702563 commit e9378ad
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 107 deletions.
2 changes: 2 additions & 0 deletions src/core.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions src/core/virtual.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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] =======================================================================================================
# -
Expand Down
7 changes: 0 additions & 7 deletions src/parser.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 20 additions & 0 deletions src/templates/definition.jl
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions src/templates/parse.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
131 changes: 31 additions & 100 deletions src/templates/templates.jl
Original file line number Diff line number Diff line change
@@ -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

Expand Down
45 changes: 45 additions & 0 deletions src/templates/utils.jl
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit e9378ad

Please sign in to comment.