Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support concentric neutral cables #16

Merged
merged 18 commits into from
Apr 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 61 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,19 @@ of the conductor for that phase.
.. code:: python


from carsons import CarsonsEquations, perform_kron_reduction
from carsons import CarsonsEquations, perform_kron_reduction, impedance

class Line:
gmr: {
'A': geometric_mean_radius_A
...
}
r: {
'A' => per-length resistance of conductor A in ohms
'A': per-length resistance of conductor A in ohms
...
}
phase_positions: {
'A' => (x, y) cross-sectional position of the conductor in meters
'A': (x, y) cross-sectional position of the conductor in meters
...
}
phases: {'A', ... }
Expand All @@ -86,6 +86,7 @@ of the conductor for that phase.

z_primitive = CarsonsEquations(Line()).build_z_primitive()
z_abc = perform_kron_reduction(z_primitive)
line_impedance = impedance(CarsonsEquations(Line()))


The model supports any combination of ABC phasings (for example BC, BCN etc...)
Expand All @@ -101,6 +102,63 @@ For examples of how to use the model, see the `tests <https://github.com/opusone
``carsons`` is tested against several cable configurations from the
`IEEE 4-bus test network <http://sites.ieee.org/pes-testfeeders/resources/>`_.


### Concentric Neutral Cable

``carsons`` also supports modelling of concentric neutral cables of any phasings.
Its usage is very similar to the example above, only requires a few more
parameters about the neutral conductors in the line model object.

.. code:: python


from carsons import (ConcentricNeutralCarsonsEquations,
perform_kron_reduction,
impedance)

class Line:
resistance: {
'A': per-length resistance of conductor A in ohms
...
}
geometric_mean_radius: {
'A': geometric_mean_radius_A
...
}
phase_positions: {
'A' => (x, y) cross-sectional position of the conductor in meters
...
}
phases: {'A', 'NA', ... }
neutral_strand_gmr: {
'NA': neutral_strand_gmr_A
...
}
neutral_strand_resistance: {
'NA': neutral_strand_resistance_A
...
}
neutral_strand_diameter: {
'NA': neutral_strand_diameter_A
...
}
diameter_over_neutral: {
'NA': diameter_over_neutral_A
...
}
neutral_strand_count: {
'NA': neutral_strand_count_A
...
}


z_primitive = ConcentricNeutralCarsonsEquations(Line()).build_z_primitive()
z_abc = perform_kron_reduction(z_primitive)
line_impedance = impedance(ConcentricNeutralCarsonsEquations(Line()))

For examples of how to use the model, see the `tests <https://github.com/opusonesolutions/carsons/blob/master/tests/test_concentric_neutral_cable.py>`_.


Problem Description
-------------------

Expand Down
4 changes: 3 additions & 1 deletion carsons/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from carsons.carsons import convert_geometric_model # noqa 401
from carsons.carsons import (convert_geometric_model, # noqa 401
impedance, # noqa 401
ConcentricNeutralCarsonsEquations) # noqa 401

name = "carsons"
97 changes: 86 additions & 11 deletions carsons/carsons.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
from numpy import pi as π
from collections import defaultdict
from itertools import islice

from numpy import zeros
from numpy import arctan, cos, log, sin, sqrt, zeros
from numpy import pi as π
from numpy.linalg import inv
from numpy import sqrt
from numpy import log
from numpy import cos
from numpy import sin
from numpy import arctan
from itertools import islice


def convert_geometric_model(geometric_model):
Expand All @@ -18,6 +14,12 @@ def convert_geometric_model(geometric_model):
return z_abc


def impedance(model):
z_primitive = model.build_z_primitive()
z_abc = perform_kron_reduction(z_primitive)
return z_abc


