GridCalEngine

GridCal is a Power Systems simulation program intended for professional use and research


Keywords
power, systems, planning, acdc, cim, comon-information-model, electrical, electrical-engineering, helm, latin-hypercube, monte-carlo-simulation, multi-terminal, newton-raphson, optimization, power-systems, powerflow, python, stochastic-power-flow
License
MPL-2.0
Install
pip install GridCalEngine==5.0.0a26

Documentation

GridCal

GridCal is a top tier power systems planning and simulation software. As such it has all the static analysis studies that you can think of, plus linear and non-linear optimization functions. Some of these functions are well known, while others you may have never heard of as they are a product of cutting-edge research.

GridCal

Codacy Badge Documentation Status Build Status DOI Downloads

GridCal started in 2015 with a clear objective: create a solid programming library and a user-friendly interface. This straightforward approach sparked many innovations — some driven by the necessity for commercial use, and others fueled by curiosity and research.

Whether you're a pro needing free tools, a researcher wanting a real-world tested platform, a teacher sharing commercial-grade software insights, or a student diving into practical algorithms, GridCal's got your back. It's a high quality product made for all of us now and for the future generations.

Installation

GridCal is a software made in the Python programming language. Therefore, it needs a Python interpreter installed in your operative system.

The GridCal project is divided in three packages:

  • GridCalEngine: A package with the database and calculations logic.
  • GridCalServer: A package that serves an API-rest to use GridCalEngine remotelly.
  • GridCal: A package that contains the Graphical User Interface (GUI) and operates with GridCalEngine and GridCalServer seamlessly.

To install everything, you only need to install the GridCal package and the others will beinstalled as dependencies.

Standalone setup

If you don't know what is this Python thing, we offer a windows installation:

Windows setup

This will install GridCal as a normal windows program and you need not to worry about any of the previous instructions. Still, if you need some guidance, the following video might be of assistance: Setup tutorial (video).

Package installation

We recommend to install the latest version of Python and then, install GridCal with the following terminal command:

pip install GridCal

You may need to use pip3 if you are under Linux or MacOS, both of which come with Python pre-installed already.

Install into an environment

python3 -m venv gc5venv
source gc5venv/bin/activate
pip install GridCal
gridcal

Run the graphical user interface

Once you install GridCal in your local Python distribution, you can run the graphical user interface with the following terminal command:

gridcal

If this doesn't work, try:

python -c "from GridCal.ExecuteGridCal import runGridCal; runGridCal()"

You may save this command in a shortcut for easy future access.

Install only the engine

Some of you may only need GridCal as a library for some other purpose like batch calculations, AI training or simple scripting. Whatever it may be, you can get the GridCal engine with the following terminal command:

pip install GridCalEngine

This will install the GridCalEngine package that is a dependency of GridCal.

Again, you may need to use pip3 if you are under Linux or MacOS.

Features

GridCal is packed with feautures:

  • Large collection of devices to model electricity grids
  • AC/DC multi-grid power flow
  • AC/DC multi-grid linear optimal power flow
  • AC linear analysis (PTDF & LODF)
  • AC linear net transfer capacity calculation
  • AC+HVDC optimal net transfer capacity calculation
  • AC/DC Stochastic power flow
  • AC Short circuit
  • AC Continuation power flow
  • Contingency analysis (Power flow and LODF variants)
  • Sigma analysis (one-shot stability analysis)
  • Investments analysis
  • Bus-branch schematic
  • Substation-line map diagram
  • Time series and snapshot for most simulations
  • Overhead tower designer
  • Inputs analysis
  • Model bug report and repair
  • Import many formats (PSSe .raw/rawx, epc, dgs, matpower, pypsa, json, cim, cgmes)
  • Export in many formats (gridcal .xlsx/.gridcal/.json, cgmes, psse .raw/.rawx)

All of these are industry tested algoriths, some of which surpass most comemercially available software. The aim is to be a drop-in replacement for the expensive and less usable commercial software, so that you can work, research and learn with it.

Resources

In an effort to ease the simulation and construction of grids, We have included extra materials to work with. These are included in the standalone setups.

Tutorials and examples

API

Since day one, GridCal was meant to be used as a library as much as it was meant to be used from the user interface. Following, we include some usage examples, but feel free to check the documentation out where you will find a complete description of the theory, the models and the objects.

Understanding the program structure

All simulations in GridCal are handled by the simulation drivers. The structure is as follows:

Any driver is fed with the data model (MultiCircuit object), the respective driver options, and often another object relative to specific inputs for that driver. The driver is run, storing the driver results object. Although this may seem overly complicated, it has proven to be maintainable and very convenient.

Snapshot vs. time series

GridCal has dual structure to handle legacy cases (snapshot), as well as cases with many variations (time series)

  • A snapshot is the grid for a particular moment in time. This includes the infrastructure plus the variable values of that infraestructure such as the load, the generation, the rating, etc.

  • The time series record the variations of the magnitudes that can vary. These are aplied along with the infrastructure definition.

In GridCal, the inputs do not get modified by the simulation results. This very important concept, helps maintaining the independence of the inputs and outputs, allowing the replicability of the results. This key feature is not true for other open-source of comercial programs.

A snapshot or any point of the time series, may be compiled to a NumericalCircuit. This object holds the numerical arrays and matrices of a time step, ready for the numerical methods. For those simulations that require many time steps, a collection of NumericalCircuit is compiled and used.

It may seem that this extra step is redundant. However the compilation step is composed by mere copy operations, which are fast. This steps benefits greatly the efficiency of the numerical calculations since the arrays are aligned in memory. The GridCal data model is object-oriented, while the numerical circuit is array-oriented (despite beign packed into objects)

Loading a grid

import GridCalEngine as gce

# load a grid (.gridcal, .m (Matpower), .raw (PSS/e) .rawx (PSS/e), .epc (PowerWorld), .dgs (PowerFactory)
my_grid = gce.open_file("my_file.gridcal")

In the case of CIM/CGMES, you may need to pass a list of files or a single zip file:

import GridCalEngine as gce

# load a grid from many xml files
my_grid = gce.open_file(["grid_EQ.xml", "grid_TP.xml", "grid_SV.xml", ])

# or from a single zip assumed to contain CGMES files
my_grid = gce.open_file("my_cgmes_set_of_files.zip")

# or load a grid from a combination of xml and zip files assumed to be CGMES
my_grid = gce.open_file(["grid_EQ.xml", "grid_TP.xml", "grid_SV.xml", "boundary.zip"])

