Coverage for pyrc \ core \ solver \ stationary.py: 93%
46 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-15 07:54 +0200
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-15 07:54 +0200
1# -------------------------------------------------------------------------------
2# Copyright (C) 2026 Joel Kimmich, Tim Jourdan
3# ------------------------------------------------------------------------------
4# License
5# This file is part of PyRC, distributed under GPL-3.0-or-later.
6# ------------------------------------------------------------------------------
8import numpy as np
9import sympy as sp
10from scipy.sparse import csr_matrix, spmatrix, sparray, issparse
11from scipy.sparse.linalg import spsolve
12from sympy import MutableDenseMatrix, MutableSparseMatrix, ImmutableSparseMatrix
15def _to_sympy_matrix(matrix: csr_matrix | sp.MatrixBase) -> sp.MatrixBase:
16 """Convert a sparse matrix to a sympy Matrix if not already one."""
17 if isinstance(matrix, sp.MatrixBase):
18 return matrix
19 return sp.Matrix(matrix.toarray())
22def _to_sparse_matrix(matrix: csr_matrix | sp.MatrixBase) -> csr_matrix:
23 """Convert a sympy Matrix to a scipy csr_matrix if not already one."""
24 if issparse(matrix):
25 return matrix
26 return csr_matrix(np.array(matrix.tolist(), dtype=float))
29def _has_free_symbols(*matrices: csr_matrix | sp.MatrixBase | np.ndarray | None) -> bool:
30 """Return True if any of the given matrices contains free sympy symbols."""
31 return any(isinstance(matrix, sp.MatrixBase) and matrix.free_symbols for matrix in matrices if matrix is not None)
34def _solve_symbolic(
35 system_matrix: csr_matrix | sp.MatrixBase,
36 input_matrix: csr_matrix | sp.MatrixBase | None,
37 input_vector: np.ndarray | sp.MatrixBase | None,
38) -> sp.MatrixBase:
39 """Solve the stationary system symbolically using sympy."""
40 system_matrix = _to_sympy_matrix(system_matrix)
41 number_of_nodes = system_matrix.shape[0]
42 if input_matrix is None:
43 right_hand_side = sp.zeros(number_of_nodes, 1)
44 else:
45 input_matrix = _to_sympy_matrix(input_matrix)
46 input_vector = sp.Matrix(input_vector) if not isinstance(input_vector, sp.MatrixBase) else input_vector
47 if input_vector.shape[1] != 1:
48 input_vector = input_vector.reshape(len(input_vector), 1)
49 right_hand_side = -(input_matrix * input_vector)
50 return system_matrix.solve(right_hand_side)
53def _solve_numeric(
54 system_matrix: csr_matrix | sp.MatrixBase,
55 input_matrix: csr_matrix | sp.MatrixBase | None,
56 input_vector: np.ndarray | sp.MatrixBase | None,
57) -> np.ndarray:
58 """Solve the stationary system numerically using scipy sparse."""
59 system_matrix = _to_sparse_matrix(system_matrix)
60 number_of_nodes = system_matrix.shape[0]
61 if input_matrix is None:
62 right_hand_side = np.zeros(number_of_nodes)
63 else:
64 input_matrix = _to_sparse_matrix(input_matrix)
65 input_vector_numeric = np.asarray(input_vector, dtype=float).ravel()
66 right_hand_side = -(input_matrix @ input_vector_numeric)
67 if hasattr(right_hand_side, "toarray"):
68 right_hand_side = right_hand_side.toarray().ravel()
69 stationary_temperatures = spsolve(system_matrix.tocsc(), right_hand_side)
70 if np.any(np.isnan(stationary_temperatures)):
71 raise ValueError("System matrix is singular; no unique stationary solution exists.")
72 return stationary_temperatures
75def solve_stationary(
76 system_matrix: csr_matrix | sp.Matrix | MutableSparseMatrix | ImmutableSparseMatrix | spmatrix | sparray,
77 input_matrix: csr_matrix | sp.Matrix | MutableSparseMatrix | ImmutableSparseMatrix | spmatrix | sparray = None,
78 input_vector: np.ndarray | sp.Matrix | sparray = None,
79) -> np.ndarray | sp.Matrix | MutableDenseMatrix:
80 """
81 Solve the stationary solution of a linear RC network: x = -A⁻¹ · B · u.
83 Dispatches to a numeric (scipy sparse) or symbolic (sympy) solver based on whether free symbols are detected in
84 the inputs. A purely numeric sympy matrix is converted and solved numerically. For homogeneous systems,
85 ``input_matrix`` and ``input_vector`` can be omitted, returning the zero vector.
87 Parameters
88 ----------
89 system_matrix : csr_matrix | sp.Matrix | MutableSparseMatrix | ImmutableSparseMatrix | spmatrix | sparray
90 System matrix (Jacobian) of shape (n, n).
91 input_matrix : csr_matrix | sp.Matrix | MutableSparseMatrix | ImmutableSparseMatrix | spmatrix | sparray, optional
92 Input matrix of shape (n, m). Must be provided together with ``input_vector``.
93 input_vector : np.ndarray | sp.Matrix, optional
94 Input vector of shape (m,) or (m, 1). Must be provided together with ``input_matrix``.
96 Returns
97 -------
98 np.ndarray | sp.Matrix | MutableDenseMatrix :
99 Stationary temperature vector of shape (n,) for numeric, (n, 1) for symbolic.
101 Raises
102 ------
103 ValueError :
104 If only one of ``input_matrix`` and ``input_vector`` is provided.
105 ValueError :
106 If the system matrix is singular.
107 """
108 if (input_matrix is None) != (input_vector is None):
109 raise ValueError("Both input_matrix and input_vector must be provided together, or both omitted.")
111 if _has_free_symbols(system_matrix, input_matrix, input_vector):
112 return _solve_symbolic(system_matrix, input_matrix, input_vector)
113 else:
114 return _solve_numeric(system_matrix, input_matrix, input_vector)