def perform_kron_reduction(z_primitive):
""" Reduces the primitive impedance matrix to an equivalent impedance
matrix.
Expand Down Expand Up @@ -107,16 +109,17 @@ def compute_X(self, i, j):
Qᵢⱼ = self.compute_Q(i, j)
ΔX = self.μ * self.ω / π * Qᵢⱼ

# calculate geometry ratio 𝛥G
if i != j:
Dᵢⱼ = self.compute_D(i, j)
dᵢⱼ = self.compute_d(i, j)
geometry_ratio = Dᵢⱼ / dᵢⱼ
𝛥G = Dᵢⱼ / dᵢⱼ
else:
hᵢ = self.get_h(i)
gmrⱼ = self.gmr[j]
geometry_ratio = 2.0 * hᵢ / gmrⱼ
𝛥G = 2.0 * hᵢ / gmrⱼ

X_o = self.ω * self.μ / (2 * π) * log(geometry_ratio)
X_o = self.ω * self.μ / (2 * π) * log(𝛥G)

return X_o + ΔX

Expand Down Expand Up @@ -172,6 +175,7 @@ def compute_d(self, i, j):

def compute_D(self, i, j):
xⱼ, yⱼ = self.phase_positions[j]

return self.calculate_distance(self.phase_positions[i], (xⱼ, -yⱼ))

@staticmethod
Expand All @@ -183,3 +187,74 @@ def calculate_distance(positionᵢ, positionⱼ):
def get_h(self, i):
_, yᵢ = self.phase_positions[i]
return yᵢ


class ConcentricNeutralCarsonsEquations(CarsonsEquations):
def __init__(self, model, *args, **kwargs):
super().__init__(model)
self.neutral_strand_gmr = model.neutral_strand_gmr
self.neutral_strand_count = defaultdict(
lambda: None, model.neutral_strand_count)
self.neutral_strand_resistance = model.neutral_strand_resistance
self.radius = defaultdict(lambda: None, {
phase: (diameter_over_neutral -
model.neutral_strand_diameter[phase]) / 2
for phase, diameter_over_neutral
in model.diameter_over_neutral.items()
})
self.phase_positions.update({
f"N{phase}": self.phase_positions[phase]
for phase in self.phase_positions.keys()
})
self.gmr.update({
phase: self.GMR_cn(phase)
for phase in model.diameter_over_neutral.keys()
})
self.r.update({
phase: resistance / model.neutral_strand_count[phase]
for phase, resistance in model.neutral_strand_resistance.items()
})
return

def compute_d(self, i, j):
Copy link
Contributor

Choose a reason for hiding this comment

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

Comparison to Gridlab-d

To try and find the issues with the impedance calculations, I audited the gridlab-d source. I think this method differs from the Gridlab-d distance calculations in the following cases.

Background

Gridlab-d appears to use the following indexes for different phases. The snippets below use these phase indexes

Index Description
1 Phase A
2 Phase B
3 Phase C
4 Concentric Neutral A
5 Concentric Neutral B
6 Concentric Neutral C
7 Neutral Conductor

Conductor to Neutral Cable

Python Implementation

In this case, the inputs are I = {'A'}, J = {'N'}

This leads to the following:

I ^ J = {'A', 'N'}
I & J = {}

one_neutral_same_phase == False
different_phase == True
one_neutral == True

Thus the code passed the check on line 233 and returns (distance_ij**2 + r**2) ** 0.5.

Gridlab-D Implementation

However, gridlab-d does the following in the case of A -> N:

#define DIST(ph1, ph2) (has_phase(PHASE_##ph1) && has_phase(PHASE_##ph2) && config->line_spacing ? OBJECTDATA(config->line_spacing, line_spacing)->distance_##ph1##to##ph2 : 0.0)
D(1, 7) = DIST(A, N);

Solution

I think the correct solution in this case is to return distance_ij

Conductor to Own Concentric Neutral

Python Implementation

In this case, the inputs are I = {'A'}, J = {'A', 'N'}

This leads to the following:

I ^ J = {'N'}
I & J = {'A}

one_neutral_same_phase == True
different_phase == False
one_neutral == True

Thus the check on line 227 is true, and so r is returned. This value was calculated earlier on lines 200 and 201 as

(diameter_over_neutral - model.neutral_strand_diameter[phase]) / 2

Gridlab-d Distance

Gridlab-d does the following:

dia_od1 = UG_GET(A, outer_diameter);
DIA(4) = UG_GET(A, neutral_diameter);
rad_14 = (dia_od1 - DIA(4)) / 24.0;
D(1, 4) = rad_14;

Solution

Divide by 24 on line 201 instead of 2. Additionally, check that the gridlab-d outer diameter is the diameter over the neutral.

Conductor to Different Phase Concentric Neutral

Python Implementation

In this case, the inputs are I = {'A'}, J = {'B', 'N'}

This leads to the following:

I ^ J = {'A', 'B', 'N'}
I & J = {}

one_neutral_same_phase == False
different_phase == True
one_neutral == True

Thus the check on line 233 is True, and so the python code returns (distance_ij**2 + r**2) ** 0.5

Gridlab-d Implementation

D(1, 5) = D(1, 2);

In otherwords, the distance from A -> BN is the same as the distance from A -> B.

Concentric Neutral to Another Concentric Neutral

Python Implementation

In this case, the inputs are I = {'A', 'N'}, J = {'B', 'N'}

This leads to the following:

I ^ J = {'A', 'B'}
I & J = {'N'}

one_neutral_same_phase == False
different_phase == False
one_neutral == False

Thus the check on line 227 and the check on line 233 are False and so distance_ij is returned

Gridlab-d Implementation

D(4, 5) = D(1, 2);

In other words, the distance from AN -> BN is the same as the distance from A -> B. The code appears to be correct here

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the thorough audit @etimberg ! A couple points here:

  • For case Conductor to Own Concentric Neutral, I think gridlab-d divides by 24 to include unit conversion from inch to ft. We handle everything using metric units, so dividing by 2 to get radius from diameter.

  • For case Conductor to Different Phase Concentric Neutral, our implementation follows the assumption and example given in the Kersting's book, approximating the concentric neutrals as one single wire directly above their respective phase conductors. So there comes the Pythagorean theorem.

Copy link
Contributor

Choose a reason for hiding this comment

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

That makes sense @veronicaguo re 2 instead of 24. For the Conductor to Different Phase Concentric Neutral case, I'm not sure that the distance calculation is correct. It assumes all the cables are in one horizontal plane because that's the only way the triangle becomes right angle. If the cables were arranged in a geometry similar to this image, the distance from the top conductor to one of the bottom concentric neutrals is formed by a non right triangle and so the extra term from the cosine law would need to be added.

Maybe a way to simplify the code here is to calculate the position (x,y) of the concentric neutral in a prior step. Then, all you'd do here is lookup distance_ij. It might be worthwhile testing this function independently too for the following cases:

  • Horizontal geometry
  • Vertical geometry
  • Bundled like the image above

I, J = set(i), set(j)
r = self.radius[i] or self.radius[j]

one_neutral_same_phase = I ^ J == set('N')
different_phase = not I & J
one_neutral = 'N' in I ^ J

if one_neutral_same_phase:
# Distance between a neutral/phase conductor of same phase
return r

distance_ij = self.calculate_distance(self.phase_positions[i],
self.phase_positions[j])
if different_phase and one_neutral:
# Distance between a neutral/phase conductor of different phase
# approximate by modelling the concentric neutral cables as one
# equivalent conductor directly above the phase conductor
return (distance_ij**2 + r**2) ** 0.5
else:
# Distance between two neutral/phase conductors
return distance_ij

def compute_X(self, i, j):
Q_first_term = super().compute_Q(i, j, 1)

# Simplify equations and don't compute Dᵢⱼ explicitly
kᵢⱼ_Dᵢⱼ_ratio = sqrt(self.ω * self.μ / self.ρ)
ΔX = Q_first_term * 2 + log(2)

if i == j:
X_o = -log(self.gmr[i]) - log(kᵢⱼ_Dᵢⱼ_ratio)
else:
X_o = -log(self.compute_d(i, j)) - log(kᵢⱼ_Dᵢⱼ_ratio)

return (X_o + ΔX) * self.ω * self.μ / (2 * π)

def GMR_cn(self, phase):
Copy link
Contributor

Choose a reason for hiding this comment

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

I audited this against gridlab-d. The gridlab-d implementation is shown below

GMRCN(4) = !(has_phase(PHASE_A) && strands_4 > 0) ? 0.0 : pow(GMR(4) * strands_4 * pow(rad_14, (strands_4 - 1)), (1.0 / strands_4));

Differences

  • Does not bail out early in the 0 case. Perhaps check GMR_s?
  • R is incorrect per the comment on the distance function.

GMR_s = self.neutral_strand_gmr[phase]
k = self.neutral_strand_count[phase]
R = self.radius[phase]
return (GMR_s * k * R**(k-1))**(1/k)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@ def readme():
'numpy>=1.13.1',
],
extras_require={
"test": ["pytest>=3.6", "pytest-cov"],
"test": ["pytest>=3.6", "pytest-cov", "pint"],
},
)
62 changes: 62 additions & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,65 @@ def wire_positions(self):
@property
def phases(self):
return self._phases


class ConcentricLineModel:
def __init__(self, conductors):
self._resistance = {}
self._geometric_mean_radius = {}
self._wire_positions = {}
self._phases = {}
self._neutral_strand_gmr = {}
self._neutral_strand_resistance = {}
self._neutral_strand_diameter = {}
self._diameter_over_neutral = {}
self._neutral_strand_count = {}

for phase, val in conductors.items():
if 'N' in phase:
self._neutral_strand_gmr[phase] = val['neutral_strand_gmr']
self._neutral_strand_resistance[phase] = val['neutral_strand_resistance'] # noqa 401
self._neutral_strand_diameter[phase] = val['neutral_strand_diameter'] # noqa 401
self._diameter_over_neutral[phase] = val['diameter_over_neutral'] # noqa 401
self._neutral_strand_count[phase] = val['neutral_strand_count']
else:
self._resistance[phase] = val['resistance']
self._geometric_mean_radius[phase] = val['gmr']
self._wire_positions[phase] = val['wire_positions']
self._phases = sorted(list(conductors.keys()))

@property
def resistance(self):
return self._resistance

@property
def geometric_mean_radius(self):
return self._geometric_mean_radius

@property
def wire_positions(self):
return self._wire_positions

@property
def phases(self):
return self._phases

@property
def neutral_strand_gmr(self):
return self._neutral_strand_gmr

@property
def neutral_strand_resistance(self):
return self._neutral_strand_resistance

@property
def neutral_strand_diameter(self):
return self._neutral_strand_diameter

@property
def diameter_over_neutral(self):
return self._diameter_over_neutral

@property
def neutral_strand_count(self):
return self._neutral_strand_count
Loading