If you need to explore the CGMEs assets before conversion, you'll need to dive deeper in the API:

import GridCalEngine as gce

fname = "tests/data/grids/CGMES_2_4_15/IEEE 118 Bus v2.zip"

logger = gce.Logger()
data_parser = gce.CgmesDataParser()
data_parser.load_files(files=[fname])
cgmes_circuit = gce.CgmesCircuit(cgmes_version=data_parser.cgmes_version,
                             cgmes_map_areas_like_raw=False, logger=logger)
cgmes_circuit.parse_files(data_parser=data_parser)

# print all the ac line segment names
for ac_line_segment in cgmes_circuit.cgmes_assets.ACLineSegment_list:
    print(ac_line_segment.name)

# print the logs
logger.print()

GridCal supports many file formats:

  • CIM 16 (.zip and .xml)
  • CGMES 2.4.15 and 3.0 (.zip and .xml)
  • PSS/e raw and rawx versions 29 to 35, including USA market exchange RAW-30 specifics.
  • Matpower .m files directly.
  • DigSilent .DGS (not fully compatible)
  • PowerWorld .EPC (not fully compatible, supports substation coordinates)

Simmilarly to CGMES you may be able to use the conversion objects to explore the original formats.

Save a grid

import GridCalEngine as gce

# load a grid
my_grid = gce.open_file("my_file.gridcal")

# save
gce.save_file(my_grid, "my_file_2.gridcal")

In the case of saving a model in CGMES mode, we need to specify some extra parameters. To simplify we can use the API function save_cgmes_file:

import GridCalEngine as gce

# load a grid
my_grid = gce.open_file("my_file.gridcal")

# run power flow (this is optional and it is used to generate the SV profile)
pf_results = gce.power_flow(my_grid)

# save the grid in CGMES mode
gce.save_cgmes_file(grid=my_grid,
                    filename="My_cgmes_model.zip",
                    cgmes_boundary_set_path="path_to_the_boundary_set.zip",
                    cgmes_version=CGMESVersions.v2_4_15,
                    pf_results=pf_results)

Creating a Grid using the API objects

We are going to create a very simple 5-node grid from the excellent book Power System Load Flow Analysis by Lynn Powell.

import GridCalEngine as gce

# declare a circuit object
grid = gce.MultiCircuit()

# Add the buses and the generators and loads attached
bus1 = gce.Bus('Bus 1', Vnom=20)
# bus1.is_slack = True  # we may mark the bus a slack
grid.add_bus(bus1)

# add a generator to the bus 1
gen1 = gce.Generator('Slack Generator', vset=1.0)
grid.add_generator(bus1, gen1)

# add bus 2 with a load attached
bus2 = gce.Bus('Bus 2', Vnom=20)
grid.add_bus(bus2)
grid.add_load(bus2, gce.Load('load 2', P=40, Q=20))

# add bus 3 with a load attached
bus3 = gce.Bus('Bus 3', Vnom=20)
grid.add_bus(bus3)
grid.add_load(bus3, gce.Load('load 3', P=25, Q=15))

# add bus 4 with a load attached
bus4 = gce.Bus('Bus 4', Vnom=20)
grid.add_bus(bus4)
grid.add_load(bus4, gce.Load('load 4', P=40, Q=20))

# add bus 5 with a load attached
bus5 = gce.Bus('Bus 5', Vnom=20)
grid.add_bus(bus5)
grid.add_load(bus5, gce.Load('load 5', P=50, Q=20))

# add Lines connecting the buses
grid.add_line(gce.Line(bus1, bus2, name='line 1-2', r=0.05, x=0.11, b=0.02))
grid.add_line(gce.Line(bus1, bus3, name='line 1-3', r=0.05, x=0.11, b=0.02))
grid.add_line(gce.Line(bus1, bus5, name='line 1-5', r=0.03, x=0.08, b=0.02))
grid.add_line(gce.Line(bus2, bus3, name='line 2-3', r=0.04, x=0.09, b=0.02))
grid.add_line(gce.Line(bus2, bus5, name='line 2-5', r=0.04, x=0.09, b=0.02))
grid.add_line(gce.Line(bus3, bus4, name='line 3-4', r=0.06, x=0.13, b=0.03))
grid.add_line(gce.Line(bus4, bus5, name='line 4-5', r=0.04, x=0.09, b=0.02))

Power Flow

Using the simplified API:

import os
import GridCalEngine as gce

folder = os.path.join('..', 'Grids_and_profiles', 'grids')
fname = os.path.join(folder, 'IEEE39_1W.gridcal')
main_circuit = gce.open_file(fname)

results = gce.power_flow(main_circuit)

print(main_circuit.name)
print('Converged:', results.converged, 'error:', results.error)
print(results.get_bus_df())
print(results.get_branch_df())

Using the more complex library objects:

import os
import GridCalEngine as gce

folder = os.path.join('..', 'Grids_and_profiles', 'grids')
fname = os.path.join(folder, 'IEEE14_from_raw.gridcal')
main_circuit = gce.open_file(fname)

options = gce.PowerFlowOptions(gce.SolverType.NR, verbose=False)
power_flow = gce.PowerFlowDriver(main_circuit, options)
power_flow.run()

print(main_circuit.name)
print('Converged:', power_flow.results.converged, 'error:', power_flow.results.error)
print(power_flow.results.get_bus_df())
print(power_flow.results.get_branch_df())

Output:

IEEE14_from_raw

Converged: True error: 5.98e-08

Bus resuts:
           Vm     Va      P      Q
BUS 1    1.06   0.00 232.39 -16.55
BUS 2    1.04  -4.98  18.30  30.86
BUS 3    1.01 -12.73 -94.20   6.08
BUS 4    1.02 -10.31 -47.80   3.90
BUS 5    1.02  -8.77  -7.60  -1.60
BUS 6    1.07 -14.22 -11.20   5.23
BUS 7    1.06 -13.36   0.00   0.00
BUS 8    1.09 -13.36   0.00  17.62
BUS 9    1.06 -14.94 -29.50 -16.60
BUS 10   1.05 -15.10  -9.00  -5.80
BUS 11   1.06 -14.79  -3.50  -1.80
BUS 12   1.06 -15.08  -6.10  -1.60
BUS 13   1.05 -15.16 -13.50  -5.80
BUS 14   1.04 -16.03 -14.90  -5.00

