A Julia package for parsing PSS/E DYR (dynamics data) files with metadata-driven field naming.
Documentation | Model Library | API Reference
- Metadata-driven parsing: YAML schemas define model structure with field names, types, units, and descriptions
- Automatic metadata loading: Bundled metadata for common PSS/E models loads by default
- Dual-mode operation: Works with or without metadata (named fields vs indexed fields)
- Validation tracking: Out-of-range values are preserved and tracked for user inspection
- CIM-style metadata: Rich field metadata including descriptions, units, types, and valid ranges
- Tables.jl integration: Seamless conversion to DataFrames
- Flexible format handling: Supports standard and user-defined PSS/E dynamic models
- Robust parsing: Handles multi-line records, scientific notation, comments, and edge cases
using Pkg
Pkg.add("PowerDynData")using PowerDynData
using DataFrames
# Parse DYR file - metadata loads automatically
dd = parse_dyr("case.dyr")
# Access models with named fields
genrou_df = DataFrame(dd["GENROU"])
tgov1_df = DataFrame(dd["TGOV1"])
# Inspect available models
keys(dd) # => ["GENROU", "ESST3A", "TGOV1", "GENCLS", ...]PowerDynData also supports a TOML-based format as an alternative to DYR. TOML provides self-documenting data with explicit field names, making it easier to read and version control.
# Parse TOML file - same API as parse_dyr
dd = parse_toml("case.toml")
# Access models identically to DYR
genrou_df = DataFrame(dd["GENROU"])# Convert existing DYR file to TOML
dyr_to_toml("case.dyr", "case.toml")DYR format (positional fields):
1 'GENCLS' 1 3.0 0.0 /
TOML format (named fields):
[[GENCLS]]
BUS = 1
ID = "1"
H = 3.0
D = 0.0In DYR, fields are identified by position. In TOML, each field is explicit with name = value. Use [[MODEL]] (double brackets) to create an array - each [[MODEL]] block adds one device instance.
# IEEE 14-bus dynamic data
# Each model instance uses [[MODEL_NAME]] syntax
[[GENROU]]
BUS = 1
ID = "1"
H = 4.0 # Inertia constant (MW·s/MVA)
D = 0.0 # Damping coefficient
Xd = 1.8 # Direct axis synchronous reactance
Xd1 = 0.6 # Direct axis transient reactance
Td10 = 6.5 # D-axis transient time constant
# ... additional fields
[[GENROU]]
BUS = 2
ID = "1"
H = 6.5
# ...
[[TGOV1]]
BUS = 1
ID = "1"
R = 0.05 # Permanent droop
T1 = 1.0 # Governor time constant
Vmax = 1.05 # Maximum valve position
Vmin = 0.3 # Minimum valve position| Feature | DYR | TOML |
|---|---|---|
| Field identification | By position | By name |
| Comments | Limited | Full support (#) |
| Readability | Requires metadata reference | Self-documenting |
| Version control | Difficult diffs | Clean diffs |
| Type safety | All text | Native types |
Field names in TOML must match the metadata YAML definitions exactly (e.g., H, Xd, Td10).
Parse a PSS/E DYR file into structured data.
Arguments:
source: Path to DYR file or IO objectmetadata_dir: Path to metadata directory (default: bundled metadata). Set tonothingto disable metadata.
Returns: DynamicData container with all parsed models
dd = parse_dyr("case.dyr")Parse a TOML file into structured data. Same return type and API as parse_dyr.
dd = parse_toml("case.toml")Convert a DYR file to TOML format.
dyr_to_toml("case.dyr", "case.toml")Top-level container for all dynamic data from a DYR file.
Fields:
models::Dict{String, DynamicRecords}- Dictionary of model records by namemetadata_registry::Union{MetadataRegistry, Nothing}- Loaded metadata (if any)source_file::String- Path to source DYR filevalidation_issues::Vector{ValidationIssue}- Validation problems encountered during parsing
Accessors:
dd["GENROU"] # Get model by name
keys(dd) # List all model names
haskey(dd, "GENROU") # Check if model exists
length(dd) # Number of model typesDynamic records with metadata-driven named fields.
Fields:
model_name::String- Model name (e.g., "GENROU")category::String- Model category (e.g., "generator")data::StructArray- Column-oriented data with named fields
Usage:
genrou = dd["GENROU"]
genrou isa NamedDynamicRecords # true
# Access as StructArray
genrou.data.H # Inertia constant for all units
genrou.data.BUS # Bus numbers for all units
# Convert to DataFrame
df = DataFrame(genrou)Dynamic records without metadata (fallback mode).
Fields:
model_name::String- Model namefields::Vector{Vector{Any}}- Raw field data as vectors
Usage:
# When metadata not available for a model
custom = dd["CustomModel"]
custom isa IndexedDynamicRecords # true
# Access raw fields
custom.fields[1] # First record's fieldsRepresents a validation problem encountered during parsing.
Fields:
model_name::String- Model where issue occurredrecord_index::Int- Record index (1-based) within the modelfield_name::Symbol- Field name where issue occurredissue_type::Symbol- Type of issue (:out_of_range,:parse_error)message::String- Human-readable descriptionvalue::Any- The actual value that caused the issue
Issue Types:
:out_of_range- Value parsed successfully but outside valid range (value is still stored):parse_error- Failed to parse the field value (default value used instead)
Usage:
# Check for validation issues
println(dd) # Shows issue count in summary
# Inspect specific issues
for issue in dd.validation_issues
if issue.issue_type == :out_of_range
println("$(issue.model_name)[$(issue.record_index)].$(issue.field_name) = $(issue.value)")
println(" $(issue.message)")
end
end
# Group by model
by_model = Dict{String, Int}()
for issue in dd.validation_issues
by_model[issue.model_name] = get(by_model, issue.model_name, 0) + 1
endLoad YAML metadata schemas from a directory.
# Load bundled metadata
registry = load_metadata_registry(pkgdir(PowerDynData, "metadata"))
# Inspect loaded models
keys(registry.models) # => ["GENROU", "GENCLS", "TGOV1", ...]
# Get metadata for a specific model
genrou_meta = registry.models["GENROU"]
println(genrou_meta.description) # "Round rotor generator model"
println(genrou_meta.category) # "generator"
# Inspect field metadata
for field in genrou_meta.fields
println("$(field.name): $(field.description) [$(field.unit)]")
endusing PowerDynData
using DataFrames
# Parse DYR file
dd = parse_dyr("ieee14.dyr")
# Display overview
println(dd)
# Output:
# DynamicData from: ieee14.dyr
# Models: ESST3A, EXST1, GENROU, IEEEG1, IEEEST, ST2CUT, TGOV1
# Metadata: loaded (34 model schemas)
# Validation issues: 35 total (35 out-of-range)
# Convert GENROU to DataFrame
genrou_df = DataFrame(dd["GENROU"])
# Work with data
println(nrow(genrou_df)) # Number of GENROU units
println(names(genrou_df)) # Column names
println(genrou_df.H) # Inertia constants
println(genrou_df[1, :]) # First recordusing PowerDynData
dd = parse_dyr("case.dyr")
# Check if there are validation issues
if !isempty(dd.validation_issues)
println("Found $(length(dd.validation_issues)) validation issues")
# Group by model
by_model = Dict{String, Int}()
for issue in dd.validation_issues
by_model[issue.model_name] = get(by_model, issue.model_name, 0) + 1
end
println("\nIssues by model:")
for (model, count) in sort(collect(by_model))
println(" $model: $count issues")
end
# Show first 5 out-of-range issues
println("\nFirst 5 out-of-range issues:")
count = 0
for issue in dd.validation_issues
if issue.issue_type == :out_of_range && count < 5
println(" $(issue.model_name)[$(issue.record_index)].$(issue.field_name):")
println(" Value: $(issue.value)")
println(" $(issue.message)")
count += 1
end
end
# Note: Out-of-range values are still in the data!
esst3a = dd["ESST3A"]
df = DataFrame(esst3a)
println("\nActual VRMAX values (including out-of-range):")
println(df.VRMAX) # [99.0, 99.0, ...] - actual values preserved
endusing PowerDynData
# Disable metadata to get indexed fields
dd = parse_dyr("case.dyr", metadata_dir=nothing)
# Models use IndexedDynamicRecords
genrou = dd["GENROU"]
genrou isa IndexedDynamicRecords # true
# Access raw fields
println("Number of records: $(length(genrou))")
println("First record fields: $(genrou.fields[1])")
println("Field 2 of first record: $(genrou.fields[1][2])") # Model name
println("Field 8 of first record: $(genrou.fields[1][8])") # H (inertia)using PowerDynData
# Create custom metadata directory
# my_metadata/
# custom_models/
# MYMODEL.yaml
# Parse with custom metadata
dd = parse_dyr("case.dyr", metadata_dir="my_metadata")
# Models with custom metadata get named fields
mymodel = dd["MYMODEL"]
mymodel isa NamedDynamicRecords # true
# Models without metadata fall back to indexed
other = dd["OtherModel"]
other isa IndexedDynamicRecords # true (no metadata found)PowerDynData uses YAML files to define model schemas with CIM-style metadata.
metadata/generators/GENROU.yaml
model:
name: GENROU
description: Round rotor generator model
category: generator
version: 33
parsing:
model_name_field: 2 # Field position of model name
multi_line: true # Spans multiple lines
line_count: 3 # Number of lines
terminator: "/" # End-of-record marker
fields:
- name: BUS
position: 1
type: Int
description: "Bus number where generator is connected"
unit: dimensionless
required: true
- name: H
position: 8
type: Float64
description: "Inertia constant"
unit: "MW·s/MVA"
required: true
range: [0.0, Inf]
# ... more fields- Create a YAML file in the appropriate category folder (e.g.,
metadata/generators/) - Define model properties, parsing rules, and field metadata
- Include descriptions, units, types, and validation ranges
- Use the metadata with
parse_dyr(..., metadata_dir="metadata")
See the plan document for complete metadata schema specification.
PowerDynData includes metadata for common PSS/E models:
Generators:
- GENROU, GENSAL, GENCLS
Exciters:
- ESST3A, ESST1A, ESST4B, EXST1, EXAC1, EXAC2, EXAC4, EXDC2, ESAC1A, ESDC1A, ESDC2A, IEEET1, IEEEX1, AC8B, SEXS
Governors:
- TGOV1, IEEEG1
Stabilizers:
- IEEEST, ST2CUT
PowerDynData/
├── src/
│ ├── PowerDynData.jl # Main module
│ ├── types.jl # Data structures
│ ├── metadata.jl # YAML schema handling
│ ├── parsing.jl # Core parsing logic
│ ├── debug.jl # Debug utilities
│ └── precompile.jl # Precompilation workload
├── metadata/ # YAML schemas by category
│ ├── generators/
│ ├── exciters/
│ ├── governors/
│ └── stabilizers/
├── test/
│ ├── runtests.jl
│ └── testfiles/
└── docs/
Phase 1: Infrastructure ✓
- Package scaffolding with PkgTemplates
- Type system (DynamicData, DynamicRecords)
- Metadata system (YAML loading, validation)
- Full DYR file parser implementation
- Multi-line record handling
- Validation issue tracking
- Automatic metadata loading
- Example metadata files for common models
- Test framework
- All tests passing (62/62)
Phase 2: Enhancement (Next)
- Performance optimization
- Extended metadata library (more PSS/E models)
- Documentation site
- CI/CD workflows
- Metadata Layer: YAML schemas define structure
- Parsing Layer: Flexible parser handles various formats
- Data Layer: Column-oriented storage with Tables.jl
- Metadata-driven: Easy to add new models without code changes
- Automatic fallback: Robust parsing even without metadata for specific models
- Validation tracking: Out-of-range values preserved, issues tracked for inspection
- Tables.jl integration: Zero-copy DataFrame conversion
- StructArrays: Memory-efficient column-oriented storage
Contributions welcome! Areas of interest:
- Metadata expansion: Add YAML schemas for more PSS/E models
- Testing: Add more edge case tests
- Documentation: User guides and examples
- Performance: Optimize parsing for large files
- PowerFlowData.jl: PSS/E RAW file parser
- Powerful.jl: Power systems simulation package
- PowerSystems.jl: PowerSystems Data Handling Package
MIT License - see LICENSE file for details.
Inspired by:
- PowerFlowData.jl's efficient parsing architecture
- ANDES's YAML-based metadata system
- CIM (Common Information Model) metadata standards