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.
@@ -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
@@ -0,0 +1,7 @@
1
+ try:
2
+ from ._version import __version__
3
+ except ImportError: # pragma: no cover
4
+ __version__ = "None"
5
+
6
+ from .FrequencySystemBuilder import FrequencySystemBuilder
7
+ from .TemporalSystemBuilder import TemporalSystemBuilder
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
+ ![Multiple system](img/schema.png)
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
+ ![Parallel system](img/schema3.png)
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
+ ![Temporal system](img/schema2.png)
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
+ ![Temporal system](img/intensities_res.png)
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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