Branch results:
            Pf     Qf      Pt     Qt               loading
1_2_1   156.88 -20.40 -152.59  27.68 -2,040,429,074,673.33
1_5_1    75.51   3.85  -72.75   2.23    385,498,944,321.99
2_3_1    73.24   3.56  -70.91   1.60    356,020,306,394.25
2_4_1    56.13  -1.55  -54.45   3.02   -155,035,233,483.95
2_5_1    41.52   1.17  -40.61  -2.10    117,099,586,051.68
3_4_1   -23.29   4.47   23.66  -4.84    447,311,351,720.93
4_5_1   -61.16  15.82   61.67 -14.20  1,582,364,180,487.11
6_11_1    7.35   3.56   -7.30  -3.44    356,047,085,671.01
6_12_1    7.79   2.50   -7.71  -2.35    250,341,387,213.42
6_13_1   17.75   7.22  -17.54  -6.80    721,657,405,311.13
7_8_1    -0.00 -17.16    0.00  17.62 -1,716,296,745,837.05
7_9_1    28.07   5.78  -28.07  -4.98    577,869,015,291.12
9_10_1    5.23   4.22   -5.21  -4.18    421,913,877,670.92
9_14_1    9.43   3.61   -9.31  -3.36    361,000,694,981.35
10_11_1  -3.79  -1.62    3.80   1.64   -161,506,127,162.22
12_13_1   1.61   0.75   -1.61  -0.75     75,395,885,855.71
13_14_1   5.64   1.75   -5.59  -1.64    174,717,248,747.17
4_7_1    28.07  -9.68  -28.07  11.38   -968,106,634,094.39
4_9_1    16.08  -0.43  -16.08   1.73    -42,761,145,748.20
5_6_1    44.09  12.47  -44.09  -8.05  1,247,068,151,943.25

Inputs analysis

GridCal can perform a summary of the inputs with the InputsAnalysisDriver:

import os
import GridCalEngine as gce

folder = os.path.join('..', 'Grids_and_profiles', 'grids')
fname = os.path.join(folder, 'IEEE 118 Bus - ntc_areas.gridcal')

main_circuit = gce.open_file(fname)

drv = gce.InputsAnalysisDriver(grid=main_circuit)
mdl = drv.results.mdl(gce.ResultTypes.AreaAnalysis)
df = mdl.to_df()

print(df)

The results per area:

               P    Pgen   Pload  Pbatt  Pstagen      Pmin      Pmax      Q    Qmin    Qmax
IEEE118-3  -57.0   906.0   963.0    0.0      0.0 -150000.0  150000.0 -345.0 -2595.0  3071.0
IEEE118-2 -117.0  1369.0  1486.0    0.0      0.0 -140000.0  140000.0 -477.0 -1431.0  2196.0
IEEE118-1  174.0  1967.0  1793.0    0.0      0.0 -250000.0  250000.0 -616.0 -3319.0  6510.0

Linear analysis

We can run an PTDF equivalent of the power flow with the linear analysys drivers:

import os
import GridCalEngine as gce

folder = os.path.join('..', 'Grids_and_profiles', 'grids')
fname = os.path.join(folder, 'IEEE 5 Bus.xlsx')

main_circuit = gce.open_file(fname)

# snapshot
results = gce.linear_power_flow(grid=main_circuit)

print("Bus results:\n", results.get_bus_df())
print("Branch results:\n", results.get_branch_df())
print("PTDF:\n", results.mdl(gce.ResultTypes.PTDF).to_df())
print("LODF:\n", results.mdl(gce.ResultTypes.LODF).to_df())

Simulating with a more detailed control of the objects:

import os
import GridCalEngine as gce

folder = os.path.join('..', 'Grids_and_profiles', 'grids')
fname = os.path.join(folder, 'IEEE 5 Bus.xlsx')

main_circuit = gce.open_file(fname)

options_ = gce.LinearAnalysisOptions(distribute_slack=False, correct_values=True)

# snapshot
sn_driver = gce.LinearAnalysisDriver(grid=main_circuit, options=options_)
sn_driver.run()

print("Bus results:\n", sn_driver.results.get_bus_df())
print("Branch results:\n", sn_driver.results.get_branch_df())
print("PTDF:\n", sn_driver.results.mdl(gce.ResultTypes.PTDF).to_df())
print("LODF:\n", sn_driver.results.mdl(gce.ResultTypes.LODF).to_df())

Output:

Bus results:
         Vm   Va       P    Q
Bus 0  1.0  0.0  2.1000  0.0
Bus 1  1.0  0.0 -3.0000  0.0
Bus 2  1.0  0.0  0.2349  0.0
Bus 3  1.0  0.0 -0.9999  0.0
Bus 4  1.0  0.0  4.6651  0.0

Branch results:
                   Pf   loading
Branch 0-1  2.497192  0.624298
Branch 0-3  1.867892  0.832394
Branch 0-4 -2.265084 -0.828791
Branch 1-2 -0.502808 -0.391900
Branch 2-3 -0.267908 -0.774300
Branch 3-4 -2.400016 -1.000006

PTDF:
                Bus 0     Bus 1     Bus 2  Bus 3     Bus 4
Branch 0-1  0.193917 -0.475895 -0.348989    0.0  0.159538
Branch 0-3  0.437588  0.258343  0.189451    0.0  0.360010
Branch 0-4  0.368495  0.217552  0.159538    0.0 -0.519548
Branch 1-2  0.193917  0.524105 -0.348989    0.0  0.159538
Branch 2-3  0.193917  0.524105  0.651011    0.0  0.159538
Branch 3-4 -0.368495 -0.217552 -0.159538    0.0 -0.480452

LODF:
             Branch 0-1  Branch 0-3  Branch 0-4  Branch 1-2  Branch 2-3  Branch 3-4
Branch 0-1   -1.000000    0.344795    0.307071   -1.000000   -1.000000   -0.307071
Branch 0-3    0.542857   -1.000000    0.692929    0.542857    0.542857   -0.692929
Branch 0-4    0.457143    0.655205   -1.000000    0.457143    0.457143    1.000000
Branch 1-2   -1.000000    0.344795    0.307071   -1.000000   -1.000000   -0.307071
Branch 2-3   -1.000000    0.344795    0.307071   -1.000000   -1.000000   -0.307071
Branch 3-4   -0.457143   -0.655205    1.000000   -0.457143   -0.457143   -1.000000

