Add a New or Custom Type
This page describes how developers should add types to PowerSystems.jl
Type Hierarchy
All structs that correlate to power system components must be subtypes of the Component
abstract type. Browse its type hierachy to choose an appropriate supertype for your new struct.
Interfaces
Refer to the managing components guide for component requirements.
In particular, please note the methods supports_time_series
(default = false) and supports_supplemental_attributes
(default = true) that you may need to implement.
Note: get_internal
and get_name
are imported into PowerSystems
, so you should implement your methods as PowerSystems
methods.
Some abstract types define required interface functions in docstring. Be sure to implement each of them for your new type.
Formalized documentation for each abstract type is TBD.
Specialize an Existing Type
There are scenarios where you may want to make a new type that is identical to an existing type except for one attribute or behavior, and don't want to duplicate the entire existing type and methods. In programming languages that support inheritance you would derive a new class from the existing class and automatically inherit its fields and methods. Julia doesn't support that. However, you can achieve a similar result with a forwarding macro. The basic idea is that you include the existing type within your struct and then use a macro to automatically forward specific methods to that instance.
A few PowerSystems structs use the macro InfrastructureSystems.@forward
to do this. Refer to the struct RoundRotorQuadratic
for an example of how to use this.
Custom Rules
Some types require special checks before they can be added to or removed from a system. One example is the case where a component includes another component that is also stored in the system. We must ensure that the parent component does not contain a reference to another component that is not already attached to the system.
Similarly, if the child object is removed from the system we must also remove the parent's reference to that child.
The source file src/base.jl
provides functions that you can implement for your new type to manage these scenarios.
check_component_addition(sys::System, component::Component; kwargs...)
handle_component_addition!(sys::System, component::Component; kwargs...)
check_component_removal(sys::System, component::Component; kwargs...)
handle_component_removal!(sys::System, component::Component; kwargs...)
The functions add_component!()
and remove_component!()
call the check function before performing actions and then call the handle function afterwards. The default behavior of these functions is to do nothing. Implement versions that take your type in order to add your own checks or perform additional actions.
Beware of the condition where a custom method is already implemented for a supertype of your type.
Note that you can call the helper functions is_attached(component, system)
and throw_if_not_attached(component, system)
.
Custom Validation
You can implement three methods to perform custom validation or correction for your type. PowerSystems calls all of these functions in add_component!
.
sanitize_component!(component::Component, sys::System)
: intended to make standard data corrections (e.g. voltage angle in degrees -> radians)validate_component(component::Component)
: intended to check component field values for internal consistencyvalidate_component_with_system(component::Component, sys::System)
: intended to check component field values for consistency with system
Struct Requirements for Serialization of custom components
One key feature of PowerSystems.jl
is the serialization capabilities. Supporting serialization and de-serialization of custom components requires the implementation of several methods. The serialization code converts structs to dictionaries where the struct fields become dictionary keys.
The code imposes these requirements:
- The InfrastructureSystems methods
serialize
anddeserialize
must be implemented for the struct. InfrastructureSystems implements a method that covers all subtypes ofInfrastructureSystemsType
. All PowerSystems components should be subtypes ofPowerSystems.Component
which is a subtypeInfrastructureSystemsType
, so any new structs should be covered as well. - All struct fields must be able to be encoded in JSON format or be covered be covered by
serialize
anddeserialize
methods. Basic types, such as numbers and strings or arrays and dictionaries of numbers and strings, should just work. Complex containers with symbols may not. - Structs relying on the default
deserialize
method must have a kwarg-only constructor. The deserialization code constructs objects by splatting the dictionary key/value pairs into the constructor. - Structs that contain other PowerSystem components (like a generator contains a bus) must serialize those components as UUIDs instead of actual values. The deserialization code uses the UUIDs as a mechanism to restore a reference to the actual object rather a new object with identical values. It also significantly reduces the size of the JSON file.
Refer to InfrastructureSystems.serialize_struct
for example behavior. New structs that are not subtypes of InfrastructureSystemsType
may be able to call it directly.
How to trouble-shoot serialization issues
Here are some examples of potential problems and solutions if you need to implement custom InfrastructureSystems.serialize
and InfrastructureSystems.deserialize
methods for your type.:
Problem: Your struct contains a field defined as an abstract type. The deserialization process doesn't know what concrete type to construct.
Solution: Encode the concrete type into the serialized dictionary as a string.
Example: serialize
and deserialize
methods for DynamicBranch
in src/models/dynamic_branch.jl
.
Problem: Similar to above in that a field is defined as an abstract type but the struct is parameterized on the actual concrete type.
Solution: Use the fact that the concrete type is encoded into the serialized type of the struct and extract it in a customized deserialze
method.
Example: deserialize
method for OuterControl
in src/models/OuterControl.jl
.
Adding PowerSystems.jl
as a dependency in a modeling package
module MyModelingModule
import PowerSystems
import InfrastructureSystems
const PSY = PowerSystems
const IS = InfrastructureSystems
export MyDevice
export get_name
mutable struct MyDevice <: PSY.Device
name::String
internal::IS.InfrastructureSystemsInternal
end
function MyDevice(name::String)
return MyDevice(name, IS.InfrastructureSystemsInternal())
end
PSY.get_name(val::MyDevice) = val.name
end
Auto-generating Structs
Most PowerSystems.jl
structs are auto-generated from the JSON descriptor file src/descriptors/power_system_structs.json
. You can add your new struct here or write it manually when contributing code to the repository.
If all you need is the basic struct definition and getter/setter functions then you will likely find the auto-generation helpful.
If you will need to write specialized functions for the type then you will probably want to write it manually.
Please refer to the docstrings for the functions generate_struct
and generate_structs
. Full details are in the InfrastructureSystems documentation at https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/devguide/autogeneration/.
Testing the addition of new struct to the code base
In order to merge new structs to the code base, your struct needs to pass several tests.
- addition to
System
- retrieval from
System
- serialization/de-serialization
The following code block is an example of the code that the new struct needs to pass
using PowerSystems
sys = System(100.0)
device = NewType(data)
# add your component to the system
add_component!(sys, device)
retrived_device = get_component(NewType, sys, "component_name")
# Serialize
to_json(sys, "sys.json")
# Re-create the system and find your component.
sys2 = System("sys.json")
serialized_device = get_component(NewType, sys, "component_name")
@test get_name(retrieved_device) == get_name(serialized_device)