ElecSolver 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ElecSolver/FrequencySystemBuilder.py +264 -0
- ElecSolver/TemporalSystemBuilder.py +462 -0
- ElecSolver/__init__.py +7 -0
- ElecSolver/_version.py +21 -0
- ElecSolver/utils.py +149 -0
- elecsolver-0.1.0.dist-info/METADATA +260 -0
- elecsolver-0.1.0.dist-info/RECORD +10 -0
- elecsolver-0.1.0.dist-info/WHEEL +5 -0
- elecsolver-0.1.0.dist-info/licenses/LICENCE.txt +21 -0
- elecsolver-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,264 @@
|
|
1
|
+
import numpy as np
|
2
|
+
import networkx as nx
|
3
|
+
from scipy.sparse import coo_matrix
|
4
|
+
|
5
|
+
|
6
|
+
class FrequencySystemBuilder():
|
7
|
+
def __init__(self,impedence_coords,impedence_data,mutuals_coords,mutuals_data):
|
8
|
+
"""FrequencySystemBuilder class for building an electrical sparse system
|
9
|
+
that can be solved by any sparse solver
|
10
|
+
it supports all forms of complex making this class fit for non linear impedences
|
11
|
+
|
12
|
+
Parameters
|
13
|
+
----------
|
14
|
+
impedence_coords : np.array of ints, shape = (2,N)
|
15
|
+
impedence coordinates
|
16
|
+
impedence_data : np.array of complex, shape = (N,)
|
17
|
+
impedence value between points impedence_coords[:,i]
|
18
|
+
mutuals_coords : np.array of ints, shape = (2,M)
|
19
|
+
indexes of the impedence within impedence data which have a mutual
|
20
|
+
mutuals_data : np.array of complex, shape=(M,)
|
21
|
+
mutual value between impedences impedence_data[mutuals_coords[0,i]] impedence_data[mutuals_coords[1,i]]
|
22
|
+
The mutual follows the order given in impedence coords
|
23
|
+
"""
|
24
|
+
all_points = np.unique(impedence_coords)
|
25
|
+
if all_points.shape != np.max(impedence_coords)+1:
|
26
|
+
IndexError("There is one or multiple lonely nodes please clean your impedence graph")
|
27
|
+
self.impedence_coords = impedence_coords
|
28
|
+
self.impedence_data = impedence_data
|
29
|
+
self.mutuals_coords = mutuals_coords
|
30
|
+
self.mutuals_data = mutuals_data
|
31
|
+
self.size = np.max(impedence_coords)+1
|
32
|
+
self.number_intensities = self.impedence_data.shape[0]
|
33
|
+
## making graph and checking number of subgraphs
|
34
|
+
unique_coords = np.unique(self.impedence_coords,axis=1)
|
35
|
+
sym_graph = np.concatenate((unique_coords,np.stack((unique_coords[1],unique_coords[0]),axis=0)),axis=1)
|
36
|
+
links = np.ones(sym_graph.shape[1])
|
37
|
+
self.graph = nx.from_scipy_sparse_array(coo_matrix((links,(sym_graph[0],sym_graph[1]))))
|
38
|
+
self.list_of_subgraphs = [ list(sub) for sub in nx.connected_components(self.graph)]
|
39
|
+
self.number_of_subsystems = len(self.list_of_subgraphs)
|
40
|
+
self.affected_potentials = [-1]*self.number_of_subsystems
|
41
|
+
self.deleted_equation_current = [subsystem[0] for subsystem in self.list_of_subgraphs]
|
42
|
+
## number of tension sources
|
43
|
+
self.source_count = 0
|
44
|
+
self.source_signs = np.array([],dtype=int)
|
45
|
+
self.rhs = (np.array([]),(np.array([],dtype=int),))
|
46
|
+
rescaler = np.zeros(self.size)
|
47
|
+
rescaler[self.deleted_equation_current]=1
|
48
|
+
rescaler = -np.cumsum(rescaler)
|
49
|
+
self.rescaler =rescaler.astype(int)
|
50
|
+
offset_j = self.impedence_data.shape[0]
|
51
|
+
offset_i = self.size-len(self.deleted_equation_current)
|
52
|
+
self.offset_i = offset_i
|
53
|
+
self.offset_j = offset_j
|
54
|
+
|
55
|
+
def set_mass(self,*args):
|
56
|
+
for index in args:
|
57
|
+
for pivot,subsystem in enumerate(self.list_of_subgraphs):
|
58
|
+
if index in subsystem:
|
59
|
+
if self.affected_potentials[pivot]!=-1:
|
60
|
+
print(f"Subsystem {pivot} already add a mass, reaffecting the value")
|
61
|
+
self.affected_potentials[pivot]=index
|
62
|
+
break
|
63
|
+
|
64
|
+
def affect_potentials(self):
|
65
|
+
"""Function to check whether the masses were affected and assign some if not
|
66
|
+
"""
|
67
|
+
for i in range(len(self.affected_potentials)):
|
68
|
+
if -1 == self.affected_potentials[i]:
|
69
|
+
self.affected_potentials[i]= self.list_of_subgraphs[i][0]
|
70
|
+
print(f"Subsytem {i} has not been affected to the mass, we chose {self.list_of_subgraphs[i][0]}")
|
71
|
+
|
72
|
+
def build_system(self):
|
73
|
+
"""Building sytem assuming that data was given as COO matrices
|
74
|
+
Fully vectorized for best performance
|
75
|
+
it is faster but less understandable
|
76
|
+
"""
|
77
|
+
## affecting masses if need be
|
78
|
+
self.affect_potentials()
|
79
|
+
## Building a sparse COO matrix
|
80
|
+
|
81
|
+
|
82
|
+
## Building all vectorized values necessary
|
83
|
+
i_s_vals = np.max(self.impedence_coords,axis=0)
|
84
|
+
j_s_vals = np.min(self.impedence_coords,axis=0)
|
85
|
+
values = self.impedence_data
|
86
|
+
|
87
|
+
|
88
|
+
## node laws
|
89
|
+
data_nodes = np.concatenate((np.ones(self.number_intensities),-np.ones(self.number_intensities)),axis=0)
|
90
|
+
j_s_nodes = np.tile(np.arange(self.number_intensities,dtype=int),(2,))
|
91
|
+
i_s_nodes =np.concatenate((i_s_vals,j_s_vals),axis=0)
|
92
|
+
|
93
|
+
# Removing one current equation per subsytem
|
94
|
+
mask_removed_eq = ~np.isin(i_s_nodes,self.deleted_equation_current)
|
95
|
+
data_nodes = data_nodes[mask_removed_eq]
|
96
|
+
j_s_nodes = j_s_nodes[mask_removed_eq]
|
97
|
+
i_s_nodes = i_s_nodes[mask_removed_eq]
|
98
|
+
i_s_nodes = i_s_nodes + self.rescaler[i_s_nodes]
|
99
|
+
|
100
|
+
|
101
|
+
|
102
|
+
## Kirchoff
|
103
|
+
i_s_edges = self.offset_i + np.concatenate([np.arange(self.number_intensities,dtype=int)]*3,axis=0 )
|
104
|
+
j_s_edges = np.concatenate([self.offset_j+i_s_vals,self.offset_j+j_s_vals,np.arange(self.number_intensities,dtype=int)],axis=0)
|
105
|
+
data_edges = np.concatenate([np.ones(values.shape[0],dtype=complex),-np.ones(values.shape[0],dtype=complex),values],axis=0)
|
106
|
+
|
107
|
+
|
108
|
+
|
109
|
+
## adding mutuals to the system
|
110
|
+
sign = np.sign(self.impedence_coords[0]-self.impedence_coords[1])
|
111
|
+
|
112
|
+
|
113
|
+
i_s_additionnal = self.offset_i + np.concatenate((self.mutuals_coords[0],self.mutuals_coords[1]),axis=0)
|
114
|
+
j_s_additionnal = np.concatenate((self.mutuals_coords[1],self.mutuals_coords[0]),axis=0)
|
115
|
+
data_additionnal = np.tile(self.mutuals_data*sign[self.mutuals_coords[0]]*sign[self.mutuals_coords[1]],(2,))
|
116
|
+
|
117
|
+
|
118
|
+
## mass equations (1 per subsystem)
|
119
|
+
i_s_mass = np.arange(self.offset_i+self.offset_j,self.offset_i+self.offset_j+len(self.affected_potentials))
|
120
|
+
j_s_mass = self.offset_j+np.array(self.affected_potentials)
|
121
|
+
data_mass = np.ones(len(self.affected_potentials))
|
122
|
+
|
123
|
+
|
124
|
+
i_s = np.concatenate((i_s_nodes,i_s_edges,i_s_additionnal,i_s_mass),axis=0)
|
125
|
+
j_s = np.concatenate((j_s_nodes,j_s_edges,j_s_additionnal,j_s_mass),axis=0)
|
126
|
+
data = np.concatenate((data_nodes,data_edges,data_additionnal,data_mass),axis=0)
|
127
|
+
|
128
|
+
self.system = (data,(i_s.astype(int),j_s.astype(int)))
|
129
|
+
return self.system
|
130
|
+
|
131
|
+
def get_system(self):
|
132
|
+
"""Function to get the system
|
133
|
+
Returns
|
134
|
+
-------
|
135
|
+
sys: scipy.coo_matrix
|
136
|
+
Linear system to solve
|
137
|
+
rhs: np.ndarray
|
138
|
+
Second member of the system
|
139
|
+
"""
|
140
|
+
(data_rhs,(nodes,)) = self.rhs
|
141
|
+
(data,(i_s,j_s)) = self.system
|
142
|
+
sys = coo_matrix(self.system)
|
143
|
+
rhs = np.zeros(self.number_intensities+self.size+self.source_count)
|
144
|
+
np.add.at(rhs, nodes, data_rhs)
|
145
|
+
return sys,rhs
|
146
|
+
|
147
|
+
def build_second_member_intensity(self,intensity,input_node,output_node):
|
148
|
+
"""Function to build a second member for the scenario of current injection in the sparse system
|
149
|
+
|
150
|
+
Parameters
|
151
|
+
----------
|
152
|
+
intensity : float
|
153
|
+
intensity to inject
|
154
|
+
input_node : int
|
155
|
+
which node to take for injection
|
156
|
+
output_node : int
|
157
|
+
which node for current retrieval
|
158
|
+
|
159
|
+
Returns
|
160
|
+
-------
|
161
|
+
Tuple(np.array,(np.array))
|
162
|
+
second member of the linear system in a COO like format
|
163
|
+
"""
|
164
|
+
data = []
|
165
|
+
nodes = []
|
166
|
+
if self.number_of_subsystems>=2:
|
167
|
+
valid = False
|
168
|
+
for system in self.list_of_subgraphs:
|
169
|
+
if input_node in system and output_node in system:
|
170
|
+
valid =True
|
171
|
+
else:
|
172
|
+
continue
|
173
|
+
if not valid:
|
174
|
+
raise IndexError("Nodes indicated do not belong to the same subsystem")
|
175
|
+
|
176
|
+
if input_node not in self.deleted_equation_current:
|
177
|
+
nodes.append(input_node+self.rescaler[input_node])
|
178
|
+
data.append(-intensity) # inbound intensity
|
179
|
+
if output_node not in self.deleted_equation_current:
|
180
|
+
nodes.append(output_node+self.rescaler[output_node])
|
181
|
+
data.append(intensity) # outbound intensity
|
182
|
+
(data_rhs,(nodes_rhs,)) = self.rhs
|
183
|
+
self.rhs = (np.append(data_rhs,np.array(data),axis=0),(np.append(nodes_rhs,np.array(nodes),axis=0),))
|
184
|
+
return self.rhs
|
185
|
+
|
186
|
+
def build_second_member_tension(self,tension,input_node,output_node):
|
187
|
+
"""building second member in the case of an enforced tension
|
188
|
+
To be able to enforce a tension we need to remove one of the equation in the system
|
189
|
+
and add the tension enforced equation
|
190
|
+
By default we remove the first kirchoff law within the correct subsystem
|
191
|
+
Warning this function changes the system, please re-get the system before solving it (call get_system)
|
192
|
+
|
193
|
+
Parameters
|
194
|
+
----------
|
195
|
+
tension : complex
|
196
|
+
enforced tension
|
197
|
+
input_node : int
|
198
|
+
node where the tension is enforce
|
199
|
+
output_node : int
|
200
|
+
node from where the tension is enforced
|
201
|
+
|
202
|
+
Returns
|
203
|
+
-------
|
204
|
+
Tuple(np.array,(np.array))
|
205
|
+
second member of the linear system in a COO like format
|
206
|
+
"""
|
207
|
+
targeted_subsystem = 0
|
208
|
+
if self.number_of_subsystems>=2:
|
209
|
+
valid = False
|
210
|
+
for index_sub,system in enumerate(self.list_of_subgraphs):
|
211
|
+
if input_node in system and output_node in system:
|
212
|
+
valid =True
|
213
|
+
targeted_subsystem=index_sub
|
214
|
+
else:
|
215
|
+
continue
|
216
|
+
if not valid:
|
217
|
+
raise IndexError("Nodes indicated do not belong to the same subsystem")
|
218
|
+
print("Warning this function changes the system, please re-evaluate it before solving the system (call get_system)")
|
219
|
+
(data,(i_s,j_s)) = self.system
|
220
|
+
(data_rhs,(nodes,)) = self.rhs
|
221
|
+
|
222
|
+
new_eq = self.number_intensities+self.size +self.source_count
|
223
|
+
sign = np.sign(output_node-input_node)
|
224
|
+
|
225
|
+
## Adding the new equation in S1
|
226
|
+
if input_node in self.deleted_equation_current:
|
227
|
+
data = np.append(data,np.array([1.,-1.,-sign]),axis=0)
|
228
|
+
i_s = np.append(i_s,np.array([new_eq,new_eq,output_node+self.rescaler[output_node]]),axis=0)
|
229
|
+
j_s = np.append(j_s,np.array([self.offset_j + input_node,self.offset_j+output_node,new_eq]),axis=0)
|
230
|
+
elif output_node in self.deleted_equation_current:
|
231
|
+
data = np.append(data,np.array([1.,-1.,sign]),axis=0)
|
232
|
+
i_s = np.append(i_s,np.array([new_eq,new_eq,input_node+self.rescaler[input_node]]),axis=0)
|
233
|
+
j_s = np.append(j_s,np.array([self.offset_j + input_node,self.offset_j+output_node,new_eq]),axis=0)
|
234
|
+
else:
|
235
|
+
data = np.append(data,np.array([1.,-1.,sign,-sign]),axis=0)
|
236
|
+
i_s = np.append(i_s,np.array([new_eq,new_eq,input_node+self.rescaler[input_node],output_node+self.rescaler[output_node]]),axis=0)
|
237
|
+
j_s = np.append(j_s,np.array([self.offset_j + input_node,self.offset_j+output_node,new_eq,new_eq]),axis=0)
|
238
|
+
|
239
|
+
## second member value
|
240
|
+
nodes= np.append(nodes, [new_eq],axis=0)
|
241
|
+
data_rhs= np.append(data_rhs, [tension],axis=0)
|
242
|
+
|
243
|
+
## reaffecting the systems and second member
|
244
|
+
self.system = (data,(i_s,j_s))
|
245
|
+
self.rhs = (data_rhs,(nodes,))
|
246
|
+
|
247
|
+
## counter for tension source
|
248
|
+
self.source_count +=1
|
249
|
+
self.source_signs=np.append(self.source_signs,[sign])
|
250
|
+
return self.rhs
|
251
|
+
|
252
|
+
def build_intensity_and_voltage_from_vector(self,sol):
|
253
|
+
sign = np.sign(self.impedence_coords[1]-self.impedence_coords[0])
|
254
|
+
if self.source_count!=0:
|
255
|
+
return (sol[:self.number_intensities]*sign,
|
256
|
+
sol[self.number_intensities:-self.source_count],
|
257
|
+
sol[...,-self.source_count:]*self.source_signs
|
258
|
+
)
|
259
|
+
|
260
|
+
else:
|
261
|
+
return (sol[:self.number_intensities]*sign,
|
262
|
+
sol[self.number_intensities:],
|
263
|
+
np.array([],dtype=float)
|
264
|
+
)
|
@@ -0,0 +1,462 @@
|
|
1
|
+
import numpy as np
|
2
|
+
import networkx as nx
|
3
|
+
from scipy.sparse import coo_matrix, block_diag
|
4
|
+
|
5
|
+
|
6
|
+
class TemporalSystemBuilder():
|
7
|
+
def __init__(self,coil_coords,coil_data,res_coords,res_data,capa_coords,capa_data,inductive_mutuals_coords,inductive_mutuals_data,res_mutual_coords,res_mutual_data):
|
8
|
+
"""Class for building the linear system to solve for both temporal and frequency studies
|
9
|
+
|
10
|
+
Parameters
|
11
|
+
----------
|
12
|
+
coil_coords : np.array of shape (L,2) L being the number of coils
|
13
|
+
Connectivity coordinates in the graph of each inductance
|
14
|
+
Repetitions will be considered as another coil in //
|
15
|
+
coil_data : np.array of shape (L,) L being the number of coils
|
16
|
+
Values of inductance whose coordinates are in coil_coords
|
17
|
+
res_coords : np.array of shape (R,2) R being the number of resistances
|
18
|
+
Connectivity coordinates in the graph of each resistance
|
19
|
+
Repetitions will be considered as another resistance in //
|
20
|
+
res_data : np.array of shape (R,) R being the number of resistances
|
21
|
+
Values of resistance whose coordinates are in res_coords
|
22
|
+
capa_coords : np.array of shape (C,2) C being the number of capacities
|
23
|
+
Connectivity coordinates in the graph of each capacity
|
24
|
+
Repetitions will be considered as another capacity in //
|
25
|
+
capa_data : np.array of shape (R,) R being the number of capacities
|
26
|
+
Values of capacity whose coordinates are in res_coords
|
27
|
+
inductive_mutuals_coords : np.array of shape (M,2) M being the number of mutuals in the system
|
28
|
+
The coordinates are the indices of the coils in the coil_data array that have a mutual effect
|
29
|
+
Inductive mutuals are only supported between coils
|
30
|
+
Repetitions will be considered as another mutual
|
31
|
+
inductive_mutuals_data : np.array of shape (M,) M being the number of mutuals in the system
|
32
|
+
Values of mutuals whose coordinates are in inductive_mutuals_coords.
|
33
|
+
The mutual follows the sign of the nodes given in the coil_coords array
|
34
|
+
res_mutual_coords :np.array of shape (N,2) N being the number of resistive mutuals in the system
|
35
|
+
the coordinates are the indices of the coils or resistance in the coil_data and res_data array that have a resistive mutual effect
|
36
|
+
resitive mutuals are only supported between coils and resistances
|
37
|
+
res_mutual_data : np.array of shape (N,) N being the number of resistive mutuals in the system
|
38
|
+
Values of mutuals whose coordinates are in res_mutuals_coords.
|
39
|
+
The mutual follows the sign of the nodes given in the coil_coords or res_coord array
|
40
|
+
"""
|
41
|
+
self.all_coords = np.concatenate((coil_coords,res_coords,capa_coords),axis=1)
|
42
|
+
all_points = np.unique(self.all_coords)
|
43
|
+
if all_points.shape != np.max(self.all_coords)+1:
|
44
|
+
IndexError("There is one or multiple lonely nodes please clean your impedence graph")
|
45
|
+
self.all_impedences = np.concatenate([coil_data,res_data,capa_data],axis=0)
|
46
|
+
|
47
|
+
self.coil_coords=coil_coords
|
48
|
+
self.coil_data=coil_data
|
49
|
+
self.res_coords=res_coords
|
50
|
+
self.res_data=res_data
|
51
|
+
self.capa_coords=capa_coords
|
52
|
+
self.capa_data=capa_data
|
53
|
+
self.inductive_mutuals_coords=inductive_mutuals_coords
|
54
|
+
self.inductive_mutuals_data=inductive_mutuals_data
|
55
|
+
self.res_mutual_coords=res_mutual_coords
|
56
|
+
self.res_mutual_data=res_mutual_data
|
57
|
+
|
58
|
+
|
59
|
+
# actual number of node in the system
|
60
|
+
self.size = np.max(self.all_coords)+1
|
61
|
+
# number of intensities
|
62
|
+
self.number_intensities = self.all_impedences.shape[0]
|
63
|
+
## making graph and checking number of subgraphs for mass enforcing and intensity checking
|
64
|
+
unique_coords = np.unique(self.all_coords,axis=1)
|
65
|
+
sym_graph = np.concatenate((unique_coords,np.stack((unique_coords[1],unique_coords[0]),axis=0)),axis=1)
|
66
|
+
links = np.ones(sym_graph.shape[1])
|
67
|
+
self.graph = nx.from_scipy_sparse_array(coo_matrix((links,(sym_graph[0],sym_graph[1]))))
|
68
|
+
## keep the subgraphs
|
69
|
+
self.list_of_subgraphs = [ list(sub) for sub in nx.connected_components(self.graph)]
|
70
|
+
self.number_of_subsystems = len(self.list_of_subgraphs)
|
71
|
+
## location of mass
|
72
|
+
self.affected_potentials = [-1]*self.number_of_subsystems
|
73
|
+
## by default remove 1 node equation per subsytem otherwise system is singular
|
74
|
+
self.deleted_equation_current = [subsystem[0] for subsystem in self.list_of_subgraphs]
|
75
|
+
## number of tension sources
|
76
|
+
self.source_count = 0
|
77
|
+
self.source_signs = np.array([],dtype=int)
|
78
|
+
## initializing second member as empty
|
79
|
+
self.rhs = (np.array([]),(np.array([],dtype=int),))
|
80
|
+
## shifter for intensities equations
|
81
|
+
rescaler = np.zeros(self.size)
|
82
|
+
rescaler[self.deleted_equation_current]=1
|
83
|
+
rescaler = -np.cumsum(rescaler)
|
84
|
+
self.rescaler =rescaler.astype(int)
|
85
|
+
## offsets for simplifying building the system
|
86
|
+
offset_j = self.all_impedences.shape[0]
|
87
|
+
offset_i = self.size-len(self.deleted_equation_current)
|
88
|
+
self.offset_i = offset_i
|
89
|
+
self.offset_j = offset_j
|
90
|
+
|
91
|
+
def set_mass(self,*args):
|
92
|
+
"""Function to affect a mass to subsystems
|
93
|
+
If the system already has a mass provided then a warning is displayed and mass reaffected
|
94
|
+
"""
|
95
|
+
for index in args:
|
96
|
+
for pivot,subsystem in enumerate(self.list_of_subgraphs):
|
97
|
+
if index in subsystem:
|
98
|
+
if self.affected_potentials[pivot]!=-1:
|
99
|
+
print(f"Subsystem {pivot} already add a mass, reaffecting the value")
|
100
|
+
self.affected_potentials[pivot]=index
|
101
|
+
break
|
102
|
+
|
103
|
+
def affect_potentials(self):
|
104
|
+
"""Function to check whether the masses were all affected and assign some if some are missing
|
105
|
+
"""
|
106
|
+
for i in range(len(self.affected_potentials)):
|
107
|
+
if -1 == self.affected_potentials[i]:
|
108
|
+
self.affected_potentials[i]= self.list_of_subgraphs[i][0]
|
109
|
+
print(f"Subsytem {i} has not been affected to the mass, we chose {self.list_of_subgraphs[i][0]}")
|
110
|
+
|
111
|
+
def build_system(self):
|
112
|
+
"""Building sytem assuming that data was given as coords / data tuples
|
113
|
+
This function builds 3 matrixes:
|
114
|
+
S1 which is the real part of the system
|
115
|
+
S2 which is the derivative part of the system
|
116
|
+
S_init which is the system that needs to be solved for having initial conditions departing from null conditions
|
117
|
+
"""
|
118
|
+
## affecting masses if need be
|
119
|
+
self.affect_potentials()
|
120
|
+
## Building a sparse COO matrix
|
121
|
+
|
122
|
+
|
123
|
+
## Building all vectorized values necessary
|
124
|
+
i_s_vals = np.max(self.all_coords,axis=0)
|
125
|
+
j_s_vals = np.min(self.all_coords,axis=0)
|
126
|
+
values = self.all_impedences
|
127
|
+
|
128
|
+
|
129
|
+
## node laws (only add to S1)
|
130
|
+
data_nodes = np.concatenate((np.ones(self.number_intensities),-np.ones(self.number_intensities)),axis=0)
|
131
|
+
j_s_nodes = np.tile(np.arange(self.number_intensities,dtype=int),(2,))
|
132
|
+
i_s_nodes = np.concatenate((i_s_vals,j_s_vals),axis=0)
|
133
|
+
|
134
|
+
# Removing one current equation per subsytem
|
135
|
+
mask_removed_eq = ~np.isin(i_s_nodes,self.deleted_equation_current)
|
136
|
+
data_nodes_S1 = data_nodes[mask_removed_eq]
|
137
|
+
j_s_nodes_S1 = j_s_nodes[mask_removed_eq]
|
138
|
+
i_s_nodes_S1 = i_s_nodes[mask_removed_eq]
|
139
|
+
i_s_nodes_S1 = i_s_nodes_S1 + self.rescaler[i_s_nodes_S1]
|
140
|
+
|
141
|
+
|
142
|
+
|
143
|
+
## Kirchoff
|
144
|
+
|
145
|
+
## coils (contribution to S1 for potentials, S2 for intensities)
|
146
|
+
i_s_coils = np.max(self.coil_coords,axis=0)
|
147
|
+
j_s_coils = np.min(self.coil_coords,axis=0)
|
148
|
+
i_s_edges_coil_S1 = self.offset_i + np.concatenate([np.arange(self.coil_data.shape[0],dtype=int)]*2,axis=0 )
|
149
|
+
j_s_edges_coil_S1 = np.concatenate([self.offset_j+i_s_coils,self.offset_j+j_s_coils],axis=0)
|
150
|
+
data_edges_coil_S1 = np.concatenate([np.ones(self.coil_data.shape[0]),-np.ones(self.coil_data.shape[0])],axis=0)
|
151
|
+
|
152
|
+
i_s_edges_coil_S2 = self.offset_i + np.arange(self.coil_data.shape[0],dtype=int)
|
153
|
+
j_s_edges_coil_S2 = np.arange(self.coil_data.shape[0],dtype=int)
|
154
|
+
data_edges_coil_S2 = self.coil_data
|
155
|
+
|
156
|
+
|
157
|
+
offset_coil= self.coil_data.shape[0]
|
158
|
+
|
159
|
+
## resistance (only goes to S1)
|
160
|
+
i_s_res = np.max(self.res_coords,axis=0)
|
161
|
+
j_s_res = np.min(self.res_coords,axis=0)
|
162
|
+
i_s_edges_res_S1 = self.offset_i+offset_coil + np.concatenate([np.arange(self.res_data.shape[0],dtype=int)]*3,axis=0 )
|
163
|
+
j_s_edges_res_S1 = np.concatenate([self.offset_j+i_s_res,self.offset_j+j_s_res,offset_coil+np.arange(self.res_data.shape[0],dtype=int)],axis=0)
|
164
|
+
data_edges_res_S1 = np.concatenate([np.ones(self.res_data.shape[0]),-np.ones(self.res_data.shape[0]),self.res_data],axis=0)
|
165
|
+
|
166
|
+
offset_res = self.res_data.shape[0]
|
167
|
+
|
168
|
+
|
169
|
+
|
170
|
+
## capacities (contribution to S2 for potentials, S1 for intensities)
|
171
|
+
i_s_capa = np.max(self.capa_coords,axis=0)
|
172
|
+
j_s_capa = np.min(self.capa_coords,axis=0)
|
173
|
+
i_s_edges_capa_S1 = self.offset_i+offset_coil+offset_res + np.arange(self.capa_data.shape[0],dtype=int)
|
174
|
+
j_s_edges_capa_S1 = offset_coil+offset_res+np.arange(self.capa_data.shape[0],dtype=int)
|
175
|
+
data_edges_capa_S1 = np.ones(self.capa_data.shape[0])
|
176
|
+
|
177
|
+
i_s_edges_capa_S2 = self.offset_i+offset_coil+offset_res + np.concatenate([np.arange(self.capa_data.shape[0],dtype=int)]*2,axis=0 )
|
178
|
+
j_s_edges_capa_S2 = np.concatenate([self.offset_j+i_s_capa,self.offset_j+j_s_capa],axis=0)
|
179
|
+
data_edges_capa_S2 = np.concatenate([self.capa_data,-self.capa_data],axis=0)
|
180
|
+
|
181
|
+
|
182
|
+
## adding mutuals to the system
|
183
|
+
sign = np.sign(self.all_coords[0]-self.all_coords[1])
|
184
|
+
|
185
|
+
## inductive mutuals (contribution to S2 only)
|
186
|
+
i_s_additionnal_S2 = self.offset_i + np.concatenate((self.inductive_mutuals_coords[0],self.inductive_mutuals_coords[1]),axis=0)
|
187
|
+
j_s_additionnal_S2 = np.concatenate((self.inductive_mutuals_coords[1],self.inductive_mutuals_coords[0]),axis=0)
|
188
|
+
data_additionnal_S2 = np.tile(self.inductive_mutuals_data*sign[self.inductive_mutuals_coords[0]]*sign[self.inductive_mutuals_coords[1]],(2,))
|
189
|
+
|
190
|
+
## resistive mutuals (contribution to S1 only)
|
191
|
+
i_s_additionnal_S1= self.offset_i + np.concatenate((self.res_mutual_coords[0],self.res_mutual_coords[1]),axis=0)
|
192
|
+
j_s_additionnal_S1 = np.concatenate((self.res_mutual_coords[1],self.res_mutual_coords[0]),axis=0)
|
193
|
+
data_additionnal_S1 = np.tile(self.res_mutual_data*sign[self.res_mutual_coords[0]]*sign[self.res_mutual_coords[1]],(2,))
|
194
|
+
|
195
|
+
|
196
|
+
## mass equations (1 per subsystem)
|
197
|
+
i_s_mass_S1 = np.arange(self.offset_i+self.offset_j,self.offset_i+self.offset_j+len(self.affected_potentials))
|
198
|
+
j_s_mass_S1 = self.offset_j+np.array(self.affected_potentials)
|
199
|
+
data_mass_S1 = np.ones(len(self.affected_potentials))
|
200
|
+
|
201
|
+
|
202
|
+
|
203
|
+
## building S1 system
|
204
|
+
i_s_S1 = np.concatenate((i_s_nodes_S1,i_s_edges_coil_S1,i_s_edges_res_S1,i_s_edges_capa_S1,i_s_additionnal_S1,i_s_mass_S1),axis=0)
|
205
|
+
j_s_S1 = np.concatenate((j_s_nodes_S1,j_s_edges_coil_S1,j_s_edges_res_S1,j_s_edges_capa_S1,j_s_additionnal_S1,j_s_mass_S1),axis=0)
|
206
|
+
data_S1 = np.concatenate((data_nodes_S1,data_edges_coil_S1,data_edges_res_S1,data_edges_capa_S1,data_additionnal_S1,data_mass_S1),axis=0)
|
207
|
+
|
208
|
+
## building S2 system
|
209
|
+
i_s_S2 = np.concatenate((i_s_edges_coil_S2,i_s_edges_capa_S2,i_s_additionnal_S2),axis=0)
|
210
|
+
j_s_S2 = np.concatenate((j_s_edges_coil_S2,j_s_edges_capa_S2,j_s_additionnal_S2),axis=0)
|
211
|
+
data_S2 = np.concatenate((data_edges_coil_S2,data_edges_capa_S2,data_additionnal_S2),axis=0)
|
212
|
+
|
213
|
+
## building S_init system
|
214
|
+
i_s_init = np.concatenate((i_s_nodes_S1,i_s_edges_coil_S2,i_s_edges_res_S1,i_s_edges_capa_S2,i_s_mass_S1),axis=0)
|
215
|
+
j_s_init = np.concatenate((j_s_nodes_S1,j_s_edges_coil_S2,j_s_edges_res_S1,j_s_edges_capa_S2,j_s_mass_S1),axis=0)
|
216
|
+
data_init = np.concatenate((data_nodes_S1,data_edges_coil_S2,data_edges_res_S1,data_edges_capa_S2,data_mass_S1),axis=0)
|
217
|
+
|
218
|
+
self.S_init=(data_init,(i_s_init,j_s_init))
|
219
|
+
|
220
|
+
self.S1 = (data_S1,(i_s_S1.astype(int),j_s_S1.astype(int)))
|
221
|
+
self.S2 = (data_S2,(i_s_S2.astype(int),j_s_S2.astype(int)))
|
222
|
+
## return for debug if needed
|
223
|
+
return self.S1,self.S2,self.S_init
|
224
|
+
|
225
|
+
|
226
|
+
def build_second_member_intensity(self,intensity,input_node,output_node):
|
227
|
+
"""Function to build a second member for the scenario of current injection in the sparse system
|
228
|
+
|
229
|
+
Parameters
|
230
|
+
----------
|
231
|
+
intensity : float
|
232
|
+
intensity to inject
|
233
|
+
input_node : int
|
234
|
+
which node to take for injection
|
235
|
+
output_node : int
|
236
|
+
which node for current retrieval
|
237
|
+
|
238
|
+
Returns
|
239
|
+
-------
|
240
|
+
Tuple(np.array,(np.array))
|
241
|
+
second member of the linear system in a COO like format
|
242
|
+
"""
|
243
|
+
data = []
|
244
|
+
nodes = []
|
245
|
+
if self.number_of_subsystems>=2:
|
246
|
+
valid = False
|
247
|
+
for system in self.list_of_subgraphs:
|
248
|
+
if input_node in system and output_node in system:
|
249
|
+
valid =True
|
250
|
+
else:
|
251
|
+
continue
|
252
|
+
if not valid:
|
253
|
+
raise IndexError("Nodes indicated do not belong to the same subsystem")
|
254
|
+
## making sure the equation is actually in the system
|
255
|
+
if input_node not in self.deleted_equation_current:
|
256
|
+
nodes.append(input_node+self.rescaler[input_node])
|
257
|
+
data.append(-intensity) # inbound intensity
|
258
|
+
if output_node not in self.deleted_equation_current:
|
259
|
+
nodes.append(output_node+self.rescaler[output_node])
|
260
|
+
data.append(intensity) # outbound intensity
|
261
|
+
(data_rhs,(nodes_rhs,)) = self.rhs
|
262
|
+
self.rhs = (np.append(data_rhs,np.array(data),axis=0),(np.append(nodes_rhs,np.array(nodes),axis=0),))
|
263
|
+
return self.rhs
|
264
|
+
|
265
|
+
def build_second_member_tension(self,tension,input_node,output_node):
|
266
|
+
"""Building second member in the case of an enforced tension
|
267
|
+
This adds one equation and one degree of freedom in the system (the source intensity)
|
268
|
+
Warning this function changes the system, please re-get the system before solving it (call get_system)
|
269
|
+
|
270
|
+
Parameters
|
271
|
+
----------
|
272
|
+
tension : complex
|
273
|
+
enforced tension
|
274
|
+
input_node : int
|
275
|
+
node where the tension is enforced
|
276
|
+
output_node : int
|
277
|
+
node from where the tension is enforced
|
278
|
+
|
279
|
+
Returns
|
280
|
+
-------
|
281
|
+
Tuple(np.array,(np.array))
|
282
|
+
second member of the linear system in a COO like format
|
283
|
+
"""
|
284
|
+
targeted_subsystem = 0
|
285
|
+
if self.number_of_subsystems>=2:
|
286
|
+
valid = False
|
287
|
+
for index_sub,system in enumerate(self.list_of_subgraphs):
|
288
|
+
if input_node in system and output_node in system:
|
289
|
+
valid =True
|
290
|
+
targeted_subsystem=index_sub
|
291
|
+
else:
|
292
|
+
continue
|
293
|
+
if not valid:
|
294
|
+
raise IndexError("Nodes indicated do not belong to the same subsystem")
|
295
|
+
print("Warning this function changes the system, please re-evaluate it before solving the system (call get_system)")
|
296
|
+
(data_init,(i_s_init,j_s_init)) = self.S_init
|
297
|
+
(data_S1,(i_s_S1,j_s_S1)) = self.S1
|
298
|
+
(data_S2,(i_s_S2,j_s_S2)) = self.S2
|
299
|
+
(data_rhs,(nodes,)) = self.rhs
|
300
|
+
new_eq = self.number_intensities+self.size +self.source_count
|
301
|
+
sign = np.sign(output_node-input_node)
|
302
|
+
|
303
|
+
## Adding the new equation in S_init
|
304
|
+
if input_node in self.deleted_equation_current:
|
305
|
+
data_init = np.append(data_init,np.array([1.,-1.,-sign]),axis=0)
|
306
|
+
i_s_init = np.append(i_s_init,np.array([new_eq,new_eq,output_node+self.rescaler[output_node]]),axis=0)
|
307
|
+
j_s_init = np.append(j_s_init,np.array([self.offset_j + input_node,self.offset_j+output_node,new_eq]),axis=0)
|
308
|
+
elif output_node in self.deleted_equation_current:
|
309
|
+
data_init = np.append(data_init,np.array([1.,-1.,sign]),axis=0)
|
310
|
+
i_s_init = np.append(i_s_init,np.array([new_eq,new_eq,input_node+self.rescaler[input_node]]),axis=0)
|
311
|
+
j_s_init = np.append(j_s_init,np.array([self.offset_j + input_node,self.offset_j+output_node,new_eq]),axis=0)
|
312
|
+
else:
|
313
|
+
data_init = np.append(data_init,np.array([1.,-1.,sign,-sign]),axis=0)
|
314
|
+
i_s_init = np.append(i_s_init,np.array([new_eq,new_eq,input_node+self.rescaler[input_node],output_node+self.rescaler[output_node]]),axis=0)
|
315
|
+
j_s_init = np.append(j_s_init,np.array([self.offset_j + input_node,self.offset_j+output_node,new_eq,new_eq]),axis=0)
|
316
|
+
|
317
|
+
## Adding the new equation in S1
|
318
|
+
if input_node in self.deleted_equation_current:
|
319
|
+
data_S1 = np.append(data_S1,np.array([1.,-1.,-sign]),axis=0)
|
320
|
+
i_s_S1 = np.append(i_s_S1,np.array([new_eq,new_eq,output_node+self.rescaler[output_node]]),axis=0)
|
321
|
+
j_s_S1 = np.append(j_s_S1,np.array([self.offset_j + input_node,self.offset_j+output_node,new_eq]),axis=0)
|
322
|
+
elif output_node in self.deleted_equation_current:
|
323
|
+
data_S1 = np.append(data_S1,np.array([1.,-1.,sign]),axis=0)
|
324
|
+
i_s_S1 = np.append(i_s_S1,np.array([new_eq,new_eq,input_node+self.rescaler[input_node]]),axis=0)
|
325
|
+
j_s_S1 = np.append(j_s_S1,np.array([self.offset_j + input_node,self.offset_j+output_node,new_eq]),axis=0)
|
326
|
+
else:
|
327
|
+
data_S1 = np.append(data_S1,np.array([1.,-1.,sign,-sign]),axis=0)
|
328
|
+
i_s_S1 = np.append(i_s_init,np.array([new_eq,new_eq,input_node+self.rescaler[input_node],output_node+self.rescaler[output_node]]),axis=0)
|
329
|
+
j_s_S1 = np.append(j_s_S1,np.array([self.offset_j + input_node,self.offset_j+output_node,new_eq,new_eq]),axis=0)
|
330
|
+
|
331
|
+
|
332
|
+
## second member value
|
333
|
+
nodes= np.append(nodes, [new_eq],axis=0)
|
334
|
+
data_rhs= np.append(data_rhs, [tension],axis=0)
|
335
|
+
|
336
|
+
|
337
|
+
## reaffecting the systems and second member
|
338
|
+
self.S_init = (data_init,(i_s_init,j_s_init))
|
339
|
+
self.S1 = (data_S1,(i_s_S1,j_s_S1))
|
340
|
+
self.S2 = (data_S2,(i_s_S2,j_s_S2)) ## S2 is unchanged
|
341
|
+
self.rhs = (data_rhs,(nodes,))
|
342
|
+
|
343
|
+
## counter for tension source
|
344
|
+
self.source_count +=1
|
345
|
+
self.source_signs=np.append(self.source_signs,[sign])
|
346
|
+
return self.rhs
|
347
|
+
|
348
|
+
def get_init_system(self):
|
349
|
+
"""Function to determine the initial state of the system as coo_matrix
|
350
|
+
|
351
|
+
Returns
|
352
|
+
-------
|
353
|
+
sys: coo_matrix
|
354
|
+
left hand side of the equation to solve
|
355
|
+
rhs:
|
356
|
+
right hand side of the system to solve
|
357
|
+
"""
|
358
|
+
sys = coo_matrix(self.S_init,shape=(self.number_intensities+self.size+self.source_count,self.number_intensities+self.size+self.source_count))
|
359
|
+
rhs = np.zeros(self.number_intensities+self.size+self.source_count)
|
360
|
+
(data_rhs,(nodes,)) = self.rhs
|
361
|
+
np.add.at(rhs, nodes, data_rhs)
|
362
|
+
return sys,rhs
|
363
|
+
|
364
|
+
def get_system(self):
|
365
|
+
"""Function to get the whole system to solve as coo_matrix
|
366
|
+
|
367
|
+
Returns
|
368
|
+
-------
|
369
|
+
sys1 : coo_matrix
|
370
|
+
real part of the system
|
371
|
+
sys2 : coo_matrix
|
372
|
+
img part of the system (need to be multiplyied by j.omega for frequency studies)
|
373
|
+
rhs: np.array
|
374
|
+
"""
|
375
|
+
sys1 = coo_matrix(self.S1,shape=(self.number_intensities+self.size+self.source_count,self.number_intensities+self.size+self.source_count))
|
376
|
+
sys2 = coo_matrix(self.S2,shape=(self.number_intensities+self.size+self.source_count,self.number_intensities+self.size+self.source_count))
|
377
|
+
rhs = np.zeros(self.number_intensities+self.size+self.source_count)
|
378
|
+
(data_rhs,(nodes,)) = self.rhs
|
379
|
+
np.add.at(rhs, nodes, data_rhs)
|
380
|
+
return sys1,sys2,rhs
|
381
|
+
|
382
|
+
def get_frequency_system(self,omega):
|
383
|
+
"""Function to get the complex matrix for frequency studies
|
384
|
+
|
385
|
+
Parameters
|
386
|
+
----------
|
387
|
+
omega : float
|
388
|
+
pulsation of the system
|
389
|
+
|
390
|
+
Returns
|
391
|
+
-------
|
392
|
+
sys: coo_matrix, dtype=complex
|
393
|
+
complex system for frequency studies
|
394
|
+
rhs: np.array
|
395
|
+
right hand side of the system
|
396
|
+
"""
|
397
|
+
sys1,sys2,rhs = self.get_system()
|
398
|
+
return sys1+1j*omega*sys2,rhs
|
399
|
+
|
400
|
+
def get_frequency_system_all_omegas(self,omegas):
|
401
|
+
"""Builds a big system for solving once for all all frequencies given
|
402
|
+
once this system has been solved, you need to reshape the output of the system with batches of size A.shape[0]//len(omegas)
|
403
|
+
to get the output for each omegas
|
404
|
+
|
405
|
+
Parameters
|
406
|
+
----------
|
407
|
+
omegas : list(float)
|
408
|
+
iterable that has a __len__ function that oontains all values of omega to compute
|
409
|
+
|
410
|
+
Returns
|
411
|
+
-------
|
412
|
+
A : coo_matrix, dtype=complex
|
413
|
+
system containing all the frequencies given in omegas
|
414
|
+
RHS : np.array
|
415
|
+
second member for the system A
|
416
|
+
|
417
|
+
"""
|
418
|
+
sys1,sys2,rhs = self.get_system()
|
419
|
+
A = block_diag([sys1+1j*omega*sys2 for omega in omegas])
|
420
|
+
RHS = np.tile(rhs,(len(omegas),))
|
421
|
+
return A,RHS
|
422
|
+
|
423
|
+
def build_intensity_and_voltage_from_vector(self,sol):
|
424
|
+
"""Utility function to reshape the solution of frequency studies or temporal studies
|
425
|
+
|
426
|
+
Parameters
|
427
|
+
----------
|
428
|
+
sol : np.array of shape (*, self.number_intensities+self.size+self.source_count)
|
429
|
+
array containing as many solutions as wanted
|
430
|
+
|
431
|
+
Returns
|
432
|
+
-------
|
433
|
+
coil_intensities: np.array of shape (*,coil_data.shape[0])
|
434
|
+
intensities following the directions given by coil_coords
|
435
|
+
res_intensities: np.array of shape (*,res_data.shape[0])
|
436
|
+
intensities following the directions given by res_coords
|
437
|
+
capa_intensities: np.array of shape (*,capa_data.shape[0])
|
438
|
+
intensities following the directions given by res_coords
|
439
|
+
voltages: np.array of shape (*,self.size)
|
440
|
+
voltage of the points in the graph
|
441
|
+
source_intensities: np.array of shape (*, self.source_count)
|
442
|
+
if there are voltage sources this contains the source intensities
|
443
|
+
"""
|
444
|
+
sign = np.sign(self.all_coords[1]-self.all_coords[0])
|
445
|
+
offset_coil = self.coil_data.shape[0]
|
446
|
+
offset_res = self.res_data.shape[0]
|
447
|
+
offset_capa = self.capa_data.shape[0]
|
448
|
+
if self.source_count!=0:
|
449
|
+
return (sol[...,:offset_coil]*sign[:offset_coil],
|
450
|
+
sol[...,offset_coil:offset_coil+offset_res]*sign[offset_coil:offset_coil+offset_res],
|
451
|
+
sol[...,offset_coil+offset_res:offset_coil+offset_res+offset_capa]*sign[offset_coil+offset_res:offset_coil+offset_res+offset_capa],
|
452
|
+
sol[...,self.number_intensities:-self.source_count],
|
453
|
+
sol[...,-self.source_count:]*self.source_signs
|
454
|
+
)
|
455
|
+
else:
|
456
|
+
return (sol[...,:offset_coil]*sign[:offset_coil],
|
457
|
+
sol[...,offset_coil:offset_coil+offset_res]*sign[offset_coil:offset_coil+offset_res],
|
458
|
+
sol[...,offset_coil+offset_res:offset_coil+offset_res+offset_capa]*sign[offset_coil+offset_res:offset_coil+offset_res+offset_capa],
|
459
|
+
sol[...,self.number_intensities:-self.source_count],
|
460
|
+
np.array([],dtype=float)
|
461
|
+
)
|
462
|
+
|
ElecSolver/__init__.py
ADDED
ElecSolver/_version.py
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# file generated by setuptools-scm
|
2
|
+
# don't change, don't track in version control
|
3
|
+
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
5
|
+
|
6
|
+
TYPE_CHECKING = False
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
from typing import Tuple
|
9
|
+
from typing import Union
|
10
|
+
|
11
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
12
|
+
else:
|
13
|
+
VERSION_TUPLE = object
|
14
|
+
|
15
|
+
version: str
|
16
|
+
__version__: str
|
17
|
+
__version_tuple__: VERSION_TUPLE
|
18
|
+
version_tuple: VERSION_TUPLE
|
19
|
+
|
20
|
+
__version__ = version = '0.1.0'
|
21
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
ElecSolver/utils.py
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
import numpy as np
|
2
|
+
from scipy.sparse import coo_matrix
|
3
|
+
|
4
|
+
def parallel_sum(*impedences):
|
5
|
+
"""Function to compute the graph of impedences resulting from // graphs
|
6
|
+
works for any number of impedence graphs
|
7
|
+
|
8
|
+
Returns
|
9
|
+
-------
|
10
|
+
scipy.sparse.coo_matrix
|
11
|
+
resulting impedence
|
12
|
+
"""
|
13
|
+
coords_tot = np.concatenate([impedence.coords for impedence in impedences],axis=1)
|
14
|
+
data_tot = np.concatenate([impedence.data for impedence in impedences])
|
15
|
+
current_indexes = np.arange(0,data_tot.shape[0],dtype=int)
|
16
|
+
|
17
|
+
uniques,indexes,inverse,counts = np.unique(coords_tot,return_index=True,return_inverse=True,return_counts=True,axis=1)
|
18
|
+
new_coords = uniques
|
19
|
+
new_data = data_tot[indexes].astype(complex)
|
20
|
+
|
21
|
+
remaining_coords = coords_tot.copy()
|
22
|
+
remaining_data = data_tot.copy()
|
23
|
+
reverse_counts = counts[inverse]
|
24
|
+
reverse_counts_ref = reverse_counts.copy()
|
25
|
+
|
26
|
+
reverse_counts[indexes]=0
|
27
|
+
reverse_indexes = indexes[inverse]
|
28
|
+
|
29
|
+
|
30
|
+
while np.max(reverse_counts)>1:
|
31
|
+
mask = (reverse_counts>1)
|
32
|
+
remaining_coords = remaining_coords[:,mask]
|
33
|
+
|
34
|
+
remaining_data = remaining_data[mask]
|
35
|
+
current_indexes=current_indexes[mask]
|
36
|
+
|
37
|
+
uniques,indexes,inverse,counts = np.unique(remaining_coords,return_index=True,return_inverse=True,return_counts=True,axis=1)
|
38
|
+
new_data[reverse_indexes[current_indexes[indexes]]] = new_data[reverse_indexes[current_indexes[indexes]]]*remaining_data[indexes]/(new_data[reverse_indexes[current_indexes[indexes]]]+ remaining_data[indexes])
|
39
|
+
reverse_counts = counts[inverse]
|
40
|
+
reverse_counts[indexes]=0
|
41
|
+
|
42
|
+
indexed_data = np.zeros(impedences[0].shape[0]**2,dtype=complex)
|
43
|
+
indexed_data[new_coords[0]*impedences[0].shape[0]+new_coords[1]]=new_data
|
44
|
+
return coo_matrix((new_data,(new_coords[0],new_coords[1])))
|
45
|
+
|
46
|
+
|
47
|
+
def serie_sum(*impedences):
|
48
|
+
"""Function to compute the impedences that go serial
|
49
|
+
|
50
|
+
Returns
|
51
|
+
-------
|
52
|
+
scipy.sparse.coo_matrix
|
53
|
+
resulting impedence
|
54
|
+
|
55
|
+
"""
|
56
|
+
return sum(impedences).tocoo()
|
57
|
+
|
58
|
+
|
59
|
+
def cast_complex_system_in_real_system(sys,b):
|
60
|
+
"""Function to cast an n dimensional complex system into an
|
61
|
+
equivalent 2n dimension real system
|
62
|
+
the solution of the initial system is the concatenation of the real and
|
63
|
+
imaginary part of the solution of this system:
|
64
|
+
sol_comp = sol_real[:n]+1.0j*sol_real[n:]
|
65
|
+
|
66
|
+
Parameters
|
67
|
+
----------
|
68
|
+
sys : scipy.sparse.coo_matric
|
69
|
+
system with complex data
|
70
|
+
b : np.array
|
71
|
+
second member with real or complex values
|
72
|
+
|
73
|
+
Returns
|
74
|
+
-------
|
75
|
+
sys_comp
|
76
|
+
real system equivalent to complex system
|
77
|
+
new_b
|
78
|
+
real second member equivalent to complex system
|
79
|
+
"""
|
80
|
+
coords = np.stack((sys.row,sys.col),axis=1)
|
81
|
+
data = np.array(sys.data,dtype=complex)
|
82
|
+
b= b.astype(complex)
|
83
|
+
new_coords = np.concatenate((coords,coords+[[0],[sys.shape[0]]],coords+[[sys.shape[0]],[0]],coords+[[sys.shape[0]],[sys.shape[0]]]),axis=1)
|
84
|
+
new_data = np.concatenate((data.real,-data.imag,data.imag,data.real),axis=0)
|
85
|
+
sys_comp = coo_matrix((new_data,(new_coords[0],new_coords[1])))
|
86
|
+
new_b = np.concatenate((b.real,b.imag),axis=0)
|
87
|
+
return sys_comp,new_b
|
88
|
+
|
89
|
+
|
90
|
+
def constant_block_diag(A,repetitions):
|
91
|
+
"""Function to repeat the matrix A multiple times along the diagonal
|
92
|
+
This is a faster version of block_diag in the case of having always the same block
|
93
|
+
|
94
|
+
Parameters
|
95
|
+
----------
|
96
|
+
A : scpiy.sparse.coo_matrix
|
97
|
+
block to repeat multiple times on the diagonal
|
98
|
+
repetitions : int
|
99
|
+
number of repetitions to perform
|
100
|
+
|
101
|
+
Returns
|
102
|
+
-------
|
103
|
+
coo_matrix
|
104
|
+
block diagonal sparse matrix
|
105
|
+
"""
|
106
|
+
size = A.shape[0]
|
107
|
+
indexes = A.data.shape[0]
|
108
|
+
rows = np.tile(A.row,(repetitions,))+size*np.repeat(np.arange(0,repetitions,dtype=int),indexes)
|
109
|
+
cols = np.tile(A.col,(repetitions,))+size*np.repeat(np.arange(0,repetitions,dtype=int),indexes)
|
110
|
+
data = np.tile(A.data,(repetitions,))
|
111
|
+
return coo_matrix((data,(rows,cols)),shape=(repetitions*size,repetitions*size))
|
112
|
+
|
113
|
+
|
114
|
+
|
115
|
+
def build_big_temporal_system(S1,S2,dt,rhs,sol,nb_timesteps):
|
116
|
+
"""Function to build the temporal system for nb_timesteps
|
117
|
+
The solution of this system is the concatenation of the all the timsteps
|
118
|
+
except for the initial timestep that the user is free to concatenate with the solutions
|
119
|
+
Tip: reshaping the solution of the systme with shape (nb_timesteps,sol.shape[0]) provides
|
120
|
+
the solution array indexed by the timestep
|
121
|
+
|
122
|
+
Parameters
|
123
|
+
----------
|
124
|
+
S1 : coo_matrix
|
125
|
+
real part of the temporal system
|
126
|
+
S2 : coo_matrix
|
127
|
+
derivative part of the temporal system
|
128
|
+
dt : float
|
129
|
+
timestep for the simulation
|
130
|
+
rhs : np.array
|
131
|
+
second member of the system
|
132
|
+
sol : initial condition
|
133
|
+
solution of initial condition system
|
134
|
+
nb_timesteps : int
|
135
|
+
number of timesteps
|
136
|
+
|
137
|
+
Returns
|
138
|
+
-------
|
139
|
+
S : coo_matrix
|
140
|
+
system left hand side for the temporal
|
141
|
+
"""
|
142
|
+
A = constant_block_diag((S2+dt*S1).tocoo(),nb_timesteps)
|
143
|
+
B = constant_block_diag(-S2,nb_timesteps-1)
|
144
|
+
B = coo_matrix((B.data,(B.row+S1.shape[0],B.col)),shape=A.shape)
|
145
|
+
S = A+B
|
146
|
+
RHS = np.concatenate([rhs*dt+S2@sol]+[rhs*dt]*(nb_timesteps-1),axis=0)
|
147
|
+
return S,RHS
|
148
|
+
|
149
|
+
|
@@ -0,0 +1,260 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: ElecSolver
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: Formalizes electric systems as linear problems for temporal and frequency-domain studies.
|
5
|
+
Author-email: William Piat <william.piat3@gmail.com>
|
6
|
+
License: MIT
|
7
|
+
Project-URL: Homepage, https://github.com/williampiat3/ElecSolver
|
8
|
+
Platform: Linux
|
9
|
+
Platform: Unix
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
11
|
+
Classifier: Operating System :: POSIX
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
15
|
+
Requires-Python: >=3.12
|
16
|
+
Description-Content-Type: text/markdown
|
17
|
+
License-File: LICENCE.txt
|
18
|
+
Requires-Dist: numpy
|
19
|
+
Requires-Dist: scipy
|
20
|
+
Requires-Dist: networkx
|
21
|
+
Provides-Extra: dev
|
22
|
+
Requires-Dist: pytest; extra == "dev"
|
23
|
+
Requires-Dist: pymumps; extra == "dev"
|
24
|
+
Dynamic: license-file
|
25
|
+
|
26
|
+
# ElectricSystemSolver
|
27
|
+
|
28
|
+
## Overview
|
29
|
+
|
30
|
+
**ElectricSystemSolver** formalizes electric systems as linear problems, suitable for both **temporal** and **frequency-domain** studies.
|
31
|
+
It focuses on constructing the linear system representation, leaving the actual numerical resolution to the user.
|
32
|
+
|
33
|
+
This repository is **not** a general-purpose electrical system solver. Instead, it acts as a **bridge** between:
|
34
|
+
|
35
|
+
- The graph-based description of an electric network
|
36
|
+
- The corresponding sparse linear system to solve
|
37
|
+
|
38
|
+
Its main goal is to provide a friendly Python interface for simulating analog electric systems. While suitable for small circuit simulations, its strength lies in scalability—handling millions of nodes and components, provided that you possess sufficient computational resources.
|
39
|
+
|
40
|
+
|
41
|
+
> [!NOTE]
|
42
|
+
> Non-linear components are not supported. You must manage event detection and system updates yourself.
|
43
|
+
|
44
|
+
|
45
|
+
## Table of content
|
46
|
+
|
47
|
+
- [ElectricSystemSolver](#electricsystemsolver)
|
48
|
+
- [Overview](#overview)
|
49
|
+
- [Table of content](#table-of-content)
|
50
|
+
- [How to install](#how-to-install)
|
51
|
+
- [Components](#components)
|
52
|
+
- [FrequencySystemBuilder](#frequencysystembuilder)
|
53
|
+
- [Features](#features)
|
54
|
+
- [Example](#example)
|
55
|
+
- [Adding a Parallel Resistance](#adding-a-parallel-resistance)
|
56
|
+
- [TemporalSystemBuilder](#temporalsystembuilder)
|
57
|
+
- [Features](#features-1)
|
58
|
+
- [Example](#example-1)
|
59
|
+
- [Solver suggestions](#solver-suggestions)
|
60
|
+
|
61
|
+
## How to install
|
62
|
+
For now this package is not distributed the only way to install it is by:
|
63
|
+
|
64
|
+
1. Cloning this repo:
|
65
|
+
|
66
|
+
```
|
67
|
+
git clone https://github.com/williampiat3/ElectricSystemSolver.git
|
68
|
+
```
|
69
|
+
|
70
|
+
2. Creating a conda environement using the `env.yml` file:
|
71
|
+
|
72
|
+
```
|
73
|
+
conda env create -n ElecSolver -f ElectricSystemSolver/env.yml
|
74
|
+
```
|
75
|
+
|
76
|
+
> [!WARNING]
|
77
|
+
> pyMumps is not supported on Windows (But it is on WSL!!): If you want to execute this code on Windows, remove pyMumps from the `env.yml` file and use other solvers instead
|
78
|
+
|
79
|
+
3. Exporting the `PYTHONPATH` variable to import the system builders more smoothly
|
80
|
+
|
81
|
+
```
|
82
|
+
export PYTHONPATH='path/to/ElectricSystemSolver/src'
|
83
|
+
```
|
84
|
+
|
85
|
+
|
86
|
+
## Components
|
87
|
+
|
88
|
+
### FrequencySystemBuilder
|
89
|
+
|
90
|
+
This class handles **frequency-domain** analysis of linear electric systems.
|
91
|
+
|
92
|
+
#### Features
|
93
|
+
|
94
|
+
- Supports tension and intensity sources
|
95
|
+
- Models inductive and resistive mutuals
|
96
|
+
- Detects and couples multiple subsystems
|
97
|
+
- Accepts arbitrary complex impedances and mutuals
|
98
|
+
- Constructs sparse linear systems (COO format)
|
99
|
+
|
100
|
+
|
101
|
+
> [!TIP]
|
102
|
+
> Some solvers do not support complex-valued systems. Use the utility function `cast_complex_system_in_real_system` in `utils.py` to convert an `n`-dimensional complex system into a `2n`-dimensional real system.
|
103
|
+
|
104
|
+
#### Example
|
105
|
+
|
106
|
+
We would like to study the following system:
|
107
|
+

|
108
|
+
|
109
|
+
this can simply be defined in the following manner (We took R=1, L=1 and M=2):
|
110
|
+
```python
|
111
|
+
import numpy as np
|
112
|
+
from scipy.sparse.linalg import spsolve
|
113
|
+
from ElecSolver import FrequencySystemBuilder
|
114
|
+
|
115
|
+
|
116
|
+
# Complex and sparse impedance matrix
|
117
|
+
# notice coil impedence between points 0 and 2, and coil impedence between 3 and 4
|
118
|
+
impedence_coords = np.array([[0,0,1,3],[1,2,2,4]], dtype=int)
|
119
|
+
impedence_data = np.array([1, 1j, 1, 1j], dtype=complex)
|
120
|
+
|
121
|
+
# Mutual inductance or coupling
|
122
|
+
# The indexes here are the impedence indexes in impedence_data
|
123
|
+
# The coupling is inductive
|
124
|
+
mutuals_coords = np.array([[1],[3]], dtype=int)
|
125
|
+
mutuals_data = np.array([2.j], dtype=complex)
|
126
|
+
|
127
|
+
electric_sys = FrequencySystemBuilder(
|
128
|
+
impedence_coords,
|
129
|
+
impedence_data,
|
130
|
+
mutuals_coords,
|
131
|
+
mutuals_data
|
132
|
+
)
|
133
|
+
|
134
|
+
# Set node masses
|
135
|
+
# 2 values because 2 subsystems
|
136
|
+
electric_sys.set_mass(0, 3)
|
137
|
+
# Building system
|
138
|
+
electric_sys.build_system()
|
139
|
+
electric_sys.build_second_member_intensity(intensity=10, input_node=2, output_node=0)
|
140
|
+
|
141
|
+
# Get and solve the system
|
142
|
+
sys, b = electric_sys.get_system()
|
143
|
+
sol = spsolve(sys.tocsr(), b)
|
144
|
+
intensities, potentials = electric_sys.build_intensity_and_voltage_from_vector(sol)
|
145
|
+
|
146
|
+
## We see a tension appearing on the lonely coil (between node 3 and 4)
|
147
|
+
print(potentials[3]-potentials[4])
|
148
|
+
```
|
149
|
+
#### Adding a Parallel Resistance
|
150
|
+
We want to add components in parallel with existing components for instance inserting a resistor in parallel with the first inductance (between nodes 0 and 2)
|
151
|
+

|
152
|
+
|
153
|
+
In python, simply add the resistance to the list of impedence in the very first lines of the script:
|
154
|
+
|
155
|
+
```python
|
156
|
+
import numpy as np
|
157
|
+
from scipy.sparse.linalg import spsolve
|
158
|
+
from ElecSolver import FrequencySystemBuilder
|
159
|
+
|
160
|
+
|
161
|
+
# We add an additionnal resistance between 0 and 2
|
162
|
+
impedence_coords = np.array([[0,0,1,3,0],[1,2,2,4,2]], dtype=int)
|
163
|
+
impedence_data = np.array([1, 1j,1, 1j,1], dtype=complex)
|
164
|
+
|
165
|
+
# No need to change the couplings since indexes of the coils did not change
|
166
|
+
mutuals_coords = np.array([[1],[3]], dtype=int)
|
167
|
+
mutuals_data = np.array([2.j], dtype=complex)
|
168
|
+
|
169
|
+
```
|
170
|
+
|
171
|
+
|
172
|
+
### TemporalSystemBuilder
|
173
|
+
|
174
|
+
This class models **time-dependent** systems using resistors, capacitors, coils, and mutuals.
|
175
|
+
|
176
|
+
#### Features
|
177
|
+
|
178
|
+
- Supports tension and intensity sources
|
179
|
+
- Models inductive and resistive mutuals
|
180
|
+
- Detects and couples multiple subsystems
|
181
|
+
- Accepts 3 dipole types: resistances, capacities and coils
|
182
|
+
- Constructs sparse linear systems (COO format)
|
183
|
+
|
184
|
+
#### Example
|
185
|
+
|
186
|
+
|
187
|
+
We would like to study the following system:
|
188
|
+

|
189
|
+
|
190
|
+
with R=1, L=0.1, C=2 this gives:
|
191
|
+
|
192
|
+
```python
|
193
|
+
import numpy as np
|
194
|
+
from scipy.sparse.linalg import spsolve
|
195
|
+
from ElecSolver import TemporalSystemBuilder
|
196
|
+
|
197
|
+
## Defining resistances
|
198
|
+
res_coords = np.array([[0,2],[1,3]],dtype=int)
|
199
|
+
res_data = np.array([1,1],dtype=float)
|
200
|
+
## Defining coils
|
201
|
+
coil_coords = np.array([[1,0],[3,2]],dtype=int)
|
202
|
+
coil_data = np.array([0.1,0.1],dtype=float)
|
203
|
+
## Defining capacities
|
204
|
+
capa_coords = np.array([[1,3],[2,0]],dtype=int)
|
205
|
+
capa_data = np.array([2,2],dtype=float)
|
206
|
+
|
207
|
+
## Defining empty mutuals here
|
208
|
+
mutuals_coords=np.array([[],[]],dtype=int)
|
209
|
+
mutuals_data = np.array([],dtype=float)
|
210
|
+
|
211
|
+
|
212
|
+
res_mutuals_coords=np.array([[],[]],dtype=int)
|
213
|
+
res_mutuals_data = np.array([],dtype=float)
|
214
|
+
|
215
|
+
## initializing system
|
216
|
+
elec_sys = TemporalSystemBuilder(coil_coords,coil_data,res_coords,res_data,capa_coords,capa_data,mutuals_coords,mutuals_data,res_mutuals_coords,res_mutuals_data)
|
217
|
+
## Seting mass at point 0
|
218
|
+
elec_sys.set_mass(0)
|
219
|
+
## Build second member
|
220
|
+
elec_sys.build_system()
|
221
|
+
elec_sys.build_second_member_intensity(10,1,0)
|
222
|
+
# getting initial condition system
|
223
|
+
S_i,b = elec_sys.get_init_system()
|
224
|
+
# initial condition
|
225
|
+
sol = spsolve(S_i.tocsr(),b)
|
226
|
+
# get system (S1 is real part, S2 derivative part)
|
227
|
+
S1,S2,rhs = elec_sys.get_system()
|
228
|
+
|
229
|
+
## Solving using euler implicit scheme
|
230
|
+
dt=0.08
|
231
|
+
vals_res1 = []
|
232
|
+
vals_res2 = []
|
233
|
+
for i in range(50):
|
234
|
+
currents_coil,currents_res,currents_capa,voltages,_ = elec_sys.build_intensity_and_voltage_from_vector(sol)
|
235
|
+
vals_res1.append(currents_res[1])
|
236
|
+
vals_res2.append(currents_res[0])
|
237
|
+
## implicit euler time iterations
|
238
|
+
sol = spsolve(S2+dt*S1,b*dt+S2@sol)
|
239
|
+
import matplotlib.pyplot as plt
|
240
|
+
plt.xlabel("Time")
|
241
|
+
plt.ylabel("Intensity")
|
242
|
+
plt.plot(vals_res1,label="intensity res 1")
|
243
|
+
plt.plot(vals_res2,label="intensity res 2")
|
244
|
+
plt.legend()
|
245
|
+
plt.savefig("intensities_res.png")
|
246
|
+
```
|
247
|
+
|
248
|
+
This outputs the following graph that displays the intensity passing through the resistances
|
249
|
+

|
250
|
+
|
251
|
+
|
252
|
+
## Solver suggestions
|
253
|
+
|
254
|
+
- For **small or moderately sized systems**, the built-in `scipy.sparse.linalg.spsolve` is effective.
|
255
|
+
- For **large-scale temporal problems**, consider using **MUMPS** (via `pyMUMPS`).
|
256
|
+
MUMPS is more efficient when only the second member (`b`) changes during time-stepping.
|
257
|
+
|
258
|
+
> [!TIP]
|
259
|
+
> See example `tests.test_temporal_system` in the tests on how to use pyMUMPS for solving the resulting system efficiently.
|
260
|
+
|
@@ -0,0 +1,10 @@
|
|
1
|
+
ElecSolver/FrequencySystemBuilder.py,sha256=8GoF8R1d1Qfs3f2y4tXJfu5Yq7iPrZXM-cwogdzu5v0,11962
|
2
|
+
ElecSolver/TemporalSystemBuilder.py,sha256=ierqBI2Vr9LoLVi9ddNzKo7Z7dmZ3uPEywGHn8F73jg,23515
|
3
|
+
ElecSolver/__init__.py,sha256=jYln4Hwpz5Y7aXUxCnHU6kPs--2MXc58rbaTTQvcE8I,224
|
4
|
+
ElecSolver/_version.py,sha256=-LyU5F1uZDjn6Q8_Z6-_FJt_8RE4Kq9zcKdg1abSSps,511
|
5
|
+
ElecSolver/utils.py,sha256=M-42jBk3BW0jPq1DDAC04VwrDIgTAS0sI9q28l2j5YY,5215
|
6
|
+
elecsolver-0.1.0.dist-info/licenses/LICENCE.txt,sha256=v-lfIiLUg0gkkSbraLUejUcFsyG7SbKQaK7GqgIEWFQ,1068
|
7
|
+
elecsolver-0.1.0.dist-info/METADATA,sha256=Uy8P0iJJIVj3ZRJ99o8SBJ-ueLrx5UZiXhzT_9DZjG4,8548
|
8
|
+
elecsolver-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
9
|
+
elecsolver-0.1.0.dist-info/top_level.txt,sha256=2toqgPNV9y44OzukZZEL4qeFZFkNR1GXZjIlZVmC-Ic,11
|
10
|
+
elecsolver-0.1.0.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 William PIAT
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1 @@
|
|
1
|
+
ElecSolver
|