pyBondGraph 0.2.0__tar.gz → 0.3.0__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.
- {pybondgraph-0.2.0 → pybondgraph-0.3.0}/PKG-INFO +47 -16
- {pybondgraph-0.2.0 → pybondgraph-0.3.0}/README.md +47 -16
- {pybondgraph-0.2.0 → pybondgraph-0.3.0}/pyBondGraph/__init__.py +3 -2
- {pybondgraph-0.2.0 → pybondgraph-0.3.0}/pyBondGraph/bondgraph.py +287 -58
- {pybondgraph-0.2.0 → pybondgraph-0.3.0}/pyBondGraph/core.py +38 -8
- {pybondgraph-0.2.0 → pybondgraph-0.3.0}/pyBondGraph.egg-info/PKG-INFO +47 -16
- {pybondgraph-0.2.0 → pybondgraph-0.3.0}/pyproject.toml +1 -1
- {pybondgraph-0.2.0 → pybondgraph-0.3.0}/LICENSE +0 -0
- {pybondgraph-0.2.0 → pybondgraph-0.3.0}/pyBondGraph/elements.py +0 -0
- {pybondgraph-0.2.0 → pybondgraph-0.3.0}/pyBondGraph/sensors.py +0 -0
- {pybondgraph-0.2.0 → pybondgraph-0.3.0}/pyBondGraph/subbondgraph.py +0 -0
- {pybondgraph-0.2.0 → pybondgraph-0.3.0}/pyBondGraph.egg-info/SOURCES.txt +0 -0
- {pybondgraph-0.2.0 → pybondgraph-0.3.0}/pyBondGraph.egg-info/dependency_links.txt +0 -0
- {pybondgraph-0.2.0 → pybondgraph-0.3.0}/pyBondGraph.egg-info/requires.txt +0 -0
- {pybondgraph-0.2.0 → pybondgraph-0.3.0}/pyBondGraph.egg-info/top_level.txt +0 -0
- {pybondgraph-0.2.0 → pybondgraph-0.3.0}/requirements.txt +0 -0
- {pybondgraph-0.2.0 → pybondgraph-0.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyBondGraph
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Modelling tool for linear bond graph systems in Python
|
|
5
5
|
Author-email: Matthias Panny <matthias.panny@mci.edu>
|
|
6
6
|
License-Expression: CC-BY-NC-SA-4.0
|
|
@@ -36,9 +36,15 @@ Bond graphs provide a **domain-independent modeling framework** for physical sys
|
|
|
36
36
|
# Features
|
|
37
37
|
|
|
38
38
|
* Programmatic construction of **bond graph models**
|
|
39
|
+
* **Automatic causality assignment** via SCAP (Sequential Causality Assignment Procedure), with optional manual override or mixed mode
|
|
39
40
|
* Automatic **symbolic equation derivation** using SymPy
|
|
40
|
-
* Conversion of models to **state-space systems**
|
|
41
|
-
*
|
|
41
|
+
* Conversion of models to **linear state-space systems** ($\dot{x} = Ax + Bu$, $y = Cx + Du$)
|
|
42
|
+
* **Composable sub-models** via `SubBondGraph` with deep-copy namespace isolation
|
|
43
|
+
* **Two-port elements**: Transformer and Gyrator with automatic causality propagation
|
|
44
|
+
* **Sensor elements**: `IntegratedEffortSensor` and `IntegratedFlowSensor` for measuring integrated generalized variables (e.g. position from velocity)
|
|
45
|
+
* **Domain-neutral aliases**: `Compliance` = `Capacitor`, `Inertance` = `Inductor`, `Resistance` = `Resistor`
|
|
46
|
+
* Example models for electrical and electromechanical systems
|
|
47
|
+
* Integration with **python-control** for numerical simulation (step response, Bode plots, etc.)
|
|
42
48
|
|
|
43
49
|
---
|
|
44
50
|
|
|
@@ -81,12 +87,12 @@ Optional dependencies are used for experimental visualization tools.
|
|
|
81
87
|
---
|
|
82
88
|
|
|
83
89
|
# Basic Usage
|
|
84
|
-
A bond graph model is constructed by creating elements and connecting them via bonds.
|
|
90
|
+
A bond graph model is constructed by creating elements and connecting them via the `connect()` convenience method, which creates bonds and adds them to the graph in one step.
|
|
85
91
|
|
|
86
|
-
|
|
92
|
+
## RC-Filter with automatic causality (SCAP)
|
|
87
93
|
|
|
88
94
|
```python
|
|
89
|
-
from pyBondGraph import BondGraph, SourceEffort, Resistor, Capacitor, OneJunction
|
|
95
|
+
from pyBondGraph import BondGraph, SourceEffort, Resistor, Capacitor, OneJunction
|
|
90
96
|
|
|
91
97
|
bg = BondGraph()
|
|
92
98
|
|
|
@@ -96,19 +102,20 @@ resistor = Resistor("R", "R")
|
|
|
96
102
|
capacitor = Capacitor("C", "C")
|
|
97
103
|
series_junction = OneJunction("J1")
|
|
98
104
|
|
|
99
|
-
# connect elements
|
|
100
|
-
|
|
101
|
-
bg.
|
|
102
|
-
bg.
|
|
103
|
-
bg.add_bond(Bond(series_junction, capacitor, Causality.FLOW_OUT))
|
|
105
|
+
# connect elements --> causality is assigned automatically by SCAP
|
|
106
|
+
bg.connect(voltage_source, series_junction)
|
|
107
|
+
bg.connect(series_junction, resistor)
|
|
108
|
+
bg.connect(series_junction, capacitor)
|
|
104
109
|
|
|
105
110
|
# plot the resulting BondGraph
|
|
106
111
|
bg.plot()
|
|
107
112
|
|
|
108
113
|
# derive system equations in linear state space form
|
|
109
|
-
A, B, C, D, x, n_states, n_inputs, n_outputs =
|
|
114
|
+
A, B, C, D, x, n_states, n_inputs, n_outputs = bg.get_state_space()
|
|
110
115
|
```
|
|
111
116
|
|
|
117
|
+
Causality can also be assigned **manually** by passing a `Causality` value to `connect()`, or in **mixed mode** where some bonds are fixed and SCAP resolves the rest.
|
|
118
|
+
|
|
112
119
|
The library automatically derives the **symbolic system equations** describing the dynamics of the model.
|
|
113
120
|
|
|
114
121
|
---
|
|
@@ -126,6 +133,12 @@ Bond graphs represent **power exchange between system components**, where power
|
|
|
126
133
|
| Se | Effort source |
|
|
127
134
|
| Sf | Flow source |
|
|
128
135
|
|
|
136
|
+
## Two-Port Elements
|
|
137
|
+
| Element | Meaning |
|
|
138
|
+
|-------------|------------------------------------------------------------|
|
|
139
|
+
| TF | Transformer — same causality on both bonds |
|
|
140
|
+
| GY | Gyrator — opposite causality on both bonds |
|
|
141
|
+
|
|
129
142
|
## Junctions
|
|
130
143
|
| Junction | Meaning |
|
|
131
144
|
|----------|---------------|
|
|
@@ -139,11 +152,9 @@ Bond graphs represent **power exchange between system components**, where power
|
|
|
139
152
|
| IntegratedFlowSensor | Measures integral of the flow at its bond |
|
|
140
153
|
|
|
141
154
|
In mechanical bond graph models:
|
|
142
|
-
* **flow** corresponds to **velocity**
|
|
155
|
+
* **flow** corresponds to **velocity**, i.e. an *IntegratedFlowSensor* can be used to compute **position**.
|
|
143
156
|
* **effort** corresponds to **force**
|
|
144
157
|
|
|
145
|
-
An **integrated flow sensor** can therefore be used to compute **position**:
|
|
146
|
-
|
|
147
158
|
---
|
|
148
159
|
|
|
149
160
|
# Example Systems
|
|
@@ -153,7 +164,7 @@ The repository contains example models illustrating typical applications of bond
|
|
|
153
164
|
Demonstrates modeling of an electrical circuit using bond graph elements.
|
|
154
165
|
|
|
155
166
|
### DC Motor
|
|
156
|
-
A multi-domain electromechanical system coupling electrical and mechanical dynamics.
|
|
167
|
+
A multi-domain electromechanical system coupling electrical and mechanical dynamics. Also demonstrates integration with the `python-control` package for numerical simulation (step response).
|
|
157
168
|
|
|
158
169
|
### Transformer
|
|
159
170
|
Example of energy transformation between two ports.
|
|
@@ -163,6 +174,18 @@ Classical mass-spring-damper system with two degrees of freedom.
|
|
|
163
174
|
|
|
164
175
|
---
|
|
165
176
|
|
|
177
|
+
# Causality Assignment
|
|
178
|
+
|
|
179
|
+
pyBondGraph supports three modes for assigning causality:
|
|
180
|
+
|
|
181
|
+
1. **Automatic (SCAP)** — omit causality in `connect()` calls; `assign_causality()` is called automatically when solving. The Sequential Causality Assignment Procedure assigns causality in priority order: sources, storage elements (integral causality), resistors, then propagation through junctions and two-port elements.
|
|
182
|
+
2. **Manual** — pass `Causality.EFFORT_OUT` or `Causality.FLOW_OUT` explicitly to each `connect()` call.
|
|
183
|
+
3. **Mixed** — fix causality on some bonds, let SCAP resolve the rest.
|
|
184
|
+
|
|
185
|
+
If a storage element cannot receive integral causality (which would imply a DAE rather than an ODE), a `DerivativeCausalityError` is raised with a clear diagnostic message.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
166
189
|
# Typical Applications
|
|
167
190
|
Bond graph modeling is particularly useful for:
|
|
168
191
|
|
|
@@ -171,3 +194,11 @@ Bond graph modeling is particularly useful for:
|
|
|
171
194
|
* multi-domain energy systems
|
|
172
195
|
* control system modeling
|
|
173
196
|
* teaching system dynamics
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
# Planned Features
|
|
201
|
+
|
|
202
|
+
* **FMU Export**: export bond graph models as Functional Mock-up Units (FMI standard) for interoperability with Simulink, Dymola, OpenModelica, and other FMI-compliant tools
|
|
203
|
+
* **Nonlinear element support**: general nonlinear constitutive laws with Jacobian linearization
|
|
204
|
+
* **Convenience bridge to python-control**: `to_control_ss(params)` method wrapping the existing manual pattern
|
|
@@ -10,9 +10,15 @@ Bond graphs provide a **domain-independent modeling framework** for physical sys
|
|
|
10
10
|
# Features
|
|
11
11
|
|
|
12
12
|
* Programmatic construction of **bond graph models**
|
|
13
|
+
* **Automatic causality assignment** via SCAP (Sequential Causality Assignment Procedure), with optional manual override or mixed mode
|
|
13
14
|
* Automatic **symbolic equation derivation** using SymPy
|
|
14
|
-
* Conversion of models to **state-space systems**
|
|
15
|
-
*
|
|
15
|
+
* Conversion of models to **linear state-space systems** ($\dot{x} = Ax + Bu$, $y = Cx + Du$)
|
|
16
|
+
* **Composable sub-models** via `SubBondGraph` with deep-copy namespace isolation
|
|
17
|
+
* **Two-port elements**: Transformer and Gyrator with automatic causality propagation
|
|
18
|
+
* **Sensor elements**: `IntegratedEffortSensor` and `IntegratedFlowSensor` for measuring integrated generalized variables (e.g. position from velocity)
|
|
19
|
+
* **Domain-neutral aliases**: `Compliance` = `Capacitor`, `Inertance` = `Inductor`, `Resistance` = `Resistor`
|
|
20
|
+
* Example models for electrical and electromechanical systems
|
|
21
|
+
* Integration with **python-control** for numerical simulation (step response, Bode plots, etc.)
|
|
16
22
|
|
|
17
23
|
---
|
|
18
24
|
|
|
@@ -55,12 +61,12 @@ Optional dependencies are used for experimental visualization tools.
|
|
|
55
61
|
---
|
|
56
62
|
|
|
57
63
|
# Basic Usage
|
|
58
|
-
A bond graph model is constructed by creating elements and connecting them via bonds.
|
|
64
|
+
A bond graph model is constructed by creating elements and connecting them via the `connect()` convenience method, which creates bonds and adds them to the graph in one step.
|
|
59
65
|
|
|
60
|
-
|
|
66
|
+
## RC-Filter with automatic causality (SCAP)
|
|
61
67
|
|
|
62
68
|
```python
|
|
63
|
-
from pyBondGraph import BondGraph, SourceEffort, Resistor, Capacitor, OneJunction
|
|
69
|
+
from pyBondGraph import BondGraph, SourceEffort, Resistor, Capacitor, OneJunction
|
|
64
70
|
|
|
65
71
|
bg = BondGraph()
|
|
66
72
|
|
|
@@ -70,19 +76,20 @@ resistor = Resistor("R", "R")
|
|
|
70
76
|
capacitor = Capacitor("C", "C")
|
|
71
77
|
series_junction = OneJunction("J1")
|
|
72
78
|
|
|
73
|
-
# connect elements
|
|
74
|
-
|
|
75
|
-
bg.
|
|
76
|
-
bg.
|
|
77
|
-
bg.add_bond(Bond(series_junction, capacitor, Causality.FLOW_OUT))
|
|
79
|
+
# connect elements --> causality is assigned automatically by SCAP
|
|
80
|
+
bg.connect(voltage_source, series_junction)
|
|
81
|
+
bg.connect(series_junction, resistor)
|
|
82
|
+
bg.connect(series_junction, capacitor)
|
|
78
83
|
|
|
79
84
|
# plot the resulting BondGraph
|
|
80
85
|
bg.plot()
|
|
81
86
|
|
|
82
87
|
# derive system equations in linear state space form
|
|
83
|
-
A, B, C, D, x, n_states, n_inputs, n_outputs =
|
|
88
|
+
A, B, C, D, x, n_states, n_inputs, n_outputs = bg.get_state_space()
|
|
84
89
|
```
|
|
85
90
|
|
|
91
|
+
Causality can also be assigned **manually** by passing a `Causality` value to `connect()`, or in **mixed mode** where some bonds are fixed and SCAP resolves the rest.
|
|
92
|
+
|
|
86
93
|
The library automatically derives the **symbolic system equations** describing the dynamics of the model.
|
|
87
94
|
|
|
88
95
|
---
|
|
@@ -100,6 +107,12 @@ Bond graphs represent **power exchange between system components**, where power
|
|
|
100
107
|
| Se | Effort source |
|
|
101
108
|
| Sf | Flow source |
|
|
102
109
|
|
|
110
|
+
## Two-Port Elements
|
|
111
|
+
| Element | Meaning |
|
|
112
|
+
|-------------|------------------------------------------------------------|
|
|
113
|
+
| TF | Transformer — same causality on both bonds |
|
|
114
|
+
| GY | Gyrator — opposite causality on both bonds |
|
|
115
|
+
|
|
103
116
|
## Junctions
|
|
104
117
|
| Junction | Meaning |
|
|
105
118
|
|----------|---------------|
|
|
@@ -113,11 +126,9 @@ Bond graphs represent **power exchange between system components**, where power
|
|
|
113
126
|
| IntegratedFlowSensor | Measures integral of the flow at its bond |
|
|
114
127
|
|
|
115
128
|
In mechanical bond graph models:
|
|
116
|
-
* **flow** corresponds to **velocity**
|
|
129
|
+
* **flow** corresponds to **velocity**, i.e. an *IntegratedFlowSensor* can be used to compute **position**.
|
|
117
130
|
* **effort** corresponds to **force**
|
|
118
131
|
|
|
119
|
-
An **integrated flow sensor** can therefore be used to compute **position**:
|
|
120
|
-
|
|
121
132
|
---
|
|
122
133
|
|
|
123
134
|
# Example Systems
|
|
@@ -127,7 +138,7 @@ The repository contains example models illustrating typical applications of bond
|
|
|
127
138
|
Demonstrates modeling of an electrical circuit using bond graph elements.
|
|
128
139
|
|
|
129
140
|
### DC Motor
|
|
130
|
-
A multi-domain electromechanical system coupling electrical and mechanical dynamics.
|
|
141
|
+
A multi-domain electromechanical system coupling electrical and mechanical dynamics. Also demonstrates integration with the `python-control` package for numerical simulation (step response).
|
|
131
142
|
|
|
132
143
|
### Transformer
|
|
133
144
|
Example of energy transformation between two ports.
|
|
@@ -137,6 +148,18 @@ Classical mass-spring-damper system with two degrees of freedom.
|
|
|
137
148
|
|
|
138
149
|
---
|
|
139
150
|
|
|
151
|
+
# Causality Assignment
|
|
152
|
+
|
|
153
|
+
pyBondGraph supports three modes for assigning causality:
|
|
154
|
+
|
|
155
|
+
1. **Automatic (SCAP)** — omit causality in `connect()` calls; `assign_causality()` is called automatically when solving. The Sequential Causality Assignment Procedure assigns causality in priority order: sources, storage elements (integral causality), resistors, then propagation through junctions and two-port elements.
|
|
156
|
+
2. **Manual** — pass `Causality.EFFORT_OUT` or `Causality.FLOW_OUT` explicitly to each `connect()` call.
|
|
157
|
+
3. **Mixed** — fix causality on some bonds, let SCAP resolve the rest.
|
|
158
|
+
|
|
159
|
+
If a storage element cannot receive integral causality (which would imply a DAE rather than an ODE), a `DerivativeCausalityError` is raised with a clear diagnostic message.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
140
163
|
# Typical Applications
|
|
141
164
|
Bond graph modeling is particularly useful for:
|
|
142
165
|
|
|
@@ -144,4 +167,12 @@ Bond graph modeling is particularly useful for:
|
|
|
144
167
|
* robotics and mechatronics
|
|
145
168
|
* multi-domain energy systems
|
|
146
169
|
* control system modeling
|
|
147
|
-
* teaching system dynamics
|
|
170
|
+
* teaching system dynamics
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
# Planned Features
|
|
175
|
+
|
|
176
|
+
* **FMU Export**: export bond graph models as Functional Mock-up Units (FMI standard) for interoperability with Simulink, Dymola, OpenModelica, and other FMI-compliant tools
|
|
177
|
+
* **Nonlinear element support**: general nonlinear constitutive laws with Jacobian linearization
|
|
178
|
+
* **Convenience bridge to python-control**: `to_control_ss(params)` method wrapping the existing manual pattern
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from .core import Bond, Causality
|
|
1
|
+
from .core import Bond, Causality, CausalityError, DerivativeCausalityError, Port # port is a type alias: dict[str, Node]
|
|
2
2
|
from .elements import (
|
|
3
3
|
SourceEffort,
|
|
4
4
|
SourceFlow,
|
|
@@ -15,7 +15,6 @@ from .elements import (
|
|
|
15
15
|
)
|
|
16
16
|
from .sensors import IntegratedEffortSensor, IntegratedFlowSensor
|
|
17
17
|
from .subbondgraph import SubBondGraph
|
|
18
|
-
from .core import Port # type alias: dict[str, Node]
|
|
19
18
|
|
|
20
19
|
from .bondgraph import BondGraph
|
|
21
20
|
|
|
@@ -39,4 +38,6 @@ __all__ = [
|
|
|
39
38
|
"IntegratedFlowSensor",
|
|
40
39
|
"Port",
|
|
41
40
|
"SubBondGraph",
|
|
41
|
+
"CausalityError",
|
|
42
|
+
"DerivativeCausalityError",
|
|
42
43
|
]
|
|
@@ -10,16 +10,8 @@ import matplotlib.pyplot as plt
|
|
|
10
10
|
from collections.abc import Callable
|
|
11
11
|
from typing import TYPE_CHECKING
|
|
12
12
|
|
|
13
|
-
from .core import
|
|
14
|
-
|
|
15
|
-
Node,
|
|
16
|
-
StatefulElement,
|
|
17
|
-
Bond,
|
|
18
|
-
ElementOnePort,
|
|
19
|
-
ElementTwoPort,
|
|
20
|
-
Junction,
|
|
21
|
-
)
|
|
22
|
-
from .elements import SourceEffort, SourceFlow, OneJunction, ZeroJunction
|
|
13
|
+
from .core import Causality, CausalityError, DerivativeCausalityError, Node, StatefulElement, Bond, ElementOnePort, ElementTwoPort, Junction
|
|
14
|
+
from .elements import SourceEffort, SourceFlow, Capacitor, Inductor, Resistor, Transformer, Gyrator, OneJunction, ZeroJunction
|
|
23
15
|
|
|
24
16
|
from .core import Port
|
|
25
17
|
|
|
@@ -94,58 +86,52 @@ class BondGraph:
|
|
|
94
86
|
def __handle_bonds(self) -> None:
|
|
95
87
|
"""Handles the bonds in the bond graph by assigning them to the appropriate elements.
|
|
96
88
|
This assignment propagates the bond references to the elements, so that each element knows which bonds it is connected to.
|
|
97
|
-
"""
|
|
98
|
-
|
|
99
|
-
def handle_bond_element(element: Node, bond: Bond):
|
|
100
|
-
"""Internal helper function to handle the assignment of a bond to an element.
|
|
101
|
-
|
|
102
|
-
Parameters
|
|
103
|
-
----------
|
|
104
|
-
element : Node
|
|
105
|
-
Element of the bond to handle. Must be called with both `from_element` and `to_element` of the bond.
|
|
106
|
-
bond : Bond
|
|
107
|
-
The bond to assign to the element.
|
|
108
89
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
ValueError
|
|
112
|
-
If the element is a junction and it is attemped to add a second strong bond.
|
|
113
|
-
"""
|
|
90
|
+
Iterates over each bond and calls _handle_element() for both the from_element and to_element
|
|
91
|
+
"""
|
|
114
92
|
|
|
93
|
+
# Reset element bond references so that repeated calls are safe
|
|
94
|
+
for el in self.elements:
|
|
95
|
+
if isinstance(el, ElementOnePort):
|
|
96
|
+
el.bond = None
|
|
97
|
+
elif isinstance(el, ElementTwoPort):
|
|
98
|
+
el.bond1 = None
|
|
99
|
+
el.bond2 = None
|
|
100
|
+
elif isinstance(el, Junction):
|
|
101
|
+
el.bonds = []
|
|
102
|
+
el.strong_bond = None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _handle_element(element: Node, bond: Bond) -> None:
|
|
106
|
+
"""Assign bond to element according to the element's type."""
|
|
115
107
|
if isinstance(element, ElementOnePort):
|
|
116
108
|
element.bond = bond
|
|
117
109
|
|
|
118
110
|
elif isinstance(element, ElementTwoPort):
|
|
119
|
-
|
|
111
|
+
# bond1 = bond INTO the element, bond2 = bond FROM the element
|
|
112
|
+
if bond.to_element is element:
|
|
120
113
|
element.bond1 = bond
|
|
121
|
-
elif bond.from_element
|
|
114
|
+
elif bond.from_element is element:
|
|
122
115
|
element.bond2 = bond
|
|
116
|
+
else:
|
|
117
|
+
raise ValueError(f"Bond {bond} is not connected to ElementTwoPort {element}. You have called this function incorrectly.")
|
|
123
118
|
|
|
124
119
|
elif isinstance(element, Junction):
|
|
125
|
-
element.bonds
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (bond.from_element == element and bond.causality == Causality.FLOW_OUT) or (bond.to_element == element and bond.causality == Causality.EFFORT_OUT):
|
|
138
|
-
# bond is strong bond for zero junction
|
|
139
|
-
if element.strong_bond is None:
|
|
140
|
-
element.strong_bond = bond
|
|
141
|
-
print(f"Assigned strong bond {bond} to ZeroJunction {element}.")
|
|
142
|
-
else:
|
|
143
|
-
raise ValueError(f"ZeroJunction {element} already has a strong bond: {element.strong_bond}. Cannot assign {bond}.")
|
|
144
|
-
|
|
145
|
-
# Call helper function for both elements of each bond
|
|
120
|
+
if bond not in element.bonds:
|
|
121
|
+
element.bonds.append(bond)
|
|
122
|
+
|
|
123
|
+
if self._is_strong_for(bond, element):
|
|
124
|
+
if element.strong_bond is None:
|
|
125
|
+
element.strong_bond = bond
|
|
126
|
+
else:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"{type(element).__name__} {element} already has a strong bond: "
|
|
129
|
+
f"{element.strong_bond}. Cannot assign {bond}."
|
|
130
|
+
)
|
|
131
|
+
|
|
146
132
|
for bond in self.bonds:
|
|
147
|
-
|
|
148
|
-
|
|
133
|
+
_handle_element(bond.from_element, bond)
|
|
134
|
+
_handle_element(bond.to_element, bond)
|
|
149
135
|
|
|
150
136
|
def __handle_equations(self) -> None:
|
|
151
137
|
"""Accumulates the equations from all elements and junctions in the bond graph.
|
|
@@ -173,7 +159,7 @@ class BondGraph:
|
|
|
173
159
|
bond1 = element.bond1
|
|
174
160
|
bond2 = element.bond2
|
|
175
161
|
if bond1 is None or bond2 is None:
|
|
176
|
-
raise ValueError(f"Element {element}
|
|
162
|
+
raise ValueError(f"Element {element} is not fully connected with bonds.")
|
|
177
163
|
|
|
178
164
|
# Add equations from the element to the bond graph
|
|
179
165
|
self.equations.extend(element.equations)
|
|
@@ -192,6 +178,10 @@ class BondGraph:
|
|
|
192
178
|
The keys include the time derivatives of the state variables and the efforts and flows of all bonds.
|
|
193
179
|
"""
|
|
194
180
|
|
|
181
|
+
# Auto-assign causality if any bonds lack it
|
|
182
|
+
if any(b.causality is None for b in self.bonds):
|
|
183
|
+
self.assign_causality()
|
|
184
|
+
|
|
195
185
|
self.__handle_bonds()
|
|
196
186
|
self.__handle_equations()
|
|
197
187
|
state_derivatives = [sp.Derivative(var, "t") for var in self.state_vars]
|
|
@@ -303,7 +293,7 @@ class BondGraph:
|
|
|
303
293
|
self,
|
|
304
294
|
node_a: Node,
|
|
305
295
|
node_b: Node,
|
|
306
|
-
causality: Causality =
|
|
296
|
+
causality: Causality | None = None,
|
|
307
297
|
) -> Bond:
|
|
308
298
|
"""Connect two nodes by adding a bond between them.
|
|
309
299
|
|
|
@@ -331,9 +321,11 @@ class BondGraph:
|
|
|
331
321
|
Source — becomes ``from_element`` of the new bond.
|
|
332
322
|
node_b : Node
|
|
333
323
|
Destination — becomes ``to_element`` of the new bond.
|
|
334
|
-
causality : Causality, optional
|
|
335
|
-
Causality of the connecting bond. Defaults to
|
|
336
|
-
|
|
324
|
+
causality : Causality | None, optional
|
|
325
|
+
Causality of the connecting bond. Defaults to ``None``,
|
|
326
|
+
meaning causality will be assigned later by
|
|
327
|
+
:meth:`assign_causality` (SCAP). Pass a ``Causality``
|
|
328
|
+
value explicitly to override automatic assignment.
|
|
337
329
|
|
|
338
330
|
Returns
|
|
339
331
|
-------
|
|
@@ -362,6 +354,241 @@ class BondGraph:
|
|
|
362
354
|
self.add_bond(bond)
|
|
363
355
|
return bond
|
|
364
356
|
|
|
357
|
+
# --- shared helpers (used by both assign_causality and __handle_bonds) ---
|
|
358
|
+
def _elements_of_type(self, *types: type) -> list[Node]:
|
|
359
|
+
"""Yield all elements that are instances of types."""
|
|
360
|
+
return [el for el in self.elements if isinstance(el, types)]
|
|
361
|
+
|
|
362
|
+
@staticmethod
|
|
363
|
+
def _is_strong_for(bond: Bond, junc: Junction) -> bool:
|
|
364
|
+
"""Return True if bond is the strong bond at junc."""
|
|
365
|
+
if bond.causality is None:
|
|
366
|
+
return False
|
|
367
|
+
|
|
368
|
+
if isinstance(junc, OneJunction):
|
|
369
|
+
return (
|
|
370
|
+
(bond.from_element is junc and bond.causality == Causality.EFFORT_OUT)
|
|
371
|
+
or (bond.to_element is junc and bond.causality == Causality.FLOW_OUT)
|
|
372
|
+
)
|
|
373
|
+
elif isinstance(junc, ZeroJunction):
|
|
374
|
+
return (
|
|
375
|
+
(bond.from_element is junc and bond.causality == Causality.FLOW_OUT)
|
|
376
|
+
or (bond.to_element is junc and bond.causality == Causality.EFFORT_OUT)
|
|
377
|
+
)
|
|
378
|
+
return False
|
|
379
|
+
|
|
380
|
+
@staticmethod
|
|
381
|
+
def _make_strong(bond: Bond, junc: Junction) -> Causality:
|
|
382
|
+
"""Return the causality that makes bond the strong bond at junc."""
|
|
383
|
+
junc_is_from = bond.from_element is junc
|
|
384
|
+
|
|
385
|
+
if isinstance(junc, OneJunction):
|
|
386
|
+
return Causality.EFFORT_OUT if junc_is_from else Causality.FLOW_OUT
|
|
387
|
+
else: # ZeroJunction
|
|
388
|
+
return Causality.FLOW_OUT if junc_is_from else Causality.EFFORT_OUT
|
|
389
|
+
|
|
390
|
+
@staticmethod
|
|
391
|
+
def _make_weak(bond: Bond, junc: Junction) -> Causality:
|
|
392
|
+
"""Return the causality that makes bond a weak bond at junc.
|
|
393
|
+
|
|
394
|
+
This is the inverse of :meth:`_make_strong`.
|
|
395
|
+
"""
|
|
396
|
+
# inverse mapping of strong to weak causality
|
|
397
|
+
strong_to_weak = {
|
|
398
|
+
Causality.EFFORT_OUT: Causality.FLOW_OUT,
|
|
399
|
+
Causality.FLOW_OUT: Causality.EFFORT_OUT,
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
strong = BondGraph._make_strong(bond, junc)
|
|
403
|
+
|
|
404
|
+
return strong_to_weak[strong]
|
|
405
|
+
|
|
406
|
+
def assign_causality(self) -> None:
|
|
407
|
+
"""Run the Sequential Causality Assignment Procedure (SCAP).
|
|
408
|
+
|
|
409
|
+
Assigns causality to every bond that currently has
|
|
410
|
+
``causality = None``. Bonds whose causality was set explicitly
|
|
411
|
+
(manually or via ``add_bond`` / ``connect``) are respected as
|
|
412
|
+
fixed constraints.
|
|
413
|
+
|
|
414
|
+
The algorithm proceeds in priority order:
|
|
415
|
+
|
|
416
|
+
1. Sources: fixed causality (Se outputs effort, Sf outputs flow).
|
|
417
|
+
2. Storage elements: preferred integral causality (C outputs effort, I outputs flow).
|
|
418
|
+
3. Remaining bonds: assigned by propagation through junctions and two-port elements.
|
|
419
|
+
|
|
420
|
+
After each assignment the consequences are propagated through connected junctions
|
|
421
|
+
(exactly one strong bond each) and two-port elements
|
|
422
|
+
(transformer: same causality on both bonds; gyrator: opposite causality).
|
|
423
|
+
|
|
424
|
+
Raises
|
|
425
|
+
------
|
|
426
|
+
DerivativeCausalityError
|
|
427
|
+
If a storage element cannot receive its preferred integral causality.
|
|
428
|
+
This indicates the system contains algebraic constraints (DAE instead of ODE) which are not supported.
|
|
429
|
+
CausalityError
|
|
430
|
+
If causality cannot be fully resolved (should not happen for a well-formed bond graph).
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
# Populate bond references on elements so we can use access them when assigning causality.
|
|
434
|
+
self.__handle_bonds()
|
|
435
|
+
|
|
436
|
+
# Track which bonds still need assignment
|
|
437
|
+
unassigned: set[Bond] = {b for b in self.bonds if b.causality is None}
|
|
438
|
+
|
|
439
|
+
if not unassigned:
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
def _assign(bond: Bond, causality: Causality) -> None:
|
|
443
|
+
"""Assign causality to a bond and remove it from the unassigned set."""
|
|
444
|
+
bond.causality = causality
|
|
445
|
+
unassigned.discard(bond)
|
|
446
|
+
|
|
447
|
+
def _propagate() -> None:
|
|
448
|
+
"""Propagate causality through junctions and two-port elements
|
|
449
|
+
until no further assignments can be made."""
|
|
450
|
+
if not unassigned:
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
changed = True
|
|
454
|
+
while changed:
|
|
455
|
+
changed = False
|
|
456
|
+
|
|
457
|
+
# Junctions: if any strong bond exists, all unassigned bonds must be weak
|
|
458
|
+
for junc in self._elements_of_type(Junction):
|
|
459
|
+
assigned_bonds = [b for b in junc.bonds if b.causality is not None]
|
|
460
|
+
unassigned_here = [b for b in junc.bonds if b.causality is None]
|
|
461
|
+
if not unassigned_here:
|
|
462
|
+
continue
|
|
463
|
+
|
|
464
|
+
has_strong = any(self._is_strong_for(b, junc) for b in assigned_bonds)
|
|
465
|
+
|
|
466
|
+
if has_strong:
|
|
467
|
+
for b in unassigned_here:
|
|
468
|
+
_assign(b, self._make_weak(b, junc))
|
|
469
|
+
changed = True
|
|
470
|
+
elif len(unassigned_here) == 1:
|
|
471
|
+
_assign(unassigned_here[0], self._make_strong(unassigned_here[0], junc))
|
|
472
|
+
changed = True
|
|
473
|
+
|
|
474
|
+
# Two-port elements: if one bond has causality, the other must be assigned accordingly
|
|
475
|
+
for tp_elem in self._elements_of_type(ElementTwoPort):
|
|
476
|
+
b1, b2 = tp_elem.bond1, tp_elem.bond2
|
|
477
|
+
if b1 is None or b2 is None:
|
|
478
|
+
continue
|
|
479
|
+
|
|
480
|
+
src, dst = None, None
|
|
481
|
+
if b1.causality is not None and b2.causality is None:
|
|
482
|
+
src, dst = b1, b2
|
|
483
|
+
elif b2.causality is not None and b1.causality is None:
|
|
484
|
+
src, dst = b2, b1
|
|
485
|
+
else:
|
|
486
|
+
continue
|
|
487
|
+
|
|
488
|
+
if isinstance(tp_elem, Transformer):
|
|
489
|
+
_assign(dst, src.causality)
|
|
490
|
+
else: # Gyrator — opposite causality
|
|
491
|
+
opp = Causality.FLOW_OUT if src.causality == Causality.EFFORT_OUT else Causality.EFFORT_OUT
|
|
492
|
+
_assign(dst, opp)
|
|
493
|
+
changed = True
|
|
494
|
+
|
|
495
|
+
if not unassigned:
|
|
496
|
+
return
|
|
497
|
+
|
|
498
|
+
def _desired_causality(element: ElementOnePort, bond: Bond) -> Causality:
|
|
499
|
+
"""Return the causality the element wants (from from_element perspective)."""
|
|
500
|
+
el_is_from = bond.from_element is element
|
|
501
|
+
if isinstance(element, (SourceEffort, Capacitor)):
|
|
502
|
+
return Causality.EFFORT_OUT if el_is_from else Causality.FLOW_OUT
|
|
503
|
+
elif isinstance(element, (SourceFlow, Inductor)):
|
|
504
|
+
return Causality.FLOW_OUT if el_is_from else Causality.EFFORT_OUT
|
|
505
|
+
return None
|
|
506
|
+
|
|
507
|
+
def _would_conflict_with_junction(bond: Bond, causality: Causality) -> bool:
|
|
508
|
+
"""Check if assigning *causality* to *bond* would create a
|
|
509
|
+
second strong bond at any connected junction."""
|
|
510
|
+
for el in bond.elements:
|
|
511
|
+
if not isinstance(el, Junction):
|
|
512
|
+
continue
|
|
513
|
+
# Temporarily check what this would mean
|
|
514
|
+
old = bond.causality
|
|
515
|
+
bond.causality = causality
|
|
516
|
+
is_strong = self._is_strong_for(bond, el)
|
|
517
|
+
bond.causality = old
|
|
518
|
+
if is_strong:
|
|
519
|
+
other_bonds = [b for b in el.bonds if b is not bond and b.causality is not None]
|
|
520
|
+
if any(self._is_strong_for(ob, el) for ob in other_bonds):
|
|
521
|
+
return True
|
|
522
|
+
return False
|
|
523
|
+
|
|
524
|
+
# --- Phase 1: Sources (fixed causality) -----------------------------
|
|
525
|
+
for elem in self._elements_of_type(SourceEffort, SourceFlow):
|
|
526
|
+
if elem.bond.causality is None:
|
|
527
|
+
_assign(elem.bond, _desired_causality(elem, elem.bond))
|
|
528
|
+
_propagate()
|
|
529
|
+
|
|
530
|
+
# --- Phase 2: Storage elements (integral causality) -----------------
|
|
531
|
+
for elem in self._elements_of_type(Capacitor, Inductor):
|
|
532
|
+
if elem.bond.causality is not None:
|
|
533
|
+
continue
|
|
534
|
+
desired = _desired_causality(elem, elem.bond)
|
|
535
|
+
if not _would_conflict_with_junction(elem.bond, desired):
|
|
536
|
+
_assign(elem.bond, desired)
|
|
537
|
+
else:
|
|
538
|
+
raise DerivativeCausalityError(
|
|
539
|
+
f"Storage element '{elem.name}' ({type(elem).__name__}) "
|
|
540
|
+
f"cannot receive its preferred integral causality "
|
|
541
|
+
f"because a connected junction already has a strong bond. "
|
|
542
|
+
f"This forces derivative causality, turning the system "
|
|
543
|
+
f"into a DAE (differential-algebraic equation) which is "
|
|
544
|
+
f"not supported.\n"
|
|
545
|
+
f"Suggestion: insert a resistive element (R) between "
|
|
546
|
+
f"conflicting storage elements, or restructure the model."
|
|
547
|
+
)
|
|
548
|
+
_propagate()
|
|
549
|
+
|
|
550
|
+
# --- Phase 3: Resistors and remaining bonds -------------------------
|
|
551
|
+
for elem in self._elements_of_type(Resistor):
|
|
552
|
+
if elem.bond.causality is not None:
|
|
553
|
+
continue
|
|
554
|
+
bond = elem.bond
|
|
555
|
+
# Determine from the connected junction what this bond needs
|
|
556
|
+
other_el = bond.to_element if bond.from_element is elem else bond.from_element
|
|
557
|
+
if isinstance(other_el, Junction):
|
|
558
|
+
assigned_bonds = [b for b in other_el.bonds if b.causality is not None]
|
|
559
|
+
has_strong = any(self._is_strong_for(b, other_el) for b in assigned_bonds)
|
|
560
|
+
if has_strong:
|
|
561
|
+
_assign(bond, self._make_weak(bond, other_el))
|
|
562
|
+
elif len([b for b in other_el.bonds if b.causality is None]) == 1:
|
|
563
|
+
# if this is the only unassigned bond at the junction, it must be strong
|
|
564
|
+
_assign(bond, self._make_strong(bond, other_el))
|
|
565
|
+
_propagate()
|
|
566
|
+
|
|
567
|
+
# --- Validate: two-port causality consistency -----------------------
|
|
568
|
+
for tp_elem in self._elements_of_type(ElementTwoPort):
|
|
569
|
+
b1, b2 = tp_elem.bond1, tp_elem.bond2
|
|
570
|
+
if b1 is None or b2 is None:
|
|
571
|
+
continue
|
|
572
|
+
if isinstance(tp_elem, Transformer) and b1.causality != b2.causality:
|
|
573
|
+
raise CausalityError(
|
|
574
|
+
f"Transformer '{tp_elem.name}' requires both bonds to have the "
|
|
575
|
+
f"same causality, but bond1={b1.causality} and bond2={b2.causality}."
|
|
576
|
+
)
|
|
577
|
+
if isinstance(tp_elem, Gyrator) and b1.causality == b2.causality:
|
|
578
|
+
raise CausalityError(
|
|
579
|
+
f"Gyrator '{tp_elem.name}' requires both bonds to have different "
|
|
580
|
+
f"causality, but both are {b1.causality}."
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
# --- Validate: unassigned bonds -------------------------------------
|
|
584
|
+
if unassigned:
|
|
585
|
+
descriptions = [f" Bond({b.from_element.name} -> {b.to_element.name})" for b in unassigned]
|
|
586
|
+
raise CausalityError(
|
|
587
|
+
f"SCAP could not assign causality to {len(unassigned)} bond(s):\n"
|
|
588
|
+
+ "\n".join(descriptions)
|
|
589
|
+
+ "\nCheck that the bond graph is fully connected and well-formed."
|
|
590
|
+
)
|
|
591
|
+
|
|
365
592
|
def plot(self, layout: Callable[[nx.Graph, ...], dict] = nx.spectral_layout, **kwargs) -> tuple[plt.Figure, plt.Axes]:
|
|
366
593
|
"""Plots the bond graph as a `networkx` graph.
|
|
367
594
|
|
|
@@ -432,7 +659,7 @@ class BondGraph:
|
|
|
432
659
|
for edge_idx, (u, v, data) in enumerate(G.edges(data=True)):
|
|
433
660
|
x1, y1 = pos[u]
|
|
434
661
|
x2, y2 = pos[v]
|
|
435
|
-
# Vary the parameter t
|
|
662
|
+
# Vary the parameter t \in [0.35, 0.65] per edge
|
|
436
663
|
t = 0.35 + 0.3 * ((edge_idx * 7 + 3) % 11) / 10.0
|
|
437
664
|
lx = x1 + t * (x2 - x1)
|
|
438
665
|
ly = y1 + t * (y2 - y1)
|
|
@@ -469,7 +696,7 @@ class BondGraph:
|
|
|
469
696
|
If the 'at' parameter is not "head" or "tail".
|
|
470
697
|
"""
|
|
471
698
|
|
|
472
|
-
#
|
|
699
|
+
# Convert node size (points^2, i.e. area) to radius in pixels
|
|
473
700
|
radius_points = np.sqrt(node_size / np.pi)
|
|
474
701
|
radius_pixels = radius_points * ax.figure.dpi / 72.0 # 1 point = 1/72 inch
|
|
475
702
|
offset = radius_pixels + padding # + padding (in px) so stroke sits outside node
|
|
@@ -515,6 +742,8 @@ class BondGraph:
|
|
|
515
742
|
_stroke_specs.append((pos[u], pos[v], "head", -2))
|
|
516
743
|
elif causality is Causality.FLOW_OUT:
|
|
517
744
|
_stroke_specs.append((pos[u], pos[v], "tail", -2))
|
|
745
|
+
elif causality is None:
|
|
746
|
+
pass # No causality assigned yet — skip causal stroke
|
|
518
747
|
else:
|
|
519
748
|
raise ValueError(f"Edge {u}->{v} has no valid causality: {causality} --> this should never happen!")
|
|
520
749
|
|
|
@@ -17,6 +17,20 @@ class Causality(Enum):
|
|
|
17
17
|
FLOW_OUT = "flow_out"
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
class CausalityError(Exception):
|
|
21
|
+
"""Raised when automatic causality assignment (SCAP) fails."""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DerivativeCausalityError(CausalityError):
|
|
26
|
+
"""Raised when a storage element would require derivative causality.
|
|
27
|
+
|
|
28
|
+
Derivative causality turns the system into a DAE (differential-algebraic equation) which cannot directly be transformed into state-space representation.
|
|
29
|
+
Consider adding a small resistance (or the like) between conflicting storage elements or restructuring the model.
|
|
30
|
+
"""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
20
34
|
class StatefulElement(ABC):
|
|
21
35
|
"""Base class for all stateful elements (capacitor, inductor) in the bond graph.
|
|
22
36
|
Requires implementation of a `state_var` property that returns the symbolic state variable associated with the element.
|
|
@@ -51,7 +65,15 @@ class Bond:
|
|
|
51
65
|
|
|
52
66
|
_counter = 0 # Global fallback counter; prefer BondGraph-scoped numbering
|
|
53
67
|
|
|
54
|
-
def __init__(
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
from_element: Node,
|
|
71
|
+
to_element: Node,
|
|
72
|
+
causality: str | Causality | None = None,
|
|
73
|
+
num: int | None = None,
|
|
74
|
+
instance_name: str = "",
|
|
75
|
+
is_prefix: bool = True,
|
|
76
|
+
):
|
|
55
77
|
"""Create a bond between two elements with specified causality.
|
|
56
78
|
The positive direction of this power bond is from `from_element` to `to_element`.
|
|
57
79
|
Efforts and flows are represented by symbolic `sympy.Symbol`s that are strictly real-valued.
|
|
@@ -62,7 +84,7 @@ class Bond:
|
|
|
62
84
|
The element where the bond originates.
|
|
63
85
|
to_element : Node
|
|
64
86
|
The element where the bond terminates.
|
|
65
|
-
causality : str | Causality
|
|
87
|
+
causality : str | Causality | None, optional
|
|
66
88
|
The causality of the bond, either `effort_out` or `flow_out`.
|
|
67
89
|
If a string is provided, it is converted to the corresponding `Causality` enum.
|
|
68
90
|
This definition is always from the perspective of the `from_element`.
|
|
@@ -70,6 +92,7 @@ class Bond:
|
|
|
70
92
|
`OneJunction` imposes effort on the `Inductor`, meaning it has an equivalent `effort_in` causality.
|
|
71
93
|
Likewise a `Bond(OneJunction(...), Capacitor(...), "flow_out")` means that the `OneJunction` imposes
|
|
72
94
|
flow on the `Capacitor`, meaning it has an equivalent `flow_in`/`effort_out` causality.
|
|
95
|
+
If None, the causality must be assigned later by calling (manually or automatically) the bond graph's causality assignment algorithm.
|
|
73
96
|
num : int | None, optional
|
|
74
97
|
Explicit bond number. If None, the global fallback counter is used.
|
|
75
98
|
When bonds are added to a BondGraph, the graph manages numbering.
|
|
@@ -89,10 +112,11 @@ class Bond:
|
|
|
89
112
|
self.to_element = to_element
|
|
90
113
|
|
|
91
114
|
if isinstance(causality, str):
|
|
92
|
-
causality = Causality(causality.lower())
|
|
115
|
+
causality = Causality(causality.lower())
|
|
116
|
+
# Convert string to Causality enum, case insensitive
|
|
93
117
|
# --> automatically raises ValueError if string is not valid
|
|
94
118
|
|
|
95
|
-
self.causality: Causality = causality
|
|
119
|
+
self.causality: Causality | None = causality
|
|
96
120
|
|
|
97
121
|
if num is None:
|
|
98
122
|
self.num = Bond._counter
|
|
@@ -114,7 +138,12 @@ class Bond:
|
|
|
114
138
|
"""tuple[Node, Node]: The two elements connected by the bond. First element is `from_element`, second is `to_element`."""
|
|
115
139
|
return (self.from_element, self.to_element)
|
|
116
140
|
|
|
117
|
-
def rename_symbols(
|
|
141
|
+
def rename_symbols(
|
|
142
|
+
self,
|
|
143
|
+
new_num: int | None = None,
|
|
144
|
+
new_instance_name: str | None = None,
|
|
145
|
+
is_prefix: bool = True,
|
|
146
|
+
) -> dict[sp.Symbol, sp.Symbol]:
|
|
118
147
|
"""Rename the effort/flow symbols of this bond and return the substitution map.
|
|
119
148
|
This is used during sub-model merging to avoid symbol collisions.
|
|
120
149
|
|
|
@@ -140,13 +169,12 @@ class Bond:
|
|
|
140
169
|
if new_instance_name is not None:
|
|
141
170
|
self.instance_name = new_instance_name
|
|
142
171
|
|
|
143
|
-
|
|
144
172
|
if is_prefix:
|
|
145
173
|
padded_name = "_" + self.instance_name + "_" if self.instance_name != "" else "_"
|
|
146
174
|
self.effort = sp.Symbol(f"e{padded_name}{self.num}", real=True)
|
|
147
175
|
self.flow = sp.Symbol(f"f{padded_name}{self.num}", real=True)
|
|
148
176
|
else:
|
|
149
|
-
padded_name = self.instance_name if self.instance_name != "" else ""
|
|
177
|
+
padded_name = self.instance_name if self.instance_name != "" else "" # conditional can be skipped
|
|
150
178
|
self.effort = sp.Symbol(f"e_{self.num}{padded_name}", real=True)
|
|
151
179
|
self.flow = sp.Symbol(f"f_{self.num}{padded_name}", real=True)
|
|
152
180
|
|
|
@@ -198,7 +226,9 @@ class ElementOnePort(Node, ABC):
|
|
|
198
226
|
"""
|
|
199
227
|
|
|
200
228
|
super().__init__(name)
|
|
201
|
-
self.value = sp.Symbol(
|
|
229
|
+
self.value = sp.Symbol(
|
|
230
|
+
value, real=True, positive=True
|
|
231
|
+
) # Ensure value is a positive real number
|
|
202
232
|
self.bond: Bond = None # bond that connects this element to a bond graph
|
|
203
233
|
|
|
204
234
|
@property
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyBondGraph
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Modelling tool for linear bond graph systems in Python
|
|
5
5
|
Author-email: Matthias Panny <matthias.panny@mci.edu>
|
|
6
6
|
License-Expression: CC-BY-NC-SA-4.0
|
|
@@ -36,9 +36,15 @@ Bond graphs provide a **domain-independent modeling framework** for physical sys
|
|
|
36
36
|
# Features
|
|
37
37
|
|
|
38
38
|
* Programmatic construction of **bond graph models**
|
|
39
|
+
* **Automatic causality assignment** via SCAP (Sequential Causality Assignment Procedure), with optional manual override or mixed mode
|
|
39
40
|
* Automatic **symbolic equation derivation** using SymPy
|
|
40
|
-
* Conversion of models to **state-space systems**
|
|
41
|
-
*
|
|
41
|
+
* Conversion of models to **linear state-space systems** ($\dot{x} = Ax + Bu$, $y = Cx + Du$)
|
|
42
|
+
* **Composable sub-models** via `SubBondGraph` with deep-copy namespace isolation
|
|
43
|
+
* **Two-port elements**: Transformer and Gyrator with automatic causality propagation
|
|
44
|
+
* **Sensor elements**: `IntegratedEffortSensor` and `IntegratedFlowSensor` for measuring integrated generalized variables (e.g. position from velocity)
|
|
45
|
+
* **Domain-neutral aliases**: `Compliance` = `Capacitor`, `Inertance` = `Inductor`, `Resistance` = `Resistor`
|
|
46
|
+
* Example models for electrical and electromechanical systems
|
|
47
|
+
* Integration with **python-control** for numerical simulation (step response, Bode plots, etc.)
|
|
42
48
|
|
|
43
49
|
---
|
|
44
50
|
|
|
@@ -81,12 +87,12 @@ Optional dependencies are used for experimental visualization tools.
|
|
|
81
87
|
---
|
|
82
88
|
|
|
83
89
|
# Basic Usage
|
|
84
|
-
A bond graph model is constructed by creating elements and connecting them via bonds.
|
|
90
|
+
A bond graph model is constructed by creating elements and connecting them via the `connect()` convenience method, which creates bonds and adds them to the graph in one step.
|
|
85
91
|
|
|
86
|
-
|
|
92
|
+
## RC-Filter with automatic causality (SCAP)
|
|
87
93
|
|
|
88
94
|
```python
|
|
89
|
-
from pyBondGraph import BondGraph, SourceEffort, Resistor, Capacitor, OneJunction
|
|
95
|
+
from pyBondGraph import BondGraph, SourceEffort, Resistor, Capacitor, OneJunction
|
|
90
96
|
|
|
91
97
|
bg = BondGraph()
|
|
92
98
|
|
|
@@ -96,19 +102,20 @@ resistor = Resistor("R", "R")
|
|
|
96
102
|
capacitor = Capacitor("C", "C")
|
|
97
103
|
series_junction = OneJunction("J1")
|
|
98
104
|
|
|
99
|
-
# connect elements
|
|
100
|
-
|
|
101
|
-
bg.
|
|
102
|
-
bg.
|
|
103
|
-
bg.add_bond(Bond(series_junction, capacitor, Causality.FLOW_OUT))
|
|
105
|
+
# connect elements --> causality is assigned automatically by SCAP
|
|
106
|
+
bg.connect(voltage_source, series_junction)
|
|
107
|
+
bg.connect(series_junction, resistor)
|
|
108
|
+
bg.connect(series_junction, capacitor)
|
|
104
109
|
|
|
105
110
|
# plot the resulting BondGraph
|
|
106
111
|
bg.plot()
|
|
107
112
|
|
|
108
113
|
# derive system equations in linear state space form
|
|
109
|
-
A, B, C, D, x, n_states, n_inputs, n_outputs =
|
|
114
|
+
A, B, C, D, x, n_states, n_inputs, n_outputs = bg.get_state_space()
|
|
110
115
|
```
|
|
111
116
|
|
|
117
|
+
Causality can also be assigned **manually** by passing a `Causality` value to `connect()`, or in **mixed mode** where some bonds are fixed and SCAP resolves the rest.
|
|
118
|
+
|
|
112
119
|
The library automatically derives the **symbolic system equations** describing the dynamics of the model.
|
|
113
120
|
|
|
114
121
|
---
|
|
@@ -126,6 +133,12 @@ Bond graphs represent **power exchange between system components**, where power
|
|
|
126
133
|
| Se | Effort source |
|
|
127
134
|
| Sf | Flow source |
|
|
128
135
|
|
|
136
|
+
## Two-Port Elements
|
|
137
|
+
| Element | Meaning |
|
|
138
|
+
|-------------|------------------------------------------------------------|
|
|
139
|
+
| TF | Transformer — same causality on both bonds |
|
|
140
|
+
| GY | Gyrator — opposite causality on both bonds |
|
|
141
|
+
|
|
129
142
|
## Junctions
|
|
130
143
|
| Junction | Meaning |
|
|
131
144
|
|----------|---------------|
|
|
@@ -139,11 +152,9 @@ Bond graphs represent **power exchange between system components**, where power
|
|
|
139
152
|
| IntegratedFlowSensor | Measures integral of the flow at its bond |
|
|
140
153
|
|
|
141
154
|
In mechanical bond graph models:
|
|
142
|
-
* **flow** corresponds to **velocity**
|
|
155
|
+
* **flow** corresponds to **velocity**, i.e. an *IntegratedFlowSensor* can be used to compute **position**.
|
|
143
156
|
* **effort** corresponds to **force**
|
|
144
157
|
|
|
145
|
-
An **integrated flow sensor** can therefore be used to compute **position**:
|
|
146
|
-
|
|
147
158
|
---
|
|
148
159
|
|
|
149
160
|
# Example Systems
|
|
@@ -153,7 +164,7 @@ The repository contains example models illustrating typical applications of bond
|
|
|
153
164
|
Demonstrates modeling of an electrical circuit using bond graph elements.
|
|
154
165
|
|
|
155
166
|
### DC Motor
|
|
156
|
-
A multi-domain electromechanical system coupling electrical and mechanical dynamics.
|
|
167
|
+
A multi-domain electromechanical system coupling electrical and mechanical dynamics. Also demonstrates integration with the `python-control` package for numerical simulation (step response).
|
|
157
168
|
|
|
158
169
|
### Transformer
|
|
159
170
|
Example of energy transformation between two ports.
|
|
@@ -163,6 +174,18 @@ Classical mass-spring-damper system with two degrees of freedom.
|
|
|
163
174
|
|
|
164
175
|
---
|
|
165
176
|
|
|
177
|
+
# Causality Assignment
|
|
178
|
+
|
|
179
|
+
pyBondGraph supports three modes for assigning causality:
|
|
180
|
+
|
|
181
|
+
1. **Automatic (SCAP)** — omit causality in `connect()` calls; `assign_causality()` is called automatically when solving. The Sequential Causality Assignment Procedure assigns causality in priority order: sources, storage elements (integral causality), resistors, then propagation through junctions and two-port elements.
|
|
182
|
+
2. **Manual** — pass `Causality.EFFORT_OUT` or `Causality.FLOW_OUT` explicitly to each `connect()` call.
|
|
183
|
+
3. **Mixed** — fix causality on some bonds, let SCAP resolve the rest.
|
|
184
|
+
|
|
185
|
+
If a storage element cannot receive integral causality (which would imply a DAE rather than an ODE), a `DerivativeCausalityError` is raised with a clear diagnostic message.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
166
189
|
# Typical Applications
|
|
167
190
|
Bond graph modeling is particularly useful for:
|
|
168
191
|
|
|
@@ -171,3 +194,11 @@ Bond graph modeling is particularly useful for:
|
|
|
171
194
|
* multi-domain energy systems
|
|
172
195
|
* control system modeling
|
|
173
196
|
* teaching system dynamics
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
# Planned Features
|
|
201
|
+
|
|
202
|
+
* **FMU Export**: export bond graph models as Functional Mock-up Units (FMI standard) for interoperability with Simulink, Dymola, OpenModelica, and other FMI-compliant tools
|
|
203
|
+
* **Nonlinear element support**: general nonlinear constitutive laws with Jacobian linearization
|
|
204
|
+
* **Convenience bridge to python-control**: `to_control_ss(params)` method wrapping the existing manual pattern
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|