Now let's make a comparison between the linear flows and the non-linear flows from Newton-Raphson:

import os
from matplotlib import pyplot as plt
import GridCalEngine as gce

plt.style.use('fivethirtyeight')

folder = os.path.join('..', 'Grids_and_profiles', 'grids')
fname = os.path.join(folder, 'IEEE39_1W.gridcal')
main_circuit = gce.open_file(fname)

ptdf_driver = gce.LinearAnalysisTimeSeriesDriver(grid=main_circuit)
ptdf_driver.run()

pf_options_ = gce.PowerFlowOptions(solver_type=gce.SolverType.NR)
ts_driver = gce.PowerFlowTimeSeriesDriver(grid=main_circuit, options=pf_options_)
ts_driver.run()

fig = plt.figure(figsize=(30, 6))
ax1 = fig.add_subplot(131)
ax1.set_title('Newton-Raphson based flow')
ax1.plot(ts_driver.results.Sf.real)
ax1.set_ylabel('MW')
ax1.set_xlabel('Time')

ax2 = fig.add_subplot(132)
ax2.set_title('PTDF based flow')
ax2.plot(ptdf_driver.results.Sf.real)
ax2.set_ylabel('MW')
ax2.set_xlabel('Time')

ax3 = fig.add_subplot(133)
ax3.set_title('Difference')
diff = ts_driver.results.Sf.real - ptdf_driver.results.Sf.real
ax3.plot(diff)
ax3.set_ylabel('MW')
ax3.set_xlabel('Time')

fig.set_tight_layout(tight=True)

plt.show()

PTDF flows comparison.png

Linear optimization

import os
import numpy as np
import GridCalEngine as gce

folder = os.path.join('..', 'Grids_and_profiles', 'grids')
fname = os.path.join(folder, 'IEEE39_1W.gridcal')

main_circuit = gce.open_file(fname)

# declare the snapshot opf
opf_options = gce.OptimalPowerFlowOptions(mip_solver=gce.MIPSolvers.HIGHS)
opf_driver = gce.OptimalPowerFlowDriver(grid=main_circuit, options=opf_options)

print('Solving...')
opf_driver.run()

print("Status:", opf_driver.results.converged)
print('Angles\n', np.angle(opf_driver.results.voltage))
print('Branch loading\n', opf_driver.results.loading)
print('Gen power\n', opf_driver.results.generator_power)
print('Nodal prices \n', opf_driver.results.bus_shadow_prices)

# declare the time series opf
opf_ts_driver = gce.OptimalPowerFlowTimeSeriesDriver(grid=main_circuit)

print('Solving...')
opf_ts_driver.run()

print("Status:", opf_ts_driver.results.converged)
print('Angles\n', np.angle(opf_ts_driver.results.voltage))
print('Branch loading\n', opf_ts_driver.results.loading)
print('Gen power\n', opf_ts_driver.results.generator_power)
print('Nodal prices \n', opf_ts_driver.results.bus_shadow_prices)

Run a linear optimization and verify with power flow

Often ties, you want to dispatch the generation using a linear optimization, to then verify the results using the power exact power flow. With GridCal, to do so is as easy as passing the results of the OPF into the PowerFlowDriver:

import os
import GridCalEngine as gce

folder = os.path.join('..', 'Grids_and_profiles', 'grids')
fname = os.path.join(folder, 'IEEE39_1W.gridcal')

main_circuit = gce.open_file(fname)

# declare the snapshot opf
opf_driver = gce.OptimalPowerFlowDriver(grid=main_circuit)
opf_driver.run()

# create the power flow driver, with the OPF results
pf_options = gce.PowerFlowOptions(solver_type=gce.SolverType.NR)
pf_driver = gce.PowerFlowDriver(grid=main_circuit,
                                options=pf_options,
                                opf_results=opf_driver.results)
pf_driver.run()

# Print results
print('Converged:', pf_driver.results.converged, '\nError:', pf_driver.results.error)
print(pf_driver.results.get_bus_df())
print(pf_driver.results.get_branch_df())

Output:

OPF results:

         Va    P  Shadow price
Bus 1  0.00  0.0           0.0
Bus 2 -2.22  0.0           0.0
Bus 3 -1.98  0.0           0.0
Bus 4 -2.12  0.0           0.0
Bus 5 -2.21  0.0           0.0

             Pf     Pt  Tap angle  Loading
Branch 1 -31.46  31.46        0.0   -44.94
Branch 1  -1.84   1.84        0.0   -10.20
Branch 1  -1.84   1.84        0.0    -9.18
Branch 1   0.14  -0.14        0.0     1.37
Branch 1 -48.30  48.30        0.0   -53.67
Branch 1 -35.24  35.24        0.0   -58.73
Branch 1  -4.62   4.62        0.0   -23.11

Power flow results:
Converged: True 
Error: 3.13e-11

         Vm    Va         P      Q
Bus 1  1.00  0.00  1.17e+02  12.90
Bus 2  0.97 -2.09 -4.00e+01 -20.00
Bus 3  0.98 -1.96 -2.50e+01 -15.00
Bus 4  1.00 -2.61  2.12e-09  32.83
Bus 5  0.98 -2.22 -5.00e+01 -20.00

             Pf     Qf     Pt     Qt  Loading
Branch 1 -31.37  -2.77  31.88   1.93   -44.81
Branch 2  -1.61  13.59   1.74 -16.24    -8.92
Branch 3  -1.44 -20.83   1.61  19.24    -7.21
Branch 4   0.46   5.59  -0.44  -7.46     4.62
Branch 5 -49.02  -4.76  49.77   4.80   -54.47
Branch 6 -34.95  -6.66  35.61   6.16   -58.25
Branch 7  -4.60  -5.88   4.62   4.01   -23.02

Hydro linear OPF

The following example loads and runs the linear optimization for a system that integrates fluid elements into a regular electrical grid.

import os
import GridCalEngine as gce

folder = os.path.join('..', 'Grids_and_profiles', 'grids')
fname = os.path.join(folder, 'hydro_simple.gridcal')
grid = gce.open_file(fname)

# Run the simulation
opf_driver = gce.OptimalPowerFlowTimeSeriesDriver(grid=grid)

print('Solving...')
opf_driver.run()

print('Gen power\n', opf_driver.results.generator_power)
print('Branch loading\n', opf_driver.results.loading)
print('Reservoir level\n', opf_driver.results.fluid_node_current_level)

