Closed danielolsen closed 2 years ago
Here's a usage example, showing how we can leverage the new functionality to automatically generate the parameters needed for Grid-building:
Assume we have a CSV data table of representative line designs:
kV | bundle_count | conductor | spacing | a_x | a_y | b_x | b_y | c_x | c_y |
---|---|---|---|---|---|---|---|---|---|
69 | 1 | Osprey | N/A | -2 | 16.5 | 2 | 15 | -2 | 13.5 |
115 | 1 | Drake | N/A | -2 | 19 | 2 | 17 | -2 | 15 |
138 | 1 | Ortolan | N/A | -3 | 22.5 | 3 | 20 | -3 | 17.5 |
230 | 1 | Falcon | N/A | -7 | 20 | 0 | 20 | 7 | 20 |
230 | 2 | Drake | 0.5 | -7 | 20 | 0 | 20 | 7 | 20 |
230 | 3 | Drake | 0.5 | -7 | 20 | 0 | 20 | 7 | 20 |
345 | 1 | Kiwi | N/A | -8 | 25 | 0 | 25 | 8 | 25 |
345 | 2 | Falcon | 0.5 | -8 | 25 | 0 | 25 | 8 | 25 |
500 | 3 | Rail | 0.5 | -10 | 30 | 0 | 30 | 10 | 30 |
765 | 4 | Martin | 0.5 | 15 | 40 | 0 | 40 | 15 | 40 |
The first three represent single-pole designs (phases oriented primarily vertically, two on one side and one on the other), while the other represent H-frame or lattice designs (phases oriented horizontally). All distances are measured in meters, with the center of the tower at ground level representing the origin.
We can build a function that interprets each row of this table into a Tower
object:
def build_tower(series):
"""Build a Tower object using the transmission line design"""
conductor = Conductor(series["conductor"])
if series["bundle_count"] != 1:
bundle = ConductorBundle(
conductor=conductor,
n=series["bundle_count"],
spacing=series["spacing"],
)
else:
bundle = ConductorBundle(conductor=conductor) # n = 1 by default
locations = PhaseLocations(
a=tuple(series[["a_x", "a_y"]]),
b=tuple(series[["b_x", "b_y"]]),
c=tuple(series[["c_x", "c_y"]]),
)
tower = Tower(bundle=bundle, locations=locations)
return tower
Then, we can define a function which calculates per-mile parameter values:
def calculate_per_mile_parameters(series):
z_base = series["voltage"]**2 / 100 # 100 MVA base
tower = build_tower(series)
line = Line(tower=tower, voltage=series["voltage"], length=1.609) # km in 1 mile
output = pd.Series(
{
"reactance_per_mile": line.series_impedance.imag / z_base,
"thermal_rating": line.thermal_rating,
"surge_impedance_loading": line.surge_impedance_loading,
}
)
return output
and one that calculates the rating for a 100-mile long line:
def calculate_100_mile_rating(series):
tower = build_tower(series)
line = Line(tower=tower, voltage=series["voltage"], length=160.9) # km in 100 mile
return line.power_rating
Putting these into action:
>>> import pandas as pd
>>> from prereise.gather.griddata.transmission.geometry import (
... Conductor,
... ConductorBundle,
... Line,
... PhaseLocations,
... Tower,
... )
>>> data = pd.read_csv("path/to/line_designs.csv")
>>> one_mile_parameters = data.apply(calculate_per_mile_parameters, axis=1)
>>> # Add a meaningful index to the new calculations, using some of the original columns
>>> index_for_display = data.set_index(["voltage", "bundle_count"]).index
>>> one_mile_parameters.index = index_for_display
>>> one_mile_parameters
reactance_per_mile thermal_rating surge_impedance_loading
voltage bundle_count
69 1 0.015501 85.450727 13.113853
115 1 0.005447 183.250975 37.534569
138 1 0.003941 250.974162 52.098497
230 1 0.001448 551.744785 142.092507
2 0.001092 733.003902 187.338069
3 0.000947 1099.505853 215.762241
345 1 0.000649 960.274948 318.173207
2 0.000482 1655.234354 425.428999
500 3 0.000217 2585.085830 941.144695
765 4 0.000092 6651.594716 2225.035726
>>> rating_100_mile = data.apply(calculate_100_mile_rating, axis=1)
>>> rating_100_mile.index = index_for_display
>>> rating_100_mile
voltage bundle_count
69 1 26.199249
115 1 74.987693
138 1 104.083946
230 1 283.876688
2 374.269634
3 431.056300
345 1 635.656013
2 849.934862
500 3 1880.247205
765 4 4445.243361
dtype: float64
As would be expected, the rating for a line of about 100 miles is less than the thermal rating, and about 2x the surge impedance loading, matching the St. Clair curve.
To build a full Grid, we just need to associate each branch with a representative tower design, and then we can use that line's specific length to calculate the allowable power transfer, which will always be the lower of the thermal limit or the stability limit. Extending the tower-building function to multiple circuits on a tower is fairly trivial: we can follow the same procedure as with single vs. multiple conductors, where one column tells us which other columns we should be looking at to build the appropriate Tower
object.
Reading the code, it looks like a conductor bundle is made of the same type of conductor. Is that what happens in reality or a this is use to simplify the model?
Reading the code, it looks like a conductor bundle is made of the same type of conductor. Is that what happens in reality or a this is use to simplify the model?
I believe this is true to real life: I've never seen reference to a heterogeneous conductor bundle.
There are a few other implicit assumptions that I think are more often violated in real life, to varying degrees:
I think these assumptions are all reasonable though, since to account for them would require more complicated logic and/or data that is unavailable or very difficult to collect, and would probably not make a large impact on the overall calculation results.
In the geometry
module, you import cmath
and constants/functions from math
. Can we just use cmath
for everything, it seems to handle complex numbers along with float/integers. It brings me to my second question, where do we encounter complex numbers? Are we dealing with complex impedance?
In the
geometry
module, you importcmath
and constants/functions frommath
. Can we just usecmath
for everything, it seems to handle complex numbers along with float/integers. It brings me to my second question, where do we encounter complex numbers? Are we dealing with complex impedance?
We are indeed working with complex impedance. Properly calculating the impedance of long-distance transmission lines requires doing calculations which combine series resistance (real), series reactance (imaginary), and shunt admittance (imaginary).
cmath
functions will always return a complex result, which is why I've stuck with math.sqrt
over cmath.sqrt
when the arguments are purely real.
In the
geometry
module, you importcmath
and constants/functions frommath
. Can we just usecmath
for everything, it seems to handle complex numbers along with float/integers. It brings me to my second question, where do we encounter complex numbers? Are we dealing with complex impedance?We are indeed working with complex impedance. Properly calculating the impedance of long-distance transmission lines requires doing calculations which combine series resistance (real), series reactance (imaginary), and shunt admittance (imaginary).
cmath
functions will always return a complex result, which is why I've stuck withmath.sqrt
overcmath.sqrt
when the arguments are purely real.
Thanks for the explanation. The other option is to take the real part of the output:
>>> import cmath
>>> cmath.sqrt(1)
(1+0j)
>>> cmath.sqrt(1).real
1.0
Not very important
Pull Request doc
Purpose
What the code is doing
approximate_loadability
adds an interface to use line length to estimate loadability. The design is flexible enough to be able to incorporate additional methods, in case we want to enable direct calculation of the St. Clair curve for a given set of per-length impedances.get_standard_conductors
is a small helper function which reads the standard conductor parameters from the new CSV and returns a dataframe.geometry
module:get_standard_conductors
is used within the instantiation of aConductor
: we can now specify conductors by code name, rather than physical parameter values, and all relevant physical parameters will be populated. This is also used to simplify the tests a bit.Line
.power_rating
parameter for aLine
is set based on the smaller of the thermal power rating and the loadability calculated using the approximated St. Clair curve.Testing
One new unit test is added, and existing unit tests are enhanced.
Usage Example/Visuals
Besides being able to instantiate a
Conductor
by name (e.g.c = Conductor("Ostrich")
), there are no other changes to how the objects are used. The only difference is that additional attributes are available.Time estimate
30 minutes? There calculations that the code is doing aren't very complex, but probably only make sense to the folks with a power systems background.