Skip to content

Commit 9eb2fa8

Browse files
authored
Merge pull request #38 from opusonesolutions/multi-conductor
Support multi-conductor cables
2 parents 6110f12 + 9377802 commit 9eb2fa8

File tree

5 files changed

+314
-30
lines changed

5 files changed

+314
-30
lines changed

README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,72 @@ cable_impedance = calculate_impedance(ConcentricNeutralCarsonsEquations(Cable())
146146
For examples of how to use the model, see the [concentric cable
147147
tests](https://github.com/opusonesolutions/carsons/blob/master/tests/test_concentric_neutral_cable.py).
148148

149+
### Multi-Conductor Cable
150+
151+
`carsons` also supports modelling of phased duplex, triplex, quadruplex cables and triplex secondary.
152+
It only requires a few more parameters to describe cable's geometry.
153+
154+
```python
155+
from carsons import (MultiConductorCarsonsEquations,
156+
calculate_impedance)
157+
158+
class Cable:
159+
resistance: {
160+
'A': per-length resistance of conductor A in ohm/meters
161+
...
162+
}
163+
geometric_mean_radius: {
164+
'A': geometric mean radius of conductor A in meters
165+
...
166+
}
167+
wire_positions: {
168+
'A': (x, y) cross-sectional position of conductor A in meters
169+
...
170+
}
171+
radius: {
172+
'A': radius of conductor A
173+
...
174+
}
175+
insulation_thickness: {
176+
'A': insulation thickness of conductor A
177+
...
178+
}
179+
phases: {'A', ... }
180+
181+
cable_impedance = calculate_impedance(MultiConductorCarsonsEquations(Cable()))
182+
```
183+
184+
To model a triplex secondary cable, the inputs should be keyed on secondary conductors `S1` and `S2`. The impedance result
185+
is a 2 x 2 matrix.
186+
187+
```python
188+
class Cable:
189+
resistance: {
190+
'S1': per-length resistance of conductor S1 in ohm/meters
191+
...
192+
}
193+
geometric_mean_radius: {
194+
'S1': geometric mean radius of conductor S1 in meters
195+
...
196+
}
197+
wire_positions: {
198+
'S1': (x, y) cross-sectional position of conductor S1 in meters
199+
...
200+
}
201+
radius: {
202+
'S1': radius of conductor S1
203+
...
204+
}
205+
insulation_thickness: {
206+
'S1': insulation thickness of conductor S1
207+
...
208+
}
209+
phases: {'S1', ... }
210+
```
211+
212+
For examples of how to use the model, see the [multi-conductor cable
213+
tests](https://github.com/opusonesolutions/carsons/blob/master/tests/test_multi_conductor.py).
214+
149215
Problem Description
150216
-------------------
151217

carsons/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from carsons.carsons import (convert_geometric_model, # noqa 401
2-
calculate_impedance, # noqa 401
3-
ConcentricNeutralCarsonsEquations) # noqa 401
2+
calculate_impedance, # noqa 401
3+
ConcentricNeutralCarsonsEquations, # noqa 401
4+
MultiConductorCarsonsEquations) # noqa 401
45

56
name = "carsons"

carsons/carsons.py

Lines changed: 86 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ def convert_geometric_model(geometric_model) -> ndarray:
1818

1919
def calculate_impedance(model) -> ndarray:
2020
z_primitive = model.build_z_primitive()
21-
z_abc = perform_kron_reduction(z_primitive)
21+
z_abc = perform_kron_reduction(z_primitive, dimension=model.dimension)
22+
2223
return z_abc
2324

2425

25-
def perform_kron_reduction(z_primitive: ndarray) -> ndarray:
26+
def perform_kron_reduction(z_primitive: ndarray, dimension=3) -> ndarray:
2627
""" Reduces the primitive impedance matrix to an equivalent impedance
2728
matrix.
2829
@@ -56,8 +57,10 @@ def perform_kron_reduction(z_primitive: ndarray) -> ndarray:
5657
[Zba, Zbb, Zbc]
5758
[Zca, Zcb, Zcc]
5859
"""
59-
Ẑpp, Ẑpn = z_primitive[0:3, 0:3], z_primitive[0:3, 3:]
60-
Ẑnp, Ẑnn = z_primitive[3:, 0:3], z_primitive[3:, 3:]
60+
Ẑpp, Ẑpn = (z_primitive[0:dimension, 0:dimension],
61+
z_primitive[0:dimension, dimension:])
62+
Ẑnp, Ẑnn = (z_primitive[dimension:, 0:dimension],
63+
z_primitive[dimension:, dimension:])
6164
Z_abc = Ẑpp - Ẑpn @ inv(Ẑnn) @ Ẑnp
6265
return Z_abc
6366

@@ -78,17 +81,11 @@ def __init__(self, model):
7881
self.ω = 2.0 * π * self.ƒ # angular frequency radians / second
7982

8083
def build_z_primitive(self) -> ndarray:
81-
neutral_conductors = sorted([
82-
ph for ph in self.phases
83-
if ph.startswith("N")
84-
])
85-
conductors = ["A", "B", "C"] + neutral_conductors
86-
87-
dimension = len(conductors)
84+
dimension = len(self.conductors)
8885
z_primitive = zeros(shape=(dimension, dimension), dtype=complex)
8986

90-
for index_i, phase_i in enumerate(conductors):
91-
for index_j, phase_j in enumerate(conductors):
87+
for index_i, phase_i in enumerate(self.conductors):
88+
for index_j, phase_j in enumerate(self.conductors):
9289
if phase_i not in self.phases or phase_j not in self.phases:
9390
continue
9491
R = self.compute_R(phase_i, phase_j)
@@ -190,8 +187,46 @@ def get_h(self, i):
190187
_, yᵢ = self.phase_positions[i]
191188
return yᵢ
192189

190+
@property
191+
def dimension(self):
192+
return 2 if getattr(self, 'is_secondary', False) else 3
193+
194+
@property
195+
def conductors(self):
196+
neutral_conductors = sorted([
197+
ph for ph in self.phases
198+
if ph.startswith("N")
199+
])
200+
201+
return ["A", "B", "C"] + neutral_conductors
202+
203+
204+
class ModifiedCarsonsEquations(CarsonsEquations):
205+
"""
206+
Modified Carson's Equation. Two approximations are made:
207+
only the first term of P and the first two terms of Q are considered.
208+
"""
209+
number_of_P_terms = 1
210+
211+
def compute_P(self, i, j, number_of_terms=1) -> float:
212+
return super().compute_P(i, j, self.number_of_P_terms)
213+
214+
def compute_X(self, i, j) -> float:
215+
Q_first_term = super().compute_Q(i, j, 1)
216+
217+
# Simplify equations and don't compute Dᵢⱼ explicitly
218+
kᵢⱼ_Dᵢⱼ_ratio = sqrt(self.ω * self.μ / self.ρ)
219+
ΔX = Q_first_term * 2 + log(2)
220+
221+
if i == j:
222+
X_o = -log(self.gmr[i]) - log(kᵢⱼ_Dᵢⱼ_ratio)
223+
else:
224+
X_o = -log(self.compute_d(i, j)) - log(kᵢⱼ_Dᵢⱼ_ratio)
225+
226+
return (X_o + ΔX) * self.ω * self.μ / (2 * π)
227+
193228

194-
class ConcentricNeutralCarsonsEquations(CarsonsEquations):
229+
class ConcentricNeutralCarsonsEquations(ModifiedCarsonsEquations):
195230
def __init__(self, model, *args, **kwargs):
196231
super().__init__(model)
197232
self.neutral_strand_gmr: Dict[str, float] = model.neutral_strand_gmr
@@ -246,22 +281,45 @@ def compute_d(self, i, j) -> float:
246281
# Distance between two neutral/phase conductors
247282
return distance_ij
248283

249-
def compute_X(self, i, j) -> float:
250-
Q_first_term = super().compute_Q(i, j, 1)
251-
252-
# Simplify equations and don't compute Dᵢⱼ explicitly
253-
kᵢⱼ_Dᵢⱼ_ratio = sqrt(self.ω * self.μ / self.ρ)
254-
ΔX = Q_first_term * 2 + log(2)
255-
256-
if i == j:
257-
X_o = -log(self.gmr[i]) - log(kᵢⱼ_Dᵢⱼ_ratio)
258-
else:
259-
X_o = -log(self.compute_d(i, j)) - log(kᵢⱼ_Dᵢⱼ_ratio)
260-
261-
return (X_o + ΔX) * self.ω * self.μ / (2 * π)
262-
263284
def GMR_cn(self, phase) -> float:
264285
GMR_s = self.neutral_strand_gmr[phase]
265286
k = self.neutral_strand_count[phase]
266287
R = self.radius[phase]
267288
return (GMR_s * k * R**(k-1))**(1/k)
289+
290+
291+
class MultiConductorCarsonsEquations(ModifiedCarsonsEquations):
292+
def __init__(self, model):
293+
super().__init__(model)
294+
self.radius: Dict[str, float] = model.radius
295+
self.insulation_thickness: Dict[str, float] = \
296+
model.insulation_thickness
297+
298+
def compute_d(self, i, j) -> float:
299+
# Assumptions:
300+
# 1. All conductors in the cable are touching each other and
301+
# therefore equidistant.
302+
# 2. In case of quadruplex cables, the space between conductors
303+
# which are diagonally positioned is neglected.
304+
return (self.radius[i] + self.radius[j]
305+
+ self.insulation_thickness[i] + self.insulation_thickness[j])
306+
307+
@property
308+
def conductors(self):
309+
neutral_conductors = sorted([
310+
ph for ph in self.phases if ph.startswith("N")
311+
])
312+
if self.is_secondary:
313+
conductors = ["S1", "S2"] + neutral_conductors
314+
else:
315+
conductors = ["A", "B", "C"] + neutral_conductors
316+
317+
return conductors
318+
319+
@property
320+
def is_secondary(self):
321+
phase_conductors = [ph for ph in self.phases if not ph.startswith('N')]
322+
if phase_conductors == ["S1", "S2"]:
323+
return True
324+
else:
325+
return False

tests/helpers.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,45 @@ def diameter_over_neutral(self):
8888
@property
8989
def neutral_strand_count(self):
9090
return self._neutral_strand_count
91+
92+
93+
class MultiLineModel:
94+
def __init__(self, conductors, is_secondary=False):
95+
self._resistance = {}
96+
self._geometric_mean_radius = {}
97+
self._wire_positions = {}
98+
self._radius = {}
99+
self._insulation_thickness = {}
100+
101+
for phase, val in conductors.items():
102+
self._resistance[phase] = val['resistance']
103+
self._geometric_mean_radius[phase] = val['gmr']
104+
self._wire_positions[phase] = val['wire_positions']
105+
self._radius[phase] = val['radius']
106+
self._insulation_thickness[phase] = val['insulation_thickness']
107+
108+
self._phases = sorted(list(conductors.keys()))
109+
110+
@property
111+
def resistance(self):
112+
return self._resistance
113+
114+
@property
115+
def geometric_mean_radius(self):
116+
return self._geometric_mean_radius
117+
118+
@property
119+
def wire_positions(self):
120+
return self._wire_positions
121+
122+
@property
123+
def phases(self):
124+
return self._phases
125+
126+
@property
127+
def radius(self):
128+
return self._radius
129+
130+
@property
131+
def insulation_thickness(self):
132+
return self._insulation_thickness

0 commit comments

Comments
 (0)