Output:

OPF results:

time                | p2x_1_gen | pump_1_gen | turbine_1_gen | slack_gen
------------------- | --------- | ---------- | ------------- | ---------
2023-01-01 00:00:00 | 0.0       | -6.8237821 | 6.0           | 11.823782
2023-01-01 01:00:00 | 0.0       | -6.8237821 | 6.0           | 11.823782
2023-01-01 02:00:00 | 0.0       | -6.8237821 | 6.0           | 11.823782
2023-01-01 03:00:00 | 0.0       | -6.8237821 | 6.0           | 11.823782
2023-01-01 04:00:00 | 0.0       | -6.8237821 | 6.0           | 11.823782
2023-01-01 05:00:00 | 0.0       | -6.8237821 | 6.0           | 11.823782
2023-01-01 06:00:00 | 0.0       | -6.8237821 | 6.0           | 11.823782
2023-01-01 07:00:00 | 0.0       | -6.8237821 | 6.0           | 11.823782
2023-01-01 08:00:00 | 0.0       | -6.8237821 | 6.0           | 11.823782
2023-01-01 09:00:00 | 0.0       | -6.8237821 | 6.0           | 11.823782


time                | line1  | line2 | line3     | line4
------------------- | ------ | ----- | --------- | -----
2023-01-01 00:00:00 | 100.0  | 0.0   | 68.237821 | 40.0
2023-01-01 01:00:00 | 100.0  | 0.0   | 68.237821 | 40.0
2023-01-01 02:00:00 | 100.0  | 0.0   | 68.237821 | 40.0
2023-01-01 03:00:00 | 100.0  | 0.0   | 68.237821 | 40.0
2023-01-01 04:00:00 | 100.0  | 0.0   | 68.237821 | 40.0
2023-01-01 05:00:00 | 100.0  | 0.0   | 68.237821 | 40.0
2023-01-01 06:00:00 | 100.0  | 0.0   | 68.237821 | 40.0
2023-01-01 07:00:00 | 100.0  | 0.0   | 68.237821 | 40.0
2023-01-01 08:00:00 | 100.0  | 0.0   | 68.237821 | 40.0
2023-01-01 09:00:00 | 100.0  | 0.0   | 68.237821 | 40.0


time                | f1         | f2  | f3  | f4        
------------------- | ---------- | --- | --- | ----------
2023-01-01 00:00:00 | 49.998977  | 0.0 | 0.0 | 50.001022
2023-01-01 01:00:00 | 49.997954  | 0.0 | 0.0 | 50.002046
2023-01-01 02:00:00 | 49.996931  | 0.0 | 0.0 | 50.003068
2023-01-01 03:00:00 | 49.995906  | 0.0 | 0.0 | 50.004093
2023-01-01 04:00:00 | 49.994884  | 0.0 | 0.0 | 50.005116
2023-01-01 05:00:00 | 49.993860  | 0.0 | 0.0 | 50.006139
2023-01-01 06:00:00 | 49.992838  | 0.0 | 0.0 | 50.007162
2023-01-01 07:00:00 | 49.991814  | 0.0 | 0.0 | 50.008185
2023-01-01 08:00:00 | 49.990792  | 0.0 | 0.0 | 50.009208
2023-01-01 09:00:00 | 49.989768  | 0.0 | 0.0 | 50.010231

Short circuit

GridCal has unbalanced short circuit calculations. Now let's run a line-ground short circuit in the third bus of the South island of New Zealand grid example from reference book Computer Analysis of Power Systems by J. Arrillaga and C.P. Arnold

import os
import GridCalEngine as gce

folder = os.path.join('..', 'Grids_and_profiles', 'grids')
fname = os.path.join(folder, 'South Island of New Zealand.gridcal')

grid = gce.open_file(filename=fname)

# Run a Line-Ground short circuit on the bus at index 2
# Since we do not provide any power flow results, it will run one for us
results = gce.short_circuit(grid, fault_index=2, fault_type=gce.FaultType.LG)

print("Short circuit power: ", results.SCpower[fault_index])

A more elaborated way to run the simulation, controlling all the steps:

import os
import GridCalEngine as gce

folder = os.path.join('..', 'Grids_and_profiles', 'grids')
fname = os.path.join(folder, 'South Island of New Zealand.gridcal')

grid = gce.open_file(filename=fname)

pf_options = gce.PowerFlowOptions()
pf = gce.PowerFlowDriver(grid, pf_options)
pf.run()

fault_index = 2
sc_options = gce.ShortCircuitOptions(bus_index=fault_index,
                                     fault_type=gce.FaultType.LG)

sc = gce.ShortCircuitDriver(grid, options=sc_options,
                            pf_options=pf_options,
                            pf_results=pf.results)
sc.run()

print("Short circuit power: ", sc.results.SCpower[fault_index])

Output:

Short circuit power:  -217.00 MW - 680.35j MVAr

Sequence voltage, currents and powers are also available.

Continuation power flow

GridCal can run continuation power flows (voltage collapse studies)

import os
from matplotlib import pyplot as plt
import GridCalEngine as gce

plt.style.use('fivethirtyeight')

folder = os.path.join('..', 'Grids_and_profiles', 'grids')
fname = os.path.join(folder, 'South Island of New Zealand.gridcal')

# open the grid file
main_circuit = gce.open_file(fname)

# Run the continuation power flow with the default options
# Since we do not provide any power flow results, it will run one for us
results = gce.continuation_power_flow(grid=main_circuit, 
                                      factor=2.0, 
                                      stop_at=gce.CpfStopAt.Full)

# plot the results
fig = plt.figure(figsize=(18, 6))

ax1 = fig.add_subplot(121)
res = results.mdl(gce.ResultTypes.BusActivePower)
res.plot(ax=ax1)

ax2 = fig.add_subplot(122)
res = results.mdl(gce.ResultTypes.BusVoltage)
res.plot(ax=ax2)

plt.tight_layout()

A more elaborated way to run the simulation, controlling all the steps:

import os
from matplotlib import pyplot as plt
import GridCalEngine as gce

plt.style.use('fivethirtyeight')

folder = os.path.join('..', 'Grids_and_profiles', 'grids')
fname = os.path.join(folder, 'South Island of New Zealand.gridcal')

# open the grid file
main_circuit = gce.FileOpen(fname).open()

