-
Notifications
You must be signed in to change notification settings - Fork 4
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
Changes from 16 commits
5187fa1
8845a58
949a7fd
18fe874
8351582
9c9c85f
7ffb2b0
c6a6ed6
9d7e4ed
04b4c1a
d2193b1
11c786a
18a8d84
c1c5a13
53fa95f
7cdfd20
723ff87
c578e9a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
from carsons.carsons import convert_geometric_model # noqa 401 | ||
from carsons.carsons import (convert_geometric_model, # noqa 401 | ||
ConcentricNeutralCarsonsEquations) # noqa 401 | ||
|
||
name = "carsons" |
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): | ||||||||||||||||||
|
@@ -107,16 +103,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 | ||||||||||||||||||
|
||||||||||||||||||
|
@@ -172,6 +169,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 | ||||||||||||||||||
|
@@ -183,3 +181,80 @@ def calculate_distance(positionᵢ, positionⱼ): | |||||||||||||||||
def get_h(self, i): | ||||||||||||||||||
_, yᵢ = self.phase_positions[i] | ||||||||||||||||||
return yᵢ | ||||||||||||||||||
|
||||||||||||||||||
@property | ||||||||||||||||||
def impedance(self): | ||||||||||||||||||
z_primitive = self.build_z_primitive() | ||||||||||||||||||
z_abc = perform_kron_reduction(z_primitive) | ||||||||||||||||||
return z_abc | ||||||||||||||||||
|
||||||||||||||||||
|
||||||||||||||||||
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): | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comparison to Gridlab-dTo 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. BackgroundGridlab-d appears to use the following indexes for different phases. The snippets below use these phase indexes
Conductor to Neutral CablePython ImplementationIn this case, the inputs are 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 Gridlab-D ImplementationHowever, 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); SolutionI think the correct solution in this case is to return Conductor to Own Concentric NeutralPython ImplementationIn this case, the inputs are 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 (diameter_over_neutral - model.neutral_strand_diameter[phase]) / 2 Gridlab-d DistanceGridlab-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; SolutionDivide 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 NeutralPython ImplementationIn this case, the inputs are 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 Gridlab-d ImplementationD(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 NeutralPython ImplementationIn this case, the inputs are 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 Gridlab-d ImplementationD(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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the thorough audit @etimberg ! A couple points here:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense @veronicaguo re 2 instead of 24. For the 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
|
||||||||||||||||||
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): | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||||||||||||||
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think this new property should be tested. perhaps also deprecate the
convert_geometric_model
functionThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it is called on the concentric neutral. its just added here because it doesn't depend on anything that
Carsons
doesn't have.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
exposed
impedance
as a stand-alone function in 723ff87