How to contribute to GenX
Introduction
GenX is an open-source project, and we welcome contributions from the community. This guide aims to help you get started with GenX and explain how to contribute to the project. In general, the two main ways to contribute to GenX are:
- Use of GitHub issues to report bugs and request new features
- Use of GitHub pull requests (PR) to submit code changes
We encourage every contributors to read this guide, which contains some guidelines on how to contribute to a collaborative project like GenX.
The following sections describe in more detail how to work with GenX resources and how to add a new resource to GenX.
Style guide
GenX project follows the SciML Style Guide. We encourage contributors to follow this style guide when submitting code changes to GenX. Before submitting a new PR, please run the following command to format a file or a directory:
julia> using JuliaFormatter
julia> format("path_to_directory", SciMLStyle(), verbose=true)
or
julia> using JuliaFormatter
julia> format("path_to_file.jl", SciMLStyle(), verbose=true)
The GitHub repository of GenX is configured to verify the code style of each PR and will automatically provide comments to assist you in formatting the code according to the style guide.
GenX resources
In GenX, a resource is defined as an instance of a GenX resource type
, a subtype of an AbstractResource
. This allows the code to use multiple dispatch and define a common interface (behavior) for all resources in the code. Type hierarchy of GenX resources:
All the interface and utility functions available for resources are defined in the resources.jl file.
The set of all the resource types available in GenX are contained in the resource_types
Tuple
defined at the of the resources.jl
file. During the initialization process, GenX reads the input data files and creates a new instance of the corresponding resource type for each row in the file.
Resource design principles
Resources in GenX are constructed from a set of input files (in .csv
format, one for each type of resource) located in the resources
folder inside the case
folder. Each row in one of these files defines a new resource instance, and each column corresponds to an attribute of that resource type.
The first column of each input data file should be called Resource
and contain a unique identifier for each resource.
For example, in the case below, the files Hydro.csv
, Thermal.csv
, Vre.csv
, and Storage.csv
contain the resource data for the hydro, thermal, VRE, and storage resources, respectively. These files are read by GenX during the initialization process and used to create the corresponding resource instances.
case_folder
├── resources
│ ├── Hydro.csv
│ ├── Thermal.csv
│ ├── Vre.csv
│ └── Storage.csv
├── system
│ └── Generators_variability.csv
//
├── setting
│ └── genx_settings.yml
└── Run.jl
When loading the file Thermal.csv
below, GenX will create three new resources of type Thermal
and assign the values of the attributes of each resource from the columns in the input data file:
Thermal.csv
resource │ zone │ existing_cap_mw │ inv_cost_per_mwyr │ heat_rate_mmbtu_per_mwh │
String │ Int64 │ Float64 │ Float64 │ Float64 │
────────────────────-┼───────┼─────────────────┼───────────────────┼─────────────────────────│
NG_combined_cycle_1 │ 1 │ 100.0 │ 239841 │ 7.89 │
NG_combined_cycle_2 │ 2 │ 200.0 │ 0.0 │ 8.29 │
Biomass │ 3 │ 200.0 │ 81998 │ 9.9 │
These three resources, together with all the other resources in the data files, will be stored in the GenX inputs
dictionary with the key RESOURCES
.
julia> gen = inputs["RESOURCES"]
julia> length(gen) # returns the number of resources in the model
julia> thermal(gen) # returns the indices of all thermal resources (Vector{Int64})
julia> gen.Thermal # returns the thermal resources (Vector{Thermal})
julia> gen.Thermal == gen[thermal(gen)] # returns true
Working with GenX resources
To access the attributes of each resource, you can either use a function interface or the standard .
notation.
For example, let's assume that thermal_gen
is the vector of the three Thermal
resources created from the input data file Thermal.csv
shown above.
In the example below, we create the vector thermal_gen
manually. However, in practice, this vector is automatically created by GenX when loading the input data file Thermal.csv
.
julia> thermal_gen = [Thermal(Dict(:resource => "NG_combined_cycle_1",
:existing_cap_mw => 100.0,
:inv_cost_per_mwyr => 239841,
:heat_rate_mmbtu_per_mwh => 7.89,
:id => 23,
:max_cap_mw => 100.0,
:esr_1 => 1,)),
Thermal(Dict(:resource => "NG_combined_cycle_2",
:existing_cap_mw => 200.0,
:inv_cost_per_mwyr => 0.0,
:heat_rate_mmbtu_per_mwh => 8.29,
:max_cap_mw => 0,
:id => 24)),
Thermal(Dict(:resource => "Biomass",
:existing_cap_mw => 200.0,
:inv_cost_per_mwyr => 81998,
:heat_rate_mmbtu_per_mwh => 9.9,
:max_cap_mw => 0,
:lds => 1,
:new_build => 1,
:id => 25))];
To access the attributes of the resources in thermal_gen
, you can either use the function interfaces defined in resources.jl
(recommended), or you can use the standard .
notation:
julia> resource_name(thermal_gen[1])
"NG_combined_cycle_1"
julia> resource_name.(thermal_gen)
3-element Vector{String}:
"NG_combined_cycle_1"
"NG_combined_cycle_2"
"Biomass"
julia> existing_cap_mw(thermal_gen[1])
100.0
julia> existing_cap_mw.(thermal_gen)
3-element Vector{Float64}:
100.0
200.0
200.0
julia> thermal_gen[1].existing_cap_mw
100.0
Moreover, inside the resources.jl
file, there is a set of utility functions to work with all the resources and that can be used as building blocks to create more complex functions:
Base.get
: Returns the value of the attributesym
of the resourcer
. If the attribute is not defined for the resource, it returns thedefault
value of that attribute.
Example:
julia> get(thermal_gen[1], :existing_cap_mw, 0)
100.0
julia> get(thermal_gen[1], :new_build, 0)
0
Base.haskey
: Returnstrue
if the resourcer
has the attributesym
, andfalse
otherwise.
Example:
julia> haskey(thermal_gen[1], :existing_cap_mw)
true
julia> haskey(thermal_gen[1], :new_build)
false
Base.findall
: Returns the indices of the resources inrs
for which the functionf
returnstrue
.
Example:
julia> findall(r -> isa(r,Thermal), thermal_gen) # returns the indices of the thermal resources in thermal_gen
3-element Vector{Int64}:
23
24
25
julia> findall(r -> get(r, :lds, 0) > 0, thermal_gen) # returns the indices of the resources in thermal_gen that have a Long Duration Storage (lds) attribute greater than 0
1-element Vector{Int64}:
25
julia> findall(r -> get(r, :new_build, 0) == 1, thermal_gen) # returns the indices of the resources in thermal_gen that are buildable (new_build = 1)
1-element Vector{Int64}:
25
GenX.ids_with
: Returns the indices of the resources in the vectorrs
for which the functionf
is different fromdefault
.
Example:
julia> ids_with(thermal_gen, inv_cost_per_mwyr)
2-element Vector{Int64}:
23
25
A similar function works with Symbol
s and String
s instead of getter functions:
julia> ids_with(thermal_gen, :inv_cost_per_mwyr)
2-element Vector{Int64}:
23
25
GenX.ids_with_policy
: Returns the indices of the resources in the vectorrs
that have a policy with the namename
and the tagtag
.
Example:
julia> ids_with_policy(thermal_gen, esr, tag=1)
1-element Vector{Int64}:
23
GenX.ids_with_positive
: Returns the indices of the resources in the vectorrs
for which the getter functionf
returns a positive value.
Example:
julia> ids_with_positive(thermal_gen, inv_cost_per_mwyr)
2-element Vector{Int64}:
23
25
A similar function works with Symbol
s and String
s instead of getter functions:
julia> ids_with_positive(thermal_gen, :inv_cost_per_mwyr)
2-element Vector{Int64}:
23
25
GenX.ids_with_nonneg
: Returns the indices of the resources inrs
for which the getter functionf
returns a non-negative value.
Other useful functions available in GenX are:
GenX.resource_id
: Returns theid
of the resourcer
.
Example:
julia> resource_id(thermal_gen[1])
23
julia> resource_id.(thermal_gen)
3-element Vector{Int64}:
23
24
25
GenX.resource_name
: Returns thename
of the resourcer
.
Example:
julia> resource_name(thermal_gen[1])
"NG_combined_cycle_1"
julia> resource_name.(thermal_gen)
3-element Vector{String}:
"NG_combined_cycle_1"
"NG_combined_cycle_2"
"Biomass"
How to add a new resource to GenX
Overview
GenX is designed to be modular and highly flexible to comply with the rapidly changing electricity landscape. For this reason, adding a new resource to GenX is relatively straightforward. This guide will walk you through the steps to do it.
Before you start, ensure you have read the section of the documentation about 1.4 Resources input files. This will help you understand the data format that GenX expects for each resource and where to place the input data files.
Step 1: Define the new resource data type
The first step to add a new resource to GenX is to create a new GenX resource type. This is done by adding a new element to the resource_types
list of symbols defined at the top of the resources.jl file. This list contains the names of all the resource types available in GenX.
For example, to add a new resource type called new_resource
, you would need to add a new Symbol
, :NewResource
to the resource_types
list:
const resource_types = (:Thermal,
:Vre,
:Hydro,
:Storage,
:MustRun,
:FlexDemand,
:VreStorage,
:Electrolyzer,
:NewResource)
We encourage you to use CamelCase
for the name of the new resource type.
The lines right after the resource_types
list automatically create a new struct
(composite type) for the new resource type. More importantly, the new resource type will be defined as a subtype of the GenX AbstractResource
type. This is important because it allows the code to use multiple dispach and define a common interface (behavior) for all resources in GenX. For instance, the resource_id()
function will return the id
of any resource in GenX, regardless of its type (and therefore will automatically work for the newly created new_resource
).
Step 2: Add the filename of the new resource type to GenX
In GenX, the attributes of a resource are automatically defined from the columns of the corresponding input data file (e.g., Thermal.csv
file for the Thermal
resources, Hydro.csv
file for the Hydro
resource, etc). The first column of these files should be called Resource
and contain a unique identifier for each resource. The rest of the columns in the input data file will be used to define the attributes of the new resource type.
So, the second step to add a new resource type to GenX is to add the filename of the input data file to GenX. The list of input data files that GenX loads during the initialization process are defined at the top of the load_resource_data.jl
file, inside an internal function called _get_resource_info()
. This function returns a NamedTuple
called resource_info
with the name of the input data file and the name of the resource type for each resource that is available in GenX.
To add the new resource type to GenX, add a new item to resource_info
, where the first field is the name of the input data file and the second is the name of the resource type that was created in Step 1. The names in resource_info
are only used to make the code more readable and are arbitrary.
For example, if you are adding a new resource type called new_resource
, you would need to add the following line to the resource_info
: new_resource = (filename="New_resource.csv", type=NewResource)
, as follows:
function _get_resource_info()
resource_info = (
hydro = (filename="Hydro.csv", type=Hydro),
thermal = (filename="Thermal.csv", type=Thermal),
vre = (filename="Vre.csv", type=Vre),
storage = (filename="Storage.csv", type=Storage),
flex_demand = (filename="Flex_demand.csv", type=FlexDemand),
must_run = (filename="Must_run.csv", type=MustRun),
electrolyzer = (filename="Electrolyzer.csv", type=Electrolyzer),
vre_stor = (filename="Vre_stor.csv", type=VreStorage)
new_resource = (filename="New_resource.csv", type=NewResource)
)
return resource_info
end
With this simple edit, whenever the file New_resource.csv
is found in the input data folder, GenX will automatically perform the following steps:
- Load the new resource input file,
- Create a new instance of the
NewResource
type for each row in the input data file, - Define the attributes of each
NewResource
from the columns in the input data file, - Populate the attributes of each
NewResource
with the values read from the input data file. - Add the new resources to the vector of resources in the model.
For example, if the input data file New_resource.csv
contains the following data:
New_resource.csv
Resource │ Zone | Exisiting_capacity | attribute_1 | attribute_2
String │ Int64 | Float64 | Float64 | Float64
──────────┼───────┼────────────────────┼─────────────┼────────────
new_res1 │ 1 │ 100.0 │ 6.2 │ 0.4
new_res2 │ 1 │ 200.0 │ 0.1 │ 4.0
new_res3 │ 2 │ 300.0 │ 2.0 │ 0.1
GenX will create three new resources of type NewResource
with the following attributes:
resource
:String
with the name of the resource (e.g.,new_res1
,new_res2
,new_res3
)zone
:Int64
with the zone of the resource (e.g.,1
,1
,2
)existing_capacity
:Float64
with the existing capacity of the resource (e.g.,100.0
,200.0
,300.0
)attribute_1
:Float64
with the value ofattribute_1
(e.g.,6.2
,0.1
,2.0
)attribute_2
:Float64
with the value ofattribute_2
(e.g.,0.4
,4.0
,0.1
)
See Step 3 for more details on how to work with the new resource type.
Each resource type must contain a Resource
attribute. This attribute should be String
that uniquely identifies the resource.
Step 3: Work with the new resource type
Once the new resource type has been defined and added to GenX, you can work with it as you would with any other resource type. To improve the robustness and readability of the code, we recommend that you define getter functions for the new attributes of the new resource type (e.g., a function zone(r) = r.zone
to get the zone of the resource r
). These functions can be defined in the resources.jl
file. However, this is not strictly necessary, and you can access the attributes of the new resource type directly using the standard .
notation:
To simplify the creation of getter functions for the new resource type, you can use the @interface
macro available in GenX. This macro automatically creates a new function with the same name as the attribute and which returns the value of the attribute. For example, if you want to create a getter function for the attribute_1
of the NewResource
type, these two ways are equivalent:
julia> default_attribute_1 = 0.0 # default value for attribute_1
julia> attribute_1(res::NewResource) = get(res, :attribute_1, default_attribute_1)
attribute_1 (generic function with 1 method)
julia> @interface(attribute_1, 0.0, NewResource)
attribute_1 (generic function with 1 method)
And then:
julia> attribute_1(new_res1)
6.2
julia> new_res1.attribute_1
6.2
Utility functions to work with JuMP expressions in GenX
GenX.add_similar_to_expression!
— Methodadd_similar_to_expression!(expr1::AbstractArray{GenericAffExpr{C,T}, dim1}, expr2::AbstractArray{V, dim2}) where {C,T,V,dim1,dim2}
Add an array of some type V
to an array of expressions, in-place. This will work on JuMP DenseContainers which do not have linear indexing from 1:length(arr). However, the accessed parts of both arrays must have the same dimensions.
GenX.add_term_to_expression!
— Methodadd_term_to_expression!(expr1::AbstractArray{GenericAffExpr{C,T}, dims}, expr2::V) where {C,T,V,dims}
Add an entry of type V
to an array of expressions, in-place. This will work on JuMP DenseContainers which do not have linear indexing from 1:length(arr).
GenX.check_addable_to_expr
— Methodcheck_addable_to_expr(C::DataType, T::DataType)
Check that two datatype can be added using addtoexpression!(). Raises an error if not.
This needs some work to make it more flexible. Right now it's challenging to use with GenericAffExpr{C,T} as the method only works on the constituent types making up the GenericAffExpr, not the resulting expression type. Also, the default MethodError from addtoexpression! is sometime more informative than the error message here.
GenX.check_sizes_match
— Methodcheck_sizes_match(expr1::AbstractArray{C, dim1}, expr2::AbstractArray{T, dim2}) where {C,T,dim1, dim2}
Check that two arrays have the same dimensions. If not, return an error message which includes the dimensions of both arrays.
GenX.create_empty_expression!
— Methodcreate_empty_expression!(EP::Model, exprname::Symbol, dims::NTuple{N, Int64}) where N
Create an dense array filled with zeros which can be altered later. Other approaches to creating zero-filled arrays will often return an array of floats, not expressions. This can lead to errors later if a method can only operate on expressions.
We don't currently have a method to do this with non-contiguous indexing.
GenX.fill_with_const!
— Methodfill_with_const!(arr::AbstractArray{GenericAffExpr{C,T}, dims}, con::Real) where {C,T,dims}
Fill an array of expressions with the specified constant, in-place.
In the future we could expand this to non AffExpr, using GenericAffExpr e.g. if we wanted to use Float32 instead of Float64
GenX.fill_with_zeros!
— Methodfill_with_zeros!(arr::AbstractArray{GenericAffExpr{C,T}, dims}) where {C,T,dims}
Fill an array of expressions with zeros in-place.
GenX.sum_expression
— Methodsum_expression(expr::AbstractArray{C, dims}) where {C,dims} :: C
Sum an array of expressions into a single expression and return the result. We're using errors from addtoexpression!() to check that the types are compatible.