# we need to initialize with a power flow solution
pf_options = gce.PowerFlowOptions()
power_flow = gce.PowerFlowDriver(grid=main_circuit, options=pf_options)
power_flow.run()

# declare the CPF options
vc_options = gce.ContinuationPowerFlowOptions(step=0.001,
                                              approximation_order=gce.CpfParametrization.ArcLength,
                                              adapt_step=True,
                                              step_min=0.00001,
                                              step_max=0.2,
                                              error_tol=1e-3,
                                              tol=1e-6,
                                              max_it=20,
                                              stop_at=gce.CpfStopAt.Full,
                                              verbose=False)

# We compose the target direction
factor = 2.0
base_power = power_flow.results.Sbus / main_circuit.Sbase
vc_inputs = gce.ContinuationPowerFlowInput(Sbase=base_power,
                                           Vbase=power_flow.results.voltage,
                                           Starget=base_power * factor)

# declare the CPF driver and run
vc = gce.ContinuationPowerFlowDriver(grid=main_circuit,
                                     options=vc_options,
                                     inputs=vc_inputs,
                                     pf_options=pf_options)
vc.run()

# plot the results
fig = plt.figure(figsize=(18, 6))

ax1 = fig.add_subplot(121)
res = vc.results.mdl(gce.ResultTypes.BusActivePower)
res.plot(ax=ax1)

ax2 = fig.add_subplot(122)
res = vc.results.mdl(gce.ResultTypes.BusVoltage)
res.plot(ax=ax2)

plt.tight_layout()

cpf_south_island_new_zealand.png

Contingency analysis

GriCal has contingency simulations, and it features a quite flexible way of defining contingencies. Firs you define a contingency group, and then define individual events that are assigned to that contingency group. The simulation then tries all the contingency groups and apply the events registered in each group:

import os
import GridCalEngine as gce

folder = os.path.join('..', 'Grids_and_profiles', 'grids')
fname = os.path.join(folder, 'IEEE 5 Bus.xlsx')

main_circuit = gce.open_file(fname)

branches = main_circuit.get_branches()

# manually generate the contingencies
for i, br in enumerate(branches):
    # add a contingency group
    group = gce.ContingencyGroup(name="contingency {}".format(i + 1))
    main_circuit.add_contingency_group(group)

    # add the branch contingency to the groups, only groups are failed at once
    con = gce.Contingency(device_idtag=br.idtag, name=br.name, group=group)
    main_circuit.add_contingency(con)

# add a special contingency
group = gce.ContingencyGroup(name="Special contingency")
main_circuit.add_contingency_group(group)
main_circuit.add_contingency(Contingency(device_idtag=branches[3].idtag,
                                         name=branches[3].name, group=group))
main_circuit.add_contingency(Contingency(device_idtag=branches[5].idtag,
                                         name=branches[5].name, group=group))

pf_options = gce.PowerFlowOptions(solver_type=SolverType.NR)

# declare the contingency options
options_ = gce.ContingencyAnalysisOptions(use_provided_flows=False,
                                          Pf=None,
                                          contingency_method=gce.ContingencyMethod.PowerFlow,
                                          # if no power flow options are provided 
                                          # a linear power flow is used
                                          pf_options=pf_options)

linear_multiple_contingencies = gce.LinearMultiContingencies(grid=main_circuit)

simulation = gce.ContingencyAnalysisDriver(grid=main_circuit,
                                           options=options_,
                                           linear_multiple_contingencies=linear_multiple_contingencies)

simulation.run()

# print results
df = simulation.results.mdl(ResultTypes.BranchActivePowerFrom).to_df()
print("Contingency flows:\n", df)

Output:

Contingency flows:
                       Branch 0-1  Branch 0-3  Branch 0-4  Branch 1-2  Branch 2-3  Branch 3-4
# contingency 1          0.000000  322.256814 -112.256814 -300.000000 -277.616985 -350.438026
# contingency 2        314.174885    0.000000 -104.174887   11.387545   34.758624 -358.359122
# contingency 3        180.382705   29.617295    0.000000 -120.547317  -97.293581 -460.040537
# contingency 4        303.046401  157.540574 -250.586975    0.000000   23.490000 -214.130663
# contingency 5        278.818887  170.710914 -239.529801  -23.378976    0.000000 -225.076976
# contingency 6        323.104522  352.002620 -465.107139   20.157096   43.521763    0.000000
# Special contingency  303.046401  372.060738 -465.107139    0.000000   23.490000    0.000000

This simulation can also be done for time series.

State estimation

Now lets program the example from the state estimation reference book State Estimation in Electric Power Systems by A. Monticelli.

import GridCalEngine as gce

m_circuit = gce.MultiCircuit()

b1 = gce.Bus('B1', is_slack=True)
b2 = gce.Bus('B2')
b3 = gce.Bus('B3')

br1 = gce.Line(b1, b2, name='Br1', r=0.01, x=0.03, rate=100.0)
br2 = gce.Line(b1, b3, name='Br2', r=0.02, x=0.05, rate=100.0)
br3 = gce.Line(b2, b3, name='Br3', r=0.03, x=0.08, rate=100.0)

# add measurements
m_circuit.add_pf_measurement(PfMeasurement(0.888, 0.008, br1))
m_circuit.add_pf_measurement(PfMeasurement(1.173, 0.008, br2))

m_circuit.add_qf_measurement(QfMeasurement(0.568, 0.008, br1))
m_circuit.add_qf_measurement(QfMeasurement(0.663, 0.008, br2))

m_circuit.add_pi_measurement(PiMeasurement(-0.501, 0.01, b2))
m_circuit.add_qi_measurement(QiMeasurement(-0.286, 0.01, b2))

m_circuit.add_vm_measurement(VmMeasurement(1.006, 0.004, b1))
m_circuit.add_vm_measurement(VmMeasurement(0.968, 0.004, b2))

m_circuit.add_bus(b1)
m_circuit.add_bus(b2)
m_circuit.add_bus(b3)

m_circuit.add_line(br1)
m_circuit.add_line(br2)
m_circuit.add_line(br3)

# Declare the simulation driver and run
se = gce.StateEstimation(circuit=m_circuit)
se.run()

print(se.results.get_bus_df())
print(se.results.get_branch_df())

Output:

          Vm        Va         P        Q
B1  0.999629  0.000000  2.064016  1.22644
B2  0.974156 -1.247547  0.000000  0.00000
B3  0.943890 -2.745717  0.000000  0.00000

             Pf         Qf   Pt   Qt    loading
Br1   89.299199  55.882169  0.0  0.0  55.882169
Br2  117.102446  66.761871  0.0  0.0  66.761871
Br3   38.591163  22.775597  0.0  0.0  22.775597

Export the results

A simple function is available to export the results of a driver.

import GridCalEngine as gce

fname = os.path.join("data", "grids", "IEEE39_1W.gridcal")
grid = gce.open_file(fname)

# create the driver
pf_driver = gce.PowerFlowTimeSeriesDriver(grid=grid,
                                          options=gce.PowerFlowOptions(),
                                          time_indices=grid.get_all_time_indices())
# run
pf_driver.run()

# Save the driver results in a zip file with CSV files inside
gce.export_drivers(drivers_list=[pf_driver], file_name="IEEE39_1W_results.zip")

You could save many drivers in the same zip file passing then into the list drivers_list.

Also there is a function to save from the results objects themselves:

import GridCalEngine as gce

fname = os.path.join("data", "grids", "IEEE39_1W.gridcal")
grid = gce.open_file(fname)

# run with the API shortcut functions
pf_results = gce.power_flow(grid)
pf_ts_results = gce.power_flow_ts(grid)

# Save the driver results in a zip file with CSV files inside
gce.export_results(results_list=[pf_results, pf_ts_results], file_name="IEEE39_1W_results.zip")

Client - Server operation

To use the gridcal server, you need to install the GridCalServer python package. Once this is done, the gridcalservercommand will be available on the system. To launch the server, simply type gridcalserver. This will launch a GridCal server on the machine, on port 8000. This is https://localhost:8000

An example on how to send a grid from a script to the server:

import os
import asyncio
import GridCalEngine as gce

# path to your file
fname = os.path.join('..', '..', '..', 'Grids_and_profiles', 'grids', "IEEE57.gridcal")

# read gridcal file
grid_ = gce.open_file(fname)

# define instruction for the server
instruction = gce.RemoteInstruction(operation=gce.SimulationTypes.NoSim)

# generate json to send
model_data = gce.gather_model_as_jsons_for_communication(circuit=grid_, instruction=instruction)

# get the sever certificate
gce.get_certificate(base_url="https://localhost:8000",
                    certificate_path=gce.get_certificate_path(),
                    pwd="")

# send json
reply_from_server = asyncio.get_event_loop().run_until_complete(
    gce.send_json_data(json_data=model_data,
                       endpoint_url="https://localhost:8000/upload",
                       certificate=gce.get_certificate_path())
)

print(reply_from_server)

Matpower grids

Matpower's excellent formulations and consistency has allowed this and other projects to evolve relying of sound math. That is why GridCal reads Matpower cases out of the box, without you having to do anything special. And of course, GridCal solves all Matpower 8 provided grids:

name n_buses n_branches P imbalance (%) Used V0 converged error (p.u.) iterations time (s)
case_SyntheticUSA.m 82000 104121 -0.1 TRUE TRUE 9.90E-07 3 1.03154
case_ACTIVSg70k.m 70000 88207 0.6 TRUE TRUE 8.00E-07 7 1.73622
case_ACTIVSg25k.m 25000 32230 -2.7 FALSE TRUE 1.45E-09 5 0.37424
case13659pegase.m 13659 20467 2411.0 FALSE TRUE 1.75E-07 6 0.16958
case_ACTIVSg10k.m 10000 12706 -7.6 TRUE TRUE 1.03E-07 1 0.02221
case9241pegase.m 9241 16049 683.5 FALSE TRUE 9.38E-08 11 0.23802
case8387pegase.m 8387 14561 -44.2 FALSE TRUE 4.34E-10 7 0.13024
case6515rte.m 6515 9037 -47.5 TRUE TRUE 1.05E-07 2 0.02758
case6495rte.m 6495 9019 -48.9 TRUE TRUE 1.12E-07 7 0.09349
case6470rte.m 6470 9005 -48.0 TRUE TRUE 3.86E-08 2 0.02659
case6468rte.m 6468 9000 -46.3 TRUE TRUE 1.11E-08 2 0.02708
case3375wp.m 3374 4161 -73.3 TRUE TRUE 9.83E-07 1 0.00804
case3120sp.m 3120 3693 -100.0 FALSE TRUE 3.27E-11 9 0.05893
case3012wp.m 3012 3572 -98.9 TRUE TRUE 3.47E-08 2 0.01262
case2869pegase.m 2869 4582 561.4 FALSE TRUE 1.07E-08 10 0.06973
case2868rte.m 2868 3808 -46.0 FALSE TRUE 3.96E-09 15 0.08018
case2848rte.m 2848 3776 -41.3 FALSE TRUE 1.91E-08 19 0.10530
case2746wop.m 2746 3514 -96.5 FALSE TRUE 2.78E-07 11 0.06114
case2746wp.m 2746 3514 -95.8 FALSE TRUE 9.13E-09 11 0.06028
case2737sop.m 2737 3506 -94.2 FALSE TRUE 1.87E-07 5 0.02754
case2736sp.m 2736 3504 -95.2 FALSE TRUE 7.90E-08 8 0.04321
case2383wp.m 2383 2896 -97.4 FALSE TRUE 2.64E-09 4 0.01960
case_ACTIVSg2000.m 2000 3206 10.8 FALSE TRUE 1.22E-08 9 0.04812
case1951rte.m 1951 2596 -46.0 TRUE TRUE 7.86E-08 2 0.00723
case1888rte.m 1888 2531 -47.1 TRUE TRUE 6.27E-07 1 0.00369
case1354pegase.m 1354 1991 862.8 FALSE TRUE 7.43E-09 10 0.02611
case_ACTIVSg500.m 500 597 2.8 FALSE TRUE 4.17E-09 9 0.00896
case300.m 300 411 -38.6 FALSE TRUE 1.54E-09 10 0.00656
case_ACTIVSg200.m 200 245 6.5 FALSE TRUE 1.82E-07 3 0.00140
case145.m 145 453 -100.0 FALSE TRUE 6.24E-10 5 0.00191
case141.m 141 140 -100.0 FALSE TRUE 4.69E-09 2 0.00060
case136ma.m 136 156 -100.0 FALSE TRUE 1.14E-08 2 0.00059
case118zh.m 118 132 -100.0 FALSE TRUE 1.46E-08 2 0.00054
case118.m 118 186 -28.3 FALSE TRUE 1.94E-07 9 0.00253
case94pi.m 94 93 -100.0 FALSE TRUE 2.08E-11 2 0.00051
case89pegase.m 89 210 4.2 FALSE TRUE 2.81E-09 4 0.00133
case85.m 85 84 -100.0 FALSE TRUE 7.90E-12 2 0.00050
case74ds.m 74 73 -100.0 FALSE TRUE 8.74E-07 1 0.00024
case_RTS_GMLC.m 73 120 -80.5 FALSE TRUE 1.62E-07 9 0.00172
case70da.m 70 76 -100.0 FALSE TRUE 2.17E-12 2 0.00031
case69.m 69 68 -100.0 FALSE TRUE 7.20E-09 2 0.00039
case60nordic.m 60 88 96.3 FALSE TRUE 5.15E-08 4 0.00074
case57.m 57 80 -100.0 FALSE TRUE 2.82E-10 9 0.00172
case51ga.m 51 50 -100.0 FALSE TRUE 1.85E-12 2 0.00034
case51he.m 51 50 -100.0 FALSE TRUE 6.16E-07 1 0.00021
case39.m 39 46 -26.1 FALSE TRUE 1.93E-11 9 0.00130
case38si.m 38 37 -100.0 FALSE TRUE 7.26E-12 2 0.00030
case34sa.m 34 33 -100.0 FALSE TRUE 8.24E-13 2 0.00030
case33bw.m 33 37 -100.0 FALSE TRUE 7.38E-09 2 0.00033
case33mg.m 33 37 -100.0 FALSE TRUE 7.46E-12 2 0.00030
case30.m 30 41 -39.6 FALSE TRUE 9.57E-10 3 0.00046
case30Q.m 30 41 -39.6 FALSE TRUE 9.57E-10 3 0.00043
case30pwl.m 30 41 -39.6 FALSE TRUE 9.57E-10 3 0.00070
case_ieee30.m 30 41 -3.2 FALSE TRUE 5.18E-08 3 0.00049
case28da.m 28 27 -100.0 FALSE TRUE 6.85E-07 1 0.00018
case24_ieee_rts.m 24 38 -70.5 FALSE TRUE 1.63E-08 6 0.00078
case22.m 22 21 -100.0 FALSE TRUE 2.13E-07 1 0.00016
case18nbr.m 18 17 -100.0 FALSE TRUE 1.35E-07 2 0.00028
case18.m 18 17 -100.0 FALSE TRUE 1.27E-08 3 0.00039
case17me.m 17 16 -100.0 FALSE TRUE 3.19E-08 3 0.00035
case16ci.m 16 16 -100.0 FALSE TRUE 1.38E-09 2 0.00022
case16am.m 15 14 -100.0 FALSE TRUE 1.22E-07 2 0.00029
case15da.m 15 14 -100.0 FALSE TRUE 8.57E-07 1 0.00019
case15nbr.m 15 14 -100.0 FALSE TRUE 5.43E-08 2 0.00025
case14.m 14 20 1.3 FALSE TRUE 5.98E-08 3 0.00040
case12da.m 12 11 -100.0 FALSE TRUE 2.71E-07 1 0.00018
case10ba.m 10 9 -100.0 FALSE TRUE 6.22E-08 2 0.00024
case9_gurobi_test.m 9 9 1.7 FALSE TRUE 3.42E-07 3 0.00031
case9target.m 9 9 -41.1 FALSE TRUE 1.40E-07 5 0.00050
case9.m 9 9 1.7 FALSE TRUE 3.42E-07 3 0.00031
case9Q.m 9 9 -21.3 FALSE TRUE 5.71E-07 3 0.00031
case6ww.m 6 11 -47.6 FALSE TRUE 2.09E-10 3 0.00030
case5.m 5 6 -36.3 FALSE TRUE 6.42E-11 3 0.00030
case4_dist.m 4 3 -100.0 FALSE TRUE 5.70E-09 3 0.00032
case4gs.m 4 4 -100.0 FALSE TRUE 1.07E-09 3 0.00031

Results simulated with AMD 9750x and 64 GB of RAM under Ubuntu 24.04. All solved using Newton-Raphson, and only using the provided solution that comes with the files when the flat start fails. That is what Used V0 means.

Cool right?

Tests

GridCal uses pytest for automatic software testing.

If you make changes to GridCal that you plan to submit, first make sure that all tests are still passing. You can do this locally with pytest.

If you have added new functionality, you should also add a new function that tests this functionality. pytest automatically detects all functions in the src/tests folder that start with test_ and are located in a file that also starts with test_ as relevant test cases. Unit test (for pytest) are included in src/tests. As defined in pytest.ini, all files matching test_*.py are executed by running pytest.

Files matching *_test.py are not executed; they were not formatted specifically for pytest but were mostly done for manual testing and documentation purposes.

Additional tests should be developped for each new and existing feature. pytest should be run before each commit to prevent easily detectable bugs.

Bug reporting

You have found a bug in GridCal or have a suggestion for a new functionality? Then get in touch with us by opening up an issue on the issue board to discuss possible new developments with the community and the maintainers.

Contributing

All contributions to the GridCal repository are made through pull requests to the devel branch. You can either submit a pull request from the develop branch of your fork or create a special feature branch that you keep the changes on. A feature branch is the way to go if you have multiple issues that you are working on in parallel and want to submit with seperate pull requests. If you only have small, one-time changes to submit, you can also use the devel branch to submit your pull request.

However, it is best to discuss your contribution before the pull request is ready to be officially submitted. We only accept high quality contributions that align with the project design. Those are heavily reviewed, and you may expect joint work with us if your proposal is deemed good enough.

An easier alternative to contribute is to use the GridCal objects and functions to produce your contribution in a script-like fashion. Again, if that meets the functional and quality standards that we impose, we'll take care of the integration.

All contributions must come with testing.

Contact

License

GridCal is licensed under the Mozilla Public License 2.0 (MPLv2)

In practical terms this means that:

  • You can use GridCal for commercial work.
  • You can sell commercial services based on GridCal.
  • If you distrubute GridCal, you must distribute GridCal's source code as well. That is always achieved in practice with python code.
  • GridCal license does not propagate, even if you use GridCal or pieces of it in your code. However, you must retain the individual files licensing.

Nonetheless, read the license carefully.

Disclaimer

All trademarks mentioned in the documentation or the source code belong to their respective owners.