Skip to content

feat: add loadability calculations and standard conductor parameters#272

Merged
danielolsen merged 11 commits intodevelopfrom
daniel/line_conductors_ratings
Mar 12, 2022
Merged

feat: add loadability calculations and standard conductor parameters#272
danielolsen merged 11 commits intodevelopfrom
daniel/line_conductors_ratings

Conversation

@danielolsen
Copy link
Copy Markdown
Contributor

Pull Request doc

Purpose

  • Add standard conductor parameters
  • Add calculation of line thermal power ratings using conductor parameters
  • Add calculation of loadability based on line length using the St. Clair curve approximation from @YifanLi86's paper
  • Miscellaneous usability fixes

What the code is doing

  • Two new helper functions are added:
    • 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.
  • Within the geometry module:
    • get_standard_conductors is used within the instantiation of a Conductor: 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.
    • Per-conductor current ratings are aggregated by conductor bundle and by number of circuits to reflect a per-phase current limit, and this per-phase current limit is used to calculate a thermal power rating for a Line.
    • The power_rating parameter for a Line 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.

@danielolsen danielolsen self-assigned this Mar 2, 2022
@danielolsen danielolsen force-pushed the daniel/line_conductors_ratings branch 3 times, most recently from 150042e to 203a2eb Compare March 4, 2022 00:19
@danielolsen
Copy link
Copy Markdown
Contributor Author

danielolsen commented Mar 4, 2022

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.

@danielolsen danielolsen force-pushed the daniel/line_conductors_ratings branch from 203a2eb to 167d47a Compare March 9, 2022 22:43
Comment thread prereise/gather/griddata/transmission/const.py Outdated
@rouille
Copy link
Copy Markdown
Collaborator

rouille commented Mar 9, 2022

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?

@danielolsen
Copy link
Copy Markdown
Contributor Author

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:

  • Phases are always transposed so that there's an exactly equal length of line in each position
  • Lines are made up of exactly one tower design
  • All circuits on a tower are of the same voltage (i.e. you would never see a 230 kV circuit and a 69 kV circuit strung together onto a double-circuit tower)

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.

@rouille
Copy link
Copy Markdown
Collaborator

rouille commented Mar 10, 2022

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?

@danielolsen
Copy link
Copy Markdown
Contributor Author

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?

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.

@rouille
Copy link
Copy Markdown
Collaborator

rouille commented Mar 10, 2022

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?

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.

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

Comment thread prereise/gather/griddata/transmission/helpers.py Outdated
Copy link
Copy Markdown
Collaborator

@rouille rouille left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not go too far into the calculations of the conductor(s) parameters but the logic and the tests look good

Comment thread prereise/gather/griddata/transmission/geometry.py
Copy link
Copy Markdown
Collaborator

@BainanXia BainanXia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to go!

@danielolsen danielolsen force-pushed the daniel/line_conductors_ratings branch from 35e91b1 to 1686bec Compare March 12, 2022 01:10
@danielolsen danielolsen merged commit b1bb7eb into develop Mar 12, 2022
@danielolsen danielolsen deleted the daniel/line_conductors_ratings branch March 12, 2022 01:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants