rtds-circuit-analysis 0.0.1__tar.gz

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.
Files changed (28) hide show
  1. rtds_circuit_analysis-0.0.1/LICENSE +22 -0
  2. rtds_circuit_analysis-0.0.1/PKG-INFO +118 -0
  3. rtds_circuit_analysis-0.0.1/README.md +102 -0
  4. rtds_circuit_analysis-0.0.1/pyproject.toml +22 -0
  5. rtds_circuit_analysis-0.0.1/setup.cfg +4 -0
  6. rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis/__init__.py +1 -0
  7. rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis/circuit.py +137 -0
  8. rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis/diference_equations.py +224 -0
  9. rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis/equivalent_circuit.py +353 -0
  10. rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis/format_output.py +165 -0
  11. rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis/parse_data.py +58 -0
  12. rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis/parse_netlist.py +174 -0
  13. rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis/solve_circuit.py +349 -0
  14. rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis/utils.py +25 -0
  15. rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis.egg-info/PKG-INFO +118 -0
  16. rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis.egg-info/SOURCES.txt +26 -0
  17. rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis.egg-info/dependency_links.txt +1 -0
  18. rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis.egg-info/entry_points.txt +2 -0
  19. rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis.egg-info/requires.txt +4 -0
  20. rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis.egg-info/top_level.txt +2 -0
  21. rtds_circuit_analysis-0.0.1/src/rtds_cli/__main__.py +3 -0
  22. rtds_circuit_analysis-0.0.1/src/rtds_cli/app.py +18 -0
  23. rtds_circuit_analysis-0.0.1/src/rtds_cli/create_parser.py +93 -0
  24. rtds_circuit_analysis-0.0.1/src/rtds_cli/errors.py +29 -0
  25. rtds_circuit_analysis-0.0.1/src/rtds_cli/print_data.py +33 -0
  26. rtds_circuit_analysis-0.0.1/tests/test_parse_value.py +120 -0
  27. rtds_circuit_analysis-0.0.1/tests/test_state_equations.py +869 -0
  28. rtds_circuit_analysis-0.0.1/tests/test_state_equations_helpers.py +58 -0
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Guilherme Natan Ferro Braga
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.
22
+
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: rtds-circuit-analysis
3
+ Version: 0.0.1
4
+ Summary: Solve electrical circuits
5
+ Author-email: Guilherme Natan <guilherme.natan.braga@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Guilherme-Natan/rtds-circuit-analysis
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: sympy>=1.13.3
12
+ Requires-Dist: networkx>=3.4.2
13
+ Requires-Dist: scipy>=1.16.3
14
+ Requires-Dist: pytest>=8.3.5
15
+ Dynamic: license-file
16
+
17
+ # RTDS Circuit Analysis
18
+
19
+ RTDS Circuit Analysis is a Python software for solving electrical circuits, when given their [netlist](#netlist).
20
+
21
+ ## Installation
22
+
23
+ If you plan on using the [command line interface](#command-line-interface) for this software, you can use
24
+ [pipx](https://github.com/pypa/pipx) for this:
25
+
26
+ <!-- TODO: Check if the names for the pipx and pip packages are correct -->
27
+ ```bash
28
+ pipx install rtds-circuit-analysis
29
+ ```
30
+
31
+ If you want to use the [library](#library), however, you might prefer using [pip](https://pip.pypa.io/en/stable/) for the installation:
32
+ ```bash
33
+ pip install rtds-circuit-analysis
34
+ ```
35
+
36
+ ## Netlist
37
+
38
+ Before actually [using](#usage) this program, you will need to write the circuit you want to analise as a *netlist*.
39
+ This software uses a netlist style similar to SPICE programs, where each line corresponds to each component in the
40
+ circuit. They will be formatted like `Vin in 0 V`, where:
41
+ - The the first letter in the first word, `V` is the component type (V for voltage source, R for resistors, etc), and
42
+ together with the rest, `Vin`, make up the component name.
43
+ - The second and third words are the nodes the component is connected to. Their names are arbitrary, except for the node
44
+ 0, which will **always** be ground. It is also **always** necessary.
45
+ - The final word will be the value for the circuit. It can be a literal (such as in this case, `V`), a numeric value
46
+ (like `10k`), or a combination of both (like `10V`).
47
+
48
+ <!-- TODO: Link to the sphinx section about netlists here -->
49
+
50
+ For example, here is a basic RLC circuit...
51
+
52
+ ![rlc_circuit](./docs/_static/rlc_circuit.png)
53
+
54
+ ... and its netlist representation:
55
+ ```
56
+ V1 1 0 10
57
+ R1 1 2 1k
58
+ L1 2 3 100m
59
+ C1 3 0 10u
60
+ ```
61
+
62
+ ## Usage
63
+
64
+ ### Command Line Interface
65
+
66
+ The basic usage for the CLI software is in the form:
67
+
68
+ ```bash
69
+ rtds-circuit-analysis netlist.cir
70
+ ```
71
+
72
+ `netlist.cir` being the path for the file containing the [netlist](#netlist). This will print all the solutions for the
73
+ circuit (node voltages, currents, states, etc). You can use flags to filter the output. For example, to show only the
74
+ currents for each component, you can run:
75
+
76
+ ```bash
77
+ rtds-circuit-analysis netlist.cir -i
78
+ ```
79
+
80
+ You can filter it even more, by giving the components/node names to the flags as parameters. So, if you want the voltage
81
+ and current for the resistor R1 only, you can run:
82
+
83
+ ```bash
84
+ rtds-circuit-analysis netlist.cir -v R1 -i R1
85
+ ```
86
+
87
+ <!-- TODO: Link the section about the cli in the sphinx docs here -->
88
+
89
+ ### Library
90
+
91
+ To solve the circuit, you need to import the `Circuit` class, and pass the netlist as a parameter, creating a circuit
92
+ object.
93
+
94
+ ```python
95
+ from rtds_circuit_analysis import Circuit
96
+
97
+ circuit = Circuit("netlist.cir")
98
+ ```
99
+
100
+ This Circuit object will have all circuit solutions stored as attributes. So, for example, to find the voltage at node
101
+ "2", you can use:
102
+
103
+ ```python
104
+ circuit.node_voltages["2"]
105
+ ```
106
+
107
+ You can also use the `print` methods to print the solutions for the circuit in a formatted manner, similar to the
108
+ [CLI](#command-line-interface). For example, to print the voltages for the components R1 and R2, you can run:
109
+
110
+ ```python
111
+ circuit.print_component_voltages("R1", "R2")
112
+ ```
113
+
114
+ <!-- TODO: Link the section about the library in the sphinx docs here -->
115
+
116
+ ## License
117
+
118
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,102 @@
1
+ # RTDS Circuit Analysis
2
+
3
+ RTDS Circuit Analysis is a Python software for solving electrical circuits, when given their [netlist](#netlist).
4
+
5
+ ## Installation
6
+
7
+ If you plan on using the [command line interface](#command-line-interface) for this software, you can use
8
+ [pipx](https://github.com/pypa/pipx) for this:
9
+
10
+ <!-- TODO: Check if the names for the pipx and pip packages are correct -->
11
+ ```bash
12
+ pipx install rtds-circuit-analysis
13
+ ```
14
+
15
+ If you want to use the [library](#library), however, you might prefer using [pip](https://pip.pypa.io/en/stable/) for the installation:
16
+ ```bash
17
+ pip install rtds-circuit-analysis
18
+ ```
19
+
20
+ ## Netlist
21
+
22
+ Before actually [using](#usage) this program, you will need to write the circuit you want to analise as a *netlist*.
23
+ This software uses a netlist style similar to SPICE programs, where each line corresponds to each component in the
24
+ circuit. They will be formatted like `Vin in 0 V`, where:
25
+ - The the first letter in the first word, `V` is the component type (V for voltage source, R for resistors, etc), and
26
+ together with the rest, `Vin`, make up the component name.
27
+ - The second and third words are the nodes the component is connected to. Their names are arbitrary, except for the node
28
+ 0, which will **always** be ground. It is also **always** necessary.
29
+ - The final word will be the value for the circuit. It can be a literal (such as in this case, `V`), a numeric value
30
+ (like `10k`), or a combination of both (like `10V`).
31
+
32
+ <!-- TODO: Link to the sphinx section about netlists here -->
33
+
34
+ For example, here is a basic RLC circuit...
35
+
36
+ ![rlc_circuit](./docs/_static/rlc_circuit.png)
37
+
38
+ ... and its netlist representation:
39
+ ```
40
+ V1 1 0 10
41
+ R1 1 2 1k
42
+ L1 2 3 100m
43
+ C1 3 0 10u
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ ### Command Line Interface
49
+
50
+ The basic usage for the CLI software is in the form:
51
+
52
+ ```bash
53
+ rtds-circuit-analysis netlist.cir
54
+ ```
55
+
56
+ `netlist.cir` being the path for the file containing the [netlist](#netlist). This will print all the solutions for the
57
+ circuit (node voltages, currents, states, etc). You can use flags to filter the output. For example, to show only the
58
+ currents for each component, you can run:
59
+
60
+ ```bash
61
+ rtds-circuit-analysis netlist.cir -i
62
+ ```
63
+
64
+ You can filter it even more, by giving the components/node names to the flags as parameters. So, if you want the voltage
65
+ and current for the resistor R1 only, you can run:
66
+
67
+ ```bash
68
+ rtds-circuit-analysis netlist.cir -v R1 -i R1
69
+ ```
70
+
71
+ <!-- TODO: Link the section about the cli in the sphinx docs here -->
72
+
73
+ ### Library
74
+
75
+ To solve the circuit, you need to import the `Circuit` class, and pass the netlist as a parameter, creating a circuit
76
+ object.
77
+
78
+ ```python
79
+ from rtds_circuit_analysis import Circuit
80
+
81
+ circuit = Circuit("netlist.cir")
82
+ ```
83
+
84
+ This Circuit object will have all circuit solutions stored as attributes. So, for example, to find the voltage at node
85
+ "2", you can use:
86
+
87
+ ```python
88
+ circuit.node_voltages["2"]
89
+ ```
90
+
91
+ You can also use the `print` methods to print the solutions for the circuit in a formatted manner, similar to the
92
+ [CLI](#command-line-interface). For example, to print the voltages for the components R1 and R2, you can run:
93
+
94
+ ```python
95
+ circuit.print_component_voltages("R1", "R2")
96
+ ```
97
+
98
+ <!-- TODO: Link the section about the library in the sphinx docs here -->
99
+
100
+ ## License
101
+
102
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools >= 77.0.3"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "rtds-circuit-analysis"
7
+ version = "0.0.1"
8
+ authors = [
9
+ { name="Guilherme Natan", email="guilherme.natan.braga@gmail.com" },
10
+ ]
11
+ description = "Solve electrical circuits"
12
+ readme = "README.md"
13
+ requires-python = ">=3.9"
14
+ dependencies = ["sympy>=1.13.3", "networkx>=3.4.2", "scipy>=1.16.3" , "pytest>=8.3.5"]
15
+ license = "MIT"
16
+ license-files = ["LICENSE"]
17
+
18
+ [project.scripts]
19
+ rtds-circuit-analysis = "rtds_cli.app:app"
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/Guilherme-Natan/rtds-circuit-analysis"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ from .circuit import Circuit
@@ -0,0 +1,137 @@
1
+ """Module that contains the circuit class"""
2
+
3
+ import os
4
+
5
+ from rtds_circuit_analysis.diference_equations import differential_to_difference
6
+ from rtds_circuit_analysis.format_output import format_output
7
+ from rtds_circuit_analysis.parse_data import parse_data
8
+ from rtds_circuit_analysis.parse_netlist import get_lines, parse_components
9
+ from rtds_circuit_analysis.solve_circuit import solve_circuit
10
+
11
+
12
+ class Circuit:
13
+ """Class that represents a circuit and all its data."""
14
+
15
+ def __init__(self, circuit_data: str, time_step: str | None = None):
16
+ if os.path.exists(circuit_data):
17
+ circuit_data = get_lines(circuit_data)
18
+ else:
19
+ circuit_data = circuit_data.strip().split("\n")
20
+
21
+ self.components, possible_time_step = parse_components(circuit_data)
22
+ if possible_time_step:
23
+ time_step = possible_time_step
24
+
25
+ parse_data(self.components)
26
+
27
+ (
28
+ self.components,
29
+ self.currents,
30
+ self.component_voltages,
31
+ self.node_voltages,
32
+ self.states,
33
+ ) = solve_circuit(self.components)
34
+
35
+ if self.states:
36
+ self.forward, self.backward, self.trapezoidal = differential_to_difference(self.states, time_step)
37
+ else:
38
+ self.forward = self.backward = self.trapezoidal = None
39
+
40
+ def _formatted_components(self):
41
+ return f'*** Components for the circuit ***\n{"\n".join(str(component) for component in self.components)}\n'
42
+
43
+ def _formatted_currents(self, components=None):
44
+ return f"*** Currents ***\n{format_output(self.currents, components)}"
45
+
46
+ def _formatted_component_voltages(self, components=None):
47
+ return f"*** Voltages (components) ***\n{format_output(self.component_voltages, components)}"
48
+
49
+ def _formatted_node_voltages(self, nodes=None):
50
+ return f"*** Voltages (nodes) ***\n{format_output(self.node_voltages, nodes)}"
51
+
52
+ def _formatted_states(self, components=None):
53
+ return f"*** State equations (continuous) ***\
54
+ \n{format_output(self.states, components, is_state=True, lhs_is_derivative=True)}"
55
+
56
+ def _formatted_forward(self, components=None):
57
+ return f"*** State equations (forward) ***\
58
+ \n{format_output(self.forward, components, is_state=True, is_discrete=True)}"
59
+
60
+ def _formatted_backward(self, components=None):
61
+ return f"*** State equations (backward) ***\
62
+ \n{format_output(self.backward, components, is_state=True, is_discrete=True)}"
63
+
64
+ def _formatted_trapezoidal(self, components=None):
65
+ return f"*** State equations (trapezoidal) ***\
66
+ \n{format_output(self.trapezoidal, components, is_state=True, is_discrete=True)}"
67
+
68
+ def print_components(self):
69
+ """Print all components for the circuit"""
70
+ print(self._formatted_components)
71
+
72
+ def print_currents(self, *components):
73
+ """Prints the current for the given components (excluding inductors and current sources, which are obvious). If
74
+ no components are given, prints the currents for *every* component."""
75
+ print(self._formatted_currents(components))
76
+
77
+ def print_component_voltages(self, *components):
78
+ """Prints the voltage for the given components (excluding capacitors and voltage sources, which are obvious). If
79
+ no components are given, prints the voltages for *every* component."""
80
+ print(self._formatted_component_voltages(components))
81
+
82
+ def print_node_voltages(self, *nodes):
83
+ """Prints the voltage for each node. If no nodes are given, prints the voltages for every node."""
84
+ print(self._formatted_node_voltages(nodes))
85
+
86
+ def print_states(self, *components):
87
+ """Prints the state equations for the system (continuous). If no components are given,"""
88
+ print(self._formatted_states(components))
89
+
90
+ def print_forward(self, *components):
91
+ """Prints the state equations for the system (forward)."""
92
+ print(self._formatted_forward(components))
93
+
94
+ def print_backward(self, *components):
95
+ """Prints the state equations for the system (backward)."""
96
+ print(self._formatted_backward(components))
97
+
98
+ def print_trapezoidal(self, *components):
99
+ """Prints the state equations for the system (trapezoidal)."""
100
+ print(self._formatted_trapezoidal(components))
101
+
102
+ def print_all(self):
103
+ """Prints all the info related to the `circuit` object."""
104
+ self.print_currents()
105
+ self.print_component_voltages()
106
+ self.print_node_voltages()
107
+ if self.states:
108
+ self.print_states()
109
+ self.print_forward()
110
+ self.print_backward()
111
+ self.print_trapezoidal()
112
+ else:
113
+ print("*** This circuit is stateless ***")
114
+ print("")
115
+
116
+ def __str__(self):
117
+ return f"""{self._formatted_components()}
118
+ {self._formatted_currents()}
119
+ {self._formatted_component_voltages()}
120
+ {self._formatted_node_voltages()}
121
+ {self._formatted_states()}
122
+ {self._formatted_forward()}
123
+ {self._formatted_backward()}
124
+ {self._formatted_trapezoidal()}"""
125
+
126
+ def __repr__(self):
127
+ return f"""Circuit(
128
+ components={str(self.components)},
129
+ node_voltages={self.node_voltages},
130
+ currents={self.currents},
131
+ component_voltages={self.component_voltages},
132
+ states={self.states}
133
+ forward={self.forward}
134
+ backward={self.backward}
135
+ trapezoidal={self.trapezoidal}
136
+ )
137
+ """
@@ -0,0 +1,224 @@
1
+ """Functions related to converting differential equations to difference equations"""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Callable
5
+
6
+ import sympy as sp
7
+
8
+
9
+ @dataclass
10
+ class DifferenceEquations:
11
+ """Class to store the difference equations for the circuit, in their implicit form."""
12
+
13
+ forward: list[sp.Eq]
14
+ backward: list[sp.Eq]
15
+ trapezoidal: list[sp.Eq]
16
+
17
+
18
+ @dataclass
19
+ class DifferenceSolution:
20
+ """The solutions for each difference method."""
21
+
22
+ forward: dict[str, sp.Eq]
23
+ backward: dict[str, sp.Eq]
24
+ trapezoidal: dict[str, sp.Eq]
25
+
26
+
27
+ @dataclass
28
+ class Symbols:
29
+ """Symbols that correspond to values that vary with time within a circuit. In their differential form, and in their
30
+ equivalent difference form.
31
+ """
32
+
33
+ continuous: list[sp.Expr]
34
+ discrete_equivalent: list[sp.Expr]
35
+
36
+
37
+ def forward(continuous_symbols: sp.Symbol) -> list[sp.Symbol]:
38
+ """Transform a symbol in continuous form to its difference form, using the forward method.
39
+
40
+ Args:
41
+ continuous_symbols (sp.Symbol): Symbol in continuous form.
42
+
43
+ Returns:
44
+ list[sp.Symbol]: Symbol in difference form.
45
+ """
46
+
47
+ return sp.Symbol(str(continuous_symbols) + "_{n-1}")
48
+
49
+
50
+ def backward(continuous_symbols: sp.Symbol) -> list[sp.Symbol]:
51
+ """Transform a symbol in continuous form to its difference form, using the backward method.
52
+
53
+ Args:
54
+ continuous_symbols (sp.Symbol): Symbol in continuous form.
55
+
56
+ Returns:
57
+ list[sp.Symbol]: Symbol in difference form.
58
+ """
59
+ return sp.Symbol(str(continuous_symbols) + "_{n}")
60
+
61
+
62
+ def trapezoidal(continuous_symbols: sp.Symbol) -> list[sp.Expr]:
63
+ """Transform a symbol in continuous form to its difference form, using the trapezoidal method.
64
+
65
+ Args:
66
+ continuous_symbols (sp.Symbol): Symbol in continuous form.
67
+
68
+ Returns:
69
+ list[sp.Expr]: Expression in difference form.
70
+ """
71
+ return (forward(continuous_symbols) + backward(continuous_symbols)) / 2
72
+
73
+
74
+ def convert_to(
75
+ discrete_method: Callable, variable: str, continuous_expression: sp.Expr, time_step: str | None
76
+ ) -> sp.Eq:
77
+ """Convert a state equation in differential form, to its respective difference form.
78
+
79
+ Args:
80
+ discrete_method (Callable): The method to use for the transform.
81
+ variable (str): The variable related to the component's equation, in differential form.
82
+ continuous_expression (sp.Expr): The variable's expression, in continuous form, related to the "right hand side"
83
+ of the state equation.
84
+ time_step (str | None): The time step for the circuit. If it is None, the expressions will receive the
85
+ time step as a "Ts" sympy symbol.
86
+
87
+ Returns:
88
+ sp.Eq: The state equation in difference form.
89
+ """
90
+ # Get the symbols for the expression
91
+ symbols = Symbols(list(continuous_expression.free_symbols), [])
92
+
93
+ # Keep only the symbols that vary with time (voltages and currents, symbols that begin with V or I)
94
+ symbols_filtered = []
95
+ for symbol in symbols.continuous:
96
+ if str(symbol)[0] in ("V", "I"):
97
+ symbols_filtered.append(symbol)
98
+ symbols.continuous = symbols_filtered
99
+
100
+ # Finds the equivalent form of each continuous symbol for the discrete method used
101
+ for symbol in symbols.continuous:
102
+ symbols.discrete_equivalent.append(discrete_method(symbol))
103
+
104
+ # Substitutes the discrete symbols in the continuous expression
105
+ discrete_expression = continuous_expression
106
+ for symbol_continuous, symbol_discrete in zip(symbols.continuous, symbols.discrete_equivalent):
107
+ discrete_expression = discrete_expression.subs(symbol_continuous, symbol_discrete)
108
+
109
+ # Returns the equation
110
+ new_value, old_value = sp.Symbol(variable + "_{n}"), sp.Symbol(variable + "_{n-1}")
111
+ if not time_step:
112
+ time_step = sp.Symbol("Ts")
113
+ else:
114
+ time_step = sp.Rational(time_step)
115
+ return sp.Eq(new_value, old_value + time_step * discrete_expression)
116
+
117
+
118
+ def keys_to_string(dictionary: dict) -> dict:
119
+ """Transforms every key in a dictionary to a string.
120
+
121
+ Args:
122
+ dictionary (dict): Dictionary to transform
123
+
124
+ Returns:
125
+ dict: Dictionary, with its keys as strings.
126
+ """
127
+ return {str(k): v for k, v in dictionary.items()}
128
+
129
+
130
+ def variable_to_component(solutions: dict[str, sp.Expr]) -> dict[str, sp.Expr]:
131
+ """Extracts the name of the capacitor/inductor from their voltage/current literal values. This is done by simply
132
+ removing the first letter (V or I), and the last four character (_{n}) from the string.
133
+
134
+ Args:
135
+ solutions (dict[str, sp.Expr]): A dictionary, where the keys are the components state variables.
136
+
137
+ Returns:
138
+ dict[str, sp.Expr]: A dictionary, where the keys are the component names.
139
+ """
140
+ # By removing the first and 4 last letters from the variable name, the component is extracted
141
+ return {variable[1:-4]: expression for variable, expression in solutions.items()}
142
+
143
+
144
+ def symbol_to_component(solutions: dict[sp.Symbol, sp.Expr]) -> dict[str, sp.Expr]:
145
+ """Transforms the keys for the solutions dictionary from sympy symbols to the component names they relate to.
146
+
147
+ Args:
148
+ solutions (dict[sp.Symbol, sp.Expr]): The dictionary, where its keys are sympy symbols.
149
+
150
+ Returns:
151
+ dict[str, sp.Expr]: Dictionary, with keys related to the component names.
152
+ """
153
+ solutions = keys_to_string(solutions)
154
+ return variable_to_component(solutions)
155
+
156
+
157
+ def convert_explicit(
158
+ discrete_method: Callable,
159
+ states_continuous: list[sp.Expr],
160
+ to_solve_for: list[sp.Expr],
161
+ time_step: str | None,
162
+ ) -> dict[str, sp.Expr]:
163
+ """Convert the state equations in continuous form into their (explicit) discrete form.
164
+
165
+ Args:
166
+ discrete_method (Callable): The method to use for the transform.
167
+ states_continuous (list[sp.Expr]): The state equations for the circuit, in continuous form.
168
+ to_solve_for (list[sp.Expr]):
169
+ time_step (str | None): The time step for the circuit. If it is None, the expressions will receive the
170
+ time step as a "Ts" sympy symbol.
171
+
172
+ Returns:
173
+ dict[str, sp.Expr]: The state equations for the chosen method
174
+ """
175
+ # Get the diference equations (implicit form)
176
+ difference_equations = []
177
+ for variable, state_expression in states_continuous.items():
178
+ difference_equations.append(convert_to(discrete_method, variable, state_expression, time_step))
179
+
180
+ # Turn them into their explicit form
181
+ solutions = sp.solve(difference_equations, to_solve_for)
182
+
183
+ # Turn the keys in the solutions dictionaries
184
+ return symbol_to_component(solutions)
185
+
186
+
187
+ def differential_to_difference(
188
+ states_continuous: list[sp.Expr], time_step: str | None = None
189
+ ) -> tuple[dict[str, sp.Expr], dict[str, sp.Expr], dict[str, sp.Expr]]:
190
+ """Transform the state equations in continuous form in the circuit, into their difference form, for the forward,
191
+ backward and trapezoidal methods.
192
+
193
+ Args:
194
+ states_continuous (list[sp.Expr]): List of differential equations.
195
+ time_step (str | None): The time step for the circuit. If it is None, the expressions will receive the
196
+ time step as a "Ts" sympy symbol.
197
+
198
+
199
+ Returns:
200
+ tuple[dict[str, sp.Expr], dict[str, sp.Expr], dict[str, sp.Expr]]: State equations in differential form,
201
+ for various methods.
202
+ """
203
+ # Turn the keys from the component name to the variable related to this component.
204
+ states_continuous_variables = {}
205
+ for component, expression in states_continuous.items():
206
+ match component[0]:
207
+ case "C":
208
+ component = "V" + component
209
+ case "L":
210
+ component = "I" + component
211
+ states_continuous_variables[component] = expression
212
+ states_continuous = states_continuous_variables
213
+
214
+ # Extract the symbols
215
+ to_solve_for = []
216
+ for variable in states_continuous:
217
+ to_solve_for.append(sp.Symbol(variable + "_{n}"))
218
+
219
+ # Get the difference equations for each method
220
+ forward_solutions = convert_explicit(forward, states_continuous, to_solve_for, time_step)
221
+ backward_solutions = convert_explicit(backward, states_continuous, to_solve_for, time_step)
222
+ trapezoidal_solutions = convert_explicit(trapezoidal, states_continuous, to_solve_for, time_step)
223
+
224
+ return forward_solutions, backward_solutions, trapezoidal_solutions