diff --git a/README.rst b/README.rst
index 8cc679e..332aa93 100644
--- a/README.rst
+++ b/README.rst
@@ -64,7 +64,7 @@ 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: {
@@ -72,11 +72,11 @@ of the conductor for that phase.
...
}
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', ... }
@@ -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...)
@@ -101,6 +102,63 @@ For examples of how to use the model, see the `tests `_.
+
+### 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 `_.
+
+
Problem Description
-------------------
diff --git a/carsons/__init__.py b/carsons/__init__.py
index 8c46b7d..6945286 100644
--- a/carsons/__init__.py
+++ b/carsons/__init__.py
@@ -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"
diff --git a/carsons/carsons.py b/carsons/carsons.py
index 771b4f7..0f98fab 100755
--- a/carsons/carsons.py
+++ b/carsons/carsons.py
@@ -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):
@@ -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.
@@ -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
@@ -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
@@ -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):
+ 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):
+ 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)
diff --git a/setup.py b/setup.py
index 358fa2e..8ecb99d 100644
--- a/setup.py
+++ b/setup.py
@@ -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"],
},
)
diff --git a/tests/helpers.py b/tests/helpers.py
index 62a0bf8..5bed7ad 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -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
diff --git a/tests/test_concentric_neutral_cable.py b/tests/test_concentric_neutral_cable.py
new file mode 100644
index 0000000..abfad1b
--- /dev/null
+++ b/tests/test_concentric_neutral_cable.py
@@ -0,0 +1,217 @@
+import pint
+from numpy import array
+from numpy.testing import assert_array_almost_equal
+
+from carsons import ConcentricNeutralCarsonsEquations, impedance
+from tests.helpers import ConcentricLineModel
+from tests.test_carsons import OHM_PER_MILE_TO_OHM_PER_METER
+
+ureg = pint.UnitRegistry()
+
+feet = ureg.feet
+inches = ureg.inches
+miles = ureg.miles
+ohms = ureg.ohms
+kft = ureg.feet * 1000
+
+
+def test_concentric_neutral_cable():
+ """
+ Validation test against example in Kersting's book.
+ """
+ model = ConcentricNeutralCarsonsEquations(ConcentricLineModel({
+ "A": {
+ 'resistance': (0.4100*(ohms / miles)).to('ohm / meters').magnitude,
+ 'gmr': (0.0171*feet).to('meters').magnitude,
+ 'wire_positions': (0, 0)
+ },
+ "B": {
+ 'resistance': (0.4100*(ohms / miles)).to('ohm / meters').magnitude,
+ 'gmr': (0.0171*feet).to('meters').magnitude,
+ 'wire_positions': ((6*inches).to('meters').magnitude, 0)
+ },
+ "C": {
+ 'resistance': (0.4100*(ohms / miles)).to('ohm / meters').magnitude,
+ 'gmr': (0.0171*feet).to('meters').magnitude,
+ 'wire_positions': ((12*inches).to('meters').magnitude, 0)
+ },
+
+ "NA": {
+ 'neutral_strand_gmr': (0.00208*feet).to('meters').magnitude,
+ 'neutral_strand_resistance':
+ (14.87*ohms / miles).to('ohm / meters').magnitude,
+ 'neutral_strand_diameter': (0.0641*inches).to('meters').magnitude,
+ 'diameter_over_neutral': (1.29*inches).to('meters').magnitude,
+ 'neutral_strand_count': 13,
+ },
+ "NB": {
+ 'neutral_strand_gmr': (0.00208*feet).to('meters').magnitude,
+ 'neutral_strand_resistance':
+ (14.87*ohms / miles).to('ohm / meters').magnitude,
+ 'neutral_strand_diameter': (0.0641*inches).to('meters').magnitude,
+ 'diameter_over_neutral': (1.29*inches).to('meters').magnitude,
+ 'neutral_strand_count': 13,
+ },
+ "NC": {
+ 'neutral_strand_gmr': (0.00208*feet).to('meters').magnitude,
+ 'neutral_strand_resistance':
+ (14.87*ohms / miles).to('ohm / meters').magnitude,
+ 'neutral_strand_diameter': (0.0641*inches).to('meters').magnitude,
+ 'diameter_over_neutral': (1.29*inches).to('meters').magnitude,
+ 'neutral_strand_count': 13,
+ },
+ }))
+
+ assert_array_almost_equal(
+ impedance(model),
+ array([
+ [0.7981 + 1j*0.4467, 0.3188 + 1j*0.0334, 0.2848 + 1j*0.0138],
+ [0.3188 + 1j*0.0334, 0.7890 + 1j*0.4048, 0.3188 + 1j*0.0334],
+ [0.2848 + 1j*0.0138, 0.3188 + 1j*0.0334, 0.7981 + 1j*0.4467],
+
+ ]) * OHM_PER_MILE_TO_OHM_PER_METER,
+ decimal=4
+ )
+
+
+def test_concentric_neutral_cable_IEEE37():
+ """
+ Validation test against IEEE37 network underground cable configuration 723.
+ """
+
+ model = ConcentricNeutralCarsonsEquations(ConcentricLineModel({
+ "A": {
+ 'resistance': (0.7690 * (ohms/miles)).to('ohm / meters').magnitude,
+ 'gmr': (0.0125 * feet).to('meters').magnitude,
+ 'wire_positions': (0, 0)
+ },
+ "B": {
+ 'resistance': (0.7690 * (ohms/miles)).to('ohm / meters').magnitude,
+ 'gmr': (0.0125 * feet).to('meters').magnitude,
+ 'wire_positions': ((6 * inches).to('meters').magnitude, 0)
+ },
+ "C": {
+ 'resistance': (0.7690 * (ohms/miles)).to('ohm / meters').magnitude,
+ 'gmr': (0.0125 * feet).to('meters').magnitude,
+ 'wire_positions': ((12 * inches).to('meters').magnitude, 0)
+ },
+
+ "NA": {
+ 'neutral_strand_gmr': (0.00208 * feet).to('meters').magnitude,
+ 'neutral_strand_resistance':
+ (14.87 * ohms / miles).to('ohm / meters').magnitude,
+ 'neutral_strand_diameter': (0.0641*inches).to('meters').magnitude,
+ 'diameter_over_neutral': (1.10 * inches).to('meters').magnitude,
+ 'neutral_strand_count': 7,
+ },
+ "NB": {
+ 'neutral_strand_gmr': (0.00208 * feet).to('meters').magnitude,
+ 'neutral_strand_resistance':
+ (14.87 * ohms / miles).to('ohm / meters').magnitude,
+ 'neutral_strand_diameter': (0.0641*inches).to('meters').magnitude,
+ 'diameter_over_neutral': (1.10 * inches).to('meters').magnitude,
+ 'neutral_strand_count': 7,
+ },
+ "NC": {
+ 'neutral_strand_gmr': (0.00208 * feet).to('meters').magnitude,
+ 'neutral_strand_resistance':
+ (14.87 * ohms / miles).to('ohm / meters').magnitude,
+ 'neutral_strand_diameter': (0.0641*inches).to('meters').magnitude,
+ 'diameter_over_neutral': (1.10 * inches).to('meters').magnitude,
+ 'neutral_strand_count': 7,
+ },
+ }))
+
+ assert_array_almost_equal(
+ impedance(model),
+ array([
+ [1.2936 + 1j*0.6713, 0.4871 + 1j*0.2111, 0.4585 + 1j*0.1521],
+ [0.4871 + 1j*0.2111, 1.3022 + 1j*0.6326, 0.4871 + 1j*0.2111],
+ [0.4585 + 1j*0.1521, 0.4871 + 1j*0.2111, 1.2936 + 1j*0.6713],
+
+ ]) * OHM_PER_MILE_TO_OHM_PER_METER,
+ decimal=4
+ )
+
+
+def test_2ph_concentric_neutral_cable():
+ """
+ Validation test against OpenDSS example found in documentation
+ http://svn.code.sf.net/p/electricdss/code/trunk/Distrib/Doc/
+ 'TechNote CableModelling.pdf' - Practical Example: Concentric Neutral Cable
+ """
+
+ model = ConcentricNeutralCarsonsEquations(ConcentricLineModel({
+ "A": {
+ 'resistance': (0.0776 * (ohms/kft)).to('ohm / meters').magnitude,
+ 'gmr': (0.205 * inches).to('meters').magnitude,
+ 'wire_positions': (0, 0)
+ },
+ "B": {
+ 'resistance': (0.0776 * (ohms/kft)).to('ohm / meters').magnitude,
+ 'gmr': (0.205 * inches).to('meters').magnitude,
+ 'wire_positions': ((6 * inches).to('meters').magnitude, 0)
+ },
+ "NA": {
+ 'neutral_strand_gmr': (0.02496 * inches).to('meters').magnitude,
+ 'neutral_strand_resistance':
+ (2.55 * (ohms/kft)).to('ohm / meters').magnitude,
+ 'neutral_strand_diameter': (0.064*inches).to('meters').magnitude,
+ 'diameter_over_neutral': (1.29 * inches).to('meters').magnitude,
+ 'neutral_strand_count': 13,
+ },
+ "NB": {
+ 'neutral_strand_gmr': (0.02496 * inches).to('meters').magnitude,
+ 'neutral_strand_resistance':
+ (2.55 * (ohms/kft)).to('ohm / meters').magnitude,
+ 'neutral_strand_diameter': (0.064*inches).to('meters').magnitude,
+ 'diameter_over_neutral': (1.29 * inches).to('meters').magnitude,
+ 'neutral_strand_count': 13,
+ },
+ }))
+
+ assert_array_almost_equal(
+ impedance(model),
+ array([
+ [0.867953 + 1j*0.442045, 0.389392 + 1j*0.0511399, 0 + 1j * 0],
+ [0.389392 + 1j*0.0511399, 0.867953 + 1j*0.442045, 0 + 1j * 0],
+ [0 + 1j * 0, 0 + 1j * 0, 0 + 1j * 0],
+
+ ]) * OHM_PER_MILE_TO_OHM_PER_METER,
+ decimal=4
+ )
+
+
+def test_1ph_concentric_neutral_cable():
+ """
+ Validation test against OpenDSS example found in documentation
+ http://svn.code.sf.net/p/electricdss/code/trunk/Distrib/Doc/
+ 'TechNote CableModelling.pdf' - Practical Example: Concentric Neutral Cable
+ """
+
+ model = ConcentricNeutralCarsonsEquations(ConcentricLineModel({
+ "A": {
+ 'resistance': (0.0776 * (ohms/kft)).to('ohm / meters').magnitude,
+ 'gmr': (0.205 * inches).to('meters').magnitude,
+ 'wire_positions': (0, 0)
+ },
+ "NA": {
+ 'neutral_strand_gmr': (0.02496 * inches).to('meters').magnitude,
+ 'neutral_strand_resistance':
+ (2.55 * (ohms/kft)).to('ohm / meters').magnitude,
+ 'neutral_strand_diameter': (0.064*inches).to('meters').magnitude,
+ 'diameter_over_neutral': (1.29 * inches).to('meters').magnitude,
+ 'neutral_strand_count': 13,
+ },
+ }))
+
+ assert_array_almost_equal(
+ impedance(model),
+ array([
+ [1.04185 + 1j*0.602329, 0 + 1j * 0, 0 + 1j * 0],
+ [0 + 1j * 0, 0 + 1j * 0, 0 + 1j * 0],
+ [0 + 1j * 0, 0 + 1j * 0, 0 + 1j * 0],
+
+ ]) * OHM_PER_MILE_TO_OHM_PER_METER,
+ decimal=4
+ )