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.
- rtds_circuit_analysis-0.0.1/LICENSE +22 -0
- rtds_circuit_analysis-0.0.1/PKG-INFO +118 -0
- rtds_circuit_analysis-0.0.1/README.md +102 -0
- rtds_circuit_analysis-0.0.1/pyproject.toml +22 -0
- rtds_circuit_analysis-0.0.1/setup.cfg +4 -0
- rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis/__init__.py +1 -0
- rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis/circuit.py +137 -0
- rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis/diference_equations.py +224 -0
- rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis/equivalent_circuit.py +353 -0
- rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis/format_output.py +165 -0
- rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis/parse_data.py +58 -0
- rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis/parse_netlist.py +174 -0
- rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis/solve_circuit.py +349 -0
- rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis/utils.py +25 -0
- rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis.egg-info/PKG-INFO +118 -0
- rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis.egg-info/SOURCES.txt +26 -0
- rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis.egg-info/dependency_links.txt +1 -0
- rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis.egg-info/entry_points.txt +2 -0
- rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis.egg-info/requires.txt +4 -0
- rtds_circuit_analysis-0.0.1/src/rtds_circuit_analysis.egg-info/top_level.txt +2 -0
- rtds_circuit_analysis-0.0.1/src/rtds_cli/__main__.py +3 -0
- rtds_circuit_analysis-0.0.1/src/rtds_cli/app.py +18 -0
- rtds_circuit_analysis-0.0.1/src/rtds_cli/create_parser.py +93 -0
- rtds_circuit_analysis-0.0.1/src/rtds_cli/errors.py +29 -0
- rtds_circuit_analysis-0.0.1/src/rtds_cli/print_data.py +33 -0
- rtds_circuit_analysis-0.0.1/tests/test_parse_value.py +120 -0
- rtds_circuit_analysis-0.0.1/tests/test_state_equations.py +869 -0
- 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
|
+

|
|
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
|
+

|
|
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 @@
|
|
|
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
|