pyBondGraph 0.3.0__tar.gz → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyBondGraph
3
- Version: 0.3.0
3
+ Version: 0.4.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
@@ -19,6 +19,7 @@ Requires-Dist: networkx>=3.5
19
19
  Requires-Dist: numpy>=2.3.0
20
20
  Requires-Dist: matplotlib>=3.10.3
21
21
  Requires-Dist: control>=0.10.2
22
+ Requires-Dist: pythonfmu3>=0.3.4
22
23
  Provides-Extra: streamlit
23
24
  Requires-Dist: streamlit>=1.47.0; extra == "streamlit"
24
25
  Requires-Dist: streamlit-flow-component>=1.6.1; extra == "streamlit"
@@ -29,6 +30,8 @@ Dynamic: license-file
29
30
 
30
31
  The library allows users to construct bond graph models programmatically, automatically derive the governing equations, and analyze the resulting dynamic systems using tools from control theory.
31
32
 
33
+ `pyBondGraph` can also export FMUs [(Functional Mock-up Units)](https://fmi-standard.org/) based on FMI 3.0 for Model Exchange, enabling interoperability with other simulation tools such as Simulink.
34
+
32
35
  Bond graphs provide a **domain-independent modeling framework** for physical systems. Using a unified representation of power exchange, the same modeling approach can be used for electrical, mechanical, hydraulic, and multi-domain systems.
33
36
 
34
37
  ---
@@ -112,6 +115,8 @@ bg.plot()
112
115
 
113
116
  # derive system equations in linear state space form
114
117
  A, B, C, D, x, n_states, n_inputs, n_outputs = bg.get_state_space()
118
+
119
+ bg.to_fmu("rc_filter")
115
120
  ```
116
121
 
117
122
  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.
@@ -199,6 +204,4 @@ Bond graph modeling is particularly useful for:
199
204
 
200
205
  # Planned Features
201
206
 
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
207
  * **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
@@ -3,6 +3,8 @@
3
3
 
4
4
  The library allows users to construct bond graph models programmatically, automatically derive the governing equations, and analyze the resulting dynamic systems using tools from control theory.
5
5
 
6
+ `pyBondGraph` can also export FMUs [(Functional Mock-up Units)](https://fmi-standard.org/) based on FMI 3.0 for Model Exchange, enabling interoperability with other simulation tools such as Simulink.
7
+
6
8
  Bond graphs provide a **domain-independent modeling framework** for physical systems. Using a unified representation of power exchange, the same modeling approach can be used for electrical, mechanical, hydraulic, and multi-domain systems.
7
9
 
8
10
  ---
@@ -86,6 +88,8 @@ bg.plot()
86
88
 
87
89
  # derive system equations in linear state space form
88
90
  A, B, C, D, x, n_states, n_inputs, n_outputs = bg.get_state_space()
91
+
92
+ bg.to_fmu("rc_filter")
89
93
  ```
90
94
 
91
95
  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.
@@ -173,6 +177,4 @@ Bond graph modeling is particularly useful for:
173
177
 
174
178
  # Planned Features
175
179
 
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
180
+ * **Nonlinear element support**: general nonlinear constitutive laws with Jacobian linearization
@@ -18,6 +18,10 @@ from .subbondgraph import SubBondGraph
18
18
 
19
19
  from .bondgraph import BondGraph
20
20
 
21
+ from .numerics import to_numpy, to_control_ss
22
+
23
+ from .fmu_export import to_fmu
24
+
21
25
  __all__ = [
22
26
  "Bond",
23
27
  "Causality",
@@ -40,4 +44,7 @@ __all__ = [
40
44
  "SubBondGraph",
41
45
  "CausalityError",
42
46
  "DerivativeCausalityError",
47
+ "to_numpy",
48
+ "to_control_ss",
49
+ "to_fmu",
43
50
  ]
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from pathlib import Path
4
+
3
5
  import sympy as sp
4
6
  import networkx as nx
5
7
  import numpy as np
@@ -14,6 +16,8 @@ from .core import Causality, CausalityError, DerivativeCausalityError, Node, Sta
14
16
  from .elements import SourceEffort, SourceFlow, Capacitor, Inductor, Resistor, Transformer, Gyrator, OneJunction, ZeroJunction
15
17
 
16
18
  from .core import Port
19
+ from .numerics import to_numpy, to_control_ss
20
+ from .fmu_export import to_fmu
17
21
 
18
22
  if TYPE_CHECKING:
19
23
  from .subbondgraph import SubBondGraph
@@ -265,6 +269,127 @@ class BondGraph:
265
269
 
266
270
  return A, B, C, D, sp.Matrix(self.state_vars), n_states, n_inputs, n_outputs
267
271
 
272
+ def get_substitution_dict(self, overrides: dict[sp.Symbol, float] | None = None) -> dict[sp.Symbol, float]:
273
+ """Build a substitution dictionary from elements that have a ``numeric_value`` set.
274
+
275
+ Iterates over all :class:`ElementOnePort` and :class:`ElementTwoPort` elements
276
+ in the bond graph and maps each element's symbolic ``value`` to its ``numeric_value``.
277
+ Elements without a ``numeric_value`` (i.e. ``None``) are silently skipped.
278
+
279
+ Parameters
280
+ ----------
281
+ overrides : dict[sp.Symbol, float] | None, optional
282
+ Additional or overriding entries merged into the result.
283
+ This is useful for parameter sweeps or for supplying values
284
+ that are not stored on the elements (e.g. external inputs).
285
+
286
+ Returns
287
+ -------
288
+ dict[sp.Symbol, float]
289
+ Mapping from symbolic parameter to numeric value.
290
+
291
+ Raises
292
+ ------
293
+ ValueError
294
+ If any element with a ``numeric_value`` would overwrite an
295
+ already-collected symbol with a *different* value (duplicate
296
+ symbols with the same numeric value are fine).
297
+ """
298
+ subs: dict[sp.Symbol, float] = {}
299
+
300
+ for elem in self.elements:
301
+
302
+ if isinstance(elem, (ElementOnePort, ElementTwoPort)) and elem.numeric_value is not None:
303
+ if elem.value in subs and subs[elem.value] != elem.numeric_value:
304
+ raise ValueError(
305
+ f"Conflicting numeric values for symbol '{elem.value}': "
306
+ f"{subs[elem.value]} vs {elem.numeric_value} (element '{elem.name}')."
307
+ )
308
+
309
+ subs[elem.value] = elem.numeric_value
310
+
311
+ # update dict with manually provded overrides (if any)
312
+ if overrides:
313
+ subs.update(overrides)
314
+
315
+ return subs
316
+
317
+ def get_numeric_state_space(
318
+ self,
319
+ subs: dict[sp.Symbol, float] | None = None,
320
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
321
+ """Return the numeric (numpy) state-space matrices ``(A, B, C, D)``.
322
+
323
+ This is a convenience wrapper around :meth:`get_state_space` that
324
+ substitutes numeric parameter values and converts the resulting
325
+ symbolic matrices to :class:`numpy.ndarray`.
326
+
327
+ Parameters
328
+ ----------
329
+ subs : dict[sp.Symbol, float] | None, optional
330
+ Explicit substitution dictionary. If ``None``,
331
+ :meth:`get_substitution_dict` is called to collect the values
332
+ stored on the elements. If provided, it is used as-is (no
333
+ merging with element values).
334
+
335
+ Returns
336
+ -------
337
+ tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]
338
+ Numeric matrices ``(A, B, C, D)``.
339
+
340
+ Raises
341
+ ------
342
+ ValueError
343
+ If the symbolic state-space cannot be computed, or if free
344
+ symbols remain after substitution (i.e. some parameters have
345
+ no numeric value).
346
+ """
347
+ A, B, C, D, _x, _ns, _ni, _no = self.get_state_space()
348
+
349
+ if subs is None:
350
+ subs = self.get_substitution_dict()
351
+
352
+ # Check for remaining free symbols before conversion
353
+ free = set()
354
+ for M in (A, B, C, D):
355
+ free |= M.free_symbols
356
+ remaining = free - set(subs.keys())
357
+
358
+ if remaining:
359
+ raise ValueError(
360
+ f"The following symbols have no numeric value: {remaining}. "
361
+ f"Set numeric_value on the corresponding elements or pass them via the subs parameter."
362
+ )
363
+
364
+ return tuple(to_numpy(M, subs) for M in (A, B, C, D))
365
+
366
+ def to_control_ss(self):
367
+ """Generate a control.StateSpace object from the bond graph's numeric state-space matrices.
368
+
369
+ Returns
370
+ -------
371
+ control.StateSpace
372
+ The :class:`control.StateSpace` object representing the bond graph's linear dynamics.
373
+
374
+ Raises
375
+ ------
376
+ ValueError
377
+ Raises a value error through :meth:`get_numeric_state_space` if not all numerical values required are set beforehand in the bondgraph elements
378
+ """
379
+
380
+ matrices = self.get_numeric_state_space()
381
+ return to_control_ss(*matrices)
382
+
383
+
384
+
385
+ def to_fmu(self, model_name: str = "BondGraphExport", dest: str | Path = ".", author_name: str = "MtP", description: str = "A bond graph model.", keep_slave_python_code: bool = False) -> None:
386
+ # if default class name is used, use the bondgraph name if it is set
387
+ if model_name == "BondGraphExport" and self.name:
388
+ model_name = self.name
389
+
390
+ to_fmu(self, dest=dest, class_name=model_name, author_name=author_name, description=description, keep_slave_python_code=keep_slave_python_code)
391
+
392
+
268
393
  def add_subbondgraph(self, sub_bondgraph: SubBondGraph, instance_name: str | None = None, is_prefix: bool = True) -> Port:
269
394
  """Instantiate a SubBondGraph into this bond graph.
270
395
 
@@ -213,7 +213,7 @@ class ElementOnePort(Node, ABC):
213
213
  This can also be just one equation.
214
214
  """
215
215
 
216
- def __init__(self, name: str, value: str):
216
+ def __init__(self, name: str, value: str, numeric_value: float | None = None):
217
217
  """ABC for a one-port element. Acts as base class for all one-port elements like Inductor, Capacitor, Resistor, SourceEffort, SourceFlow.
218
218
  Stores an associated bond and a symbolic value (real and positive) for its defining characteristics e.g. resistance, capacitance, etc.
219
219
 
@@ -223,12 +223,16 @@ class ElementOnePort(Node, ABC):
223
223
  The name of the element. Will be shown on the bond graph plot.
224
224
  value : str
225
225
  The name of the value associated with the element, is internally used for creating a `sympy.Symbol`.
226
+ numeric_value : float | None, optional
227
+ Numeric value for the element parameter. If provided, it is used
228
+ when building substitution dictionaries for numeric evaluation.
226
229
  """
227
230
 
228
231
  super().__init__(name)
229
232
  self.value = sp.Symbol(
230
233
  value, real=True, positive=True
231
234
  ) # Ensure value is a positive real number
235
+ self.numeric_value: float | None = numeric_value
232
236
  self.bond: Bond = None # bond that connects this element to a bond graph
233
237
 
234
238
  @property
@@ -246,7 +250,7 @@ class ElementTwoPort(Node, ABC):
246
250
  Requires implementation of an `equations` property that returns the list of symbolic equations defining the element's behavior.
247
251
  """
248
252
 
249
- def __init__(self, name: str, value: str):
253
+ def __init__(self, name: str, value: str, numeric_value: float | None = None):
250
254
  """ABC for a two-port element. Acts as base class for all two-port elements like Transformer, Gyrator.
251
255
  Stores two associated bonds and a symbolic value (real and positive) for its defining characteristics e.g. conversion factor, ratio, etc.
252
256
 
@@ -256,10 +260,14 @@ class ElementTwoPort(Node, ABC):
256
260
  The name of the element. Will be shown on the bond graph plot.
257
261
  value : str
258
262
  The name of the value associated with the element, is internally used for creating a `sympy.Symbol`.
263
+ numeric_value : float | None, optional
264
+ Numeric value for the element parameter. If provided, it is used
265
+ when building substitution dictionaries for numeric evaluation.
259
266
  """
260
267
 
261
268
  super().__init__(name)
262
269
  self.value = sp.Symbol(value, real=True, positive=True)
270
+ self.numeric_value: float | None = numeric_value
263
271
  self.bond1: Bond = None # ElementOther --(bond1)--> ElementTwoPort
264
272
  self.bond2: Bond = None # ElementTwoPort --(bond2)--> ElementOther
265
273
 
@@ -53,7 +53,7 @@ class Capacitor(ElementOnePort, StatefulElement):
53
53
  A capacitance relates the effort of its port with the integral of its flow by a constant capacitance value.
54
54
  """
55
55
 
56
- def __init__(self, name: str, value: str):
56
+ def __init__(self, name: str, value: str, numeric_value: float = None):
57
57
  """Create a linear compliance/capacitance element in the bond graph.
58
58
 
59
59
  Parameters
@@ -62,8 +62,10 @@ class Capacitor(ElementOnePort, StatefulElement):
62
62
  The name of the element. Forwarded to the `Node` base class.
63
63
  value : str
64
64
  The name of the element value, is internally used for creating a `sympy.Symbol`.
65
+ numeric_value : float, optional
66
+ The numeric value of the capacitance, used for numerical simulations.
65
67
  """
66
- super().__init__(name, value)
68
+ super().__init__(name, value, numeric_value)
67
69
 
68
70
  @property
69
71
  def state_var(self) -> sp.Symbol:
@@ -102,7 +104,7 @@ class Inductor(ElementOnePort, StatefulElement):
102
104
  An inductor relates the flow of its port with the integral of its effort by a constant inductance value.
103
105
  """
104
106
 
105
- def __init__(self, name: str, value: str):
107
+ def __init__(self, name: str, value: str, numeric_value: float = None):
106
108
  """Create a linear inertia/inductance element in the bond graph.
107
109
 
108
110
  Parameters
@@ -111,8 +113,10 @@ class Inductor(ElementOnePort, StatefulElement):
111
113
  The name of the element. Forwarded to the `Node` base class.
112
114
  value : str
113
115
  The name of the element value, is internally used for creating a `sympy.Symbol`.
116
+ numeric_value : float, optional
117
+ The numeric value of the inductance, used for numerical simulations.
114
118
  """
115
- super().__init__(name, value)
119
+ super().__init__(name, value, numeric_value)
116
120
 
117
121
  @property
118
122
  def state_var(self) -> sp.Symbol:
@@ -151,7 +155,7 @@ class Resistor(ElementOnePort):
151
155
  A resistor relates the effort and flow of its port by a constant resistance value.
152
156
  """
153
157
 
154
- def __init__(self, name: str, value: str):
158
+ def __init__(self, name: str, value: str, numeric_value: float = None):
155
159
  """Create a linear resistance element.
156
160
 
157
161
  Parameters
@@ -160,8 +164,10 @@ class Resistor(ElementOnePort):
160
164
  The name of the resistance. Forwarded to the `Node` base class.
161
165
  value : str
162
166
  The name of the resistance value, is internally used for creating a `sympy.Symbol`.
167
+ numeric_value : float, optional
168
+ The numeric value of the resistance, used for numerical simulations.
163
169
  """
164
- super().__init__(name, value)
170
+ super().__init__(name, value, numeric_value)
165
171
 
166
172
  @property
167
173
  def equations(self) -> list[sp.Expr]:
@@ -190,7 +196,7 @@ class Transformer(ElementTwoPort):
190
196
  A transformer relates the efforts of its two ports and the flows of its two ports by a constant ratio.
191
197
  """
192
198
 
193
- def __init__(self, name: str, value: str):
199
+ def __init__(self, name: str, value: str, numeric_value: float = None):
194
200
  """Create a transformer element in the bond graph.
195
201
 
196
202
  Parameters
@@ -199,8 +205,10 @@ class Transformer(ElementTwoPort):
199
205
  The name of the element. Forwarded to the `Node` base class.
200
206
  value : str
201
207
  The name of the element value, is internally used for creating a `sympy.Symbol`.
208
+ numeric_value : float, optional
209
+ The numeric value of the transformer ratio, used for numerical simulations.
202
210
  """
203
- super().__init__(name, value)
211
+ super().__init__(name, value, numeric_value)
204
212
 
205
213
  @property
206
214
  def equations(self) -> list[sp.Expr]:
@@ -242,7 +250,7 @@ class Gyrator(ElementTwoPort):
242
250
  A gyrator relates the effort of one port with the flow of the other (and vice versa) by a constant ratio.
243
251
  """
244
252
 
245
- def __init__(self, name: str, value: str):
253
+ def __init__(self, name: str, value: str, numeric_value: float = None):
246
254
  """Create a gyrator element in the bond graph.
247
255
 
248
256
  Parameters
@@ -251,8 +259,10 @@ class Gyrator(ElementTwoPort):
251
259
  The name of the element. Forwarded to the `Node` base class.
252
260
  value : str
253
261
  The name of the element value, is internally used for creating a `sympy.Symbol`.
262
+ numeric_value : float, optional
263
+ The numeric value of the gyrator ratio, used for numerical simulations.
254
264
  """
255
- super().__init__(name, value)
265
+ super().__init__(name, value, numeric_value)
256
266
 
257
267
  @property
258
268
  def equations(self) -> list[sp.Expr]:
@@ -0,0 +1,57 @@
1
+ import numpy as np
2
+ import sympy as sp
3
+
4
+
5
+ def to_numpy(
6
+ M: sp.Matrix,
7
+ subs: dict[sp.Symbol, float],
8
+ ) -> np.ndarray:
9
+ """Convert a symbolic SymPy matrix to a numeric NumPy array.
10
+
11
+ Parameters
12
+ ----------
13
+ M : sp.Matrix
14
+ Symbolic matrix.
15
+ subs : dict[sp.Symbol, float]
16
+ Substitution dictionary mapping symbols to numeric values.
17
+
18
+ Returns
19
+ -------
20
+ np.ndarray
21
+ Float64 array.
22
+ """
23
+ return np.array(M.subs(subs), dtype=np.float64)
24
+
25
+
26
+ def to_control_ss(A: sp.Matrix | np.ndarray, B: sp.Matrix | np.ndarray, C: sp.Matrix | np.ndarray, D: sp.Matrix | np.ndarray, subs: dict[sp.Symbol, float] | None = None):
27
+ """Convert state-space matrices to a :class:`control.StateSpace` object.
28
+
29
+ Parameters
30
+ ----------
31
+ A, B, C, D : sp.Matrix or np.ndarray
32
+ State-space matrices (symbolic or already numeric).
33
+ subs : dict, optional
34
+ Substitution dictionary. If provided the symbolic matrices are
35
+ substituted and converted to NumPy arrays first.
36
+
37
+ Returns
38
+ -------
39
+ control.StateSpace
40
+
41
+ Raises
42
+ ------
43
+ ImportError
44
+ If the ``control`` package is not installed.
45
+ """
46
+ try:
47
+ import control
48
+ except ImportError:
49
+ raise ImportError(
50
+ "python-control is required for to_control_ss(). "
51
+ "Install it with: pip install control"
52
+ )
53
+
54
+ if subs is not None:
55
+ A, B, C, D = (to_numpy(M, subs) for M in (A, B, C, D))
56
+
57
+ return control.ss(A, B, C, D)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyBondGraph
3
- Version: 0.3.0
3
+ Version: 0.4.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
@@ -19,6 +19,7 @@ Requires-Dist: networkx>=3.5
19
19
  Requires-Dist: numpy>=2.3.0
20
20
  Requires-Dist: matplotlib>=3.10.3
21
21
  Requires-Dist: control>=0.10.2
22
+ Requires-Dist: pythonfmu3>=0.3.4
22
23
  Provides-Extra: streamlit
23
24
  Requires-Dist: streamlit>=1.47.0; extra == "streamlit"
24
25
  Requires-Dist: streamlit-flow-component>=1.6.1; extra == "streamlit"
@@ -29,6 +30,8 @@ Dynamic: license-file
29
30
 
30
31
  The library allows users to construct bond graph models programmatically, automatically derive the governing equations, and analyze the resulting dynamic systems using tools from control theory.
31
32
 
33
+ `pyBondGraph` can also export FMUs [(Functional Mock-up Units)](https://fmi-standard.org/) based on FMI 3.0 for Model Exchange, enabling interoperability with other simulation tools such as Simulink.
34
+
32
35
  Bond graphs provide a **domain-independent modeling framework** for physical systems. Using a unified representation of power exchange, the same modeling approach can be used for electrical, mechanical, hydraulic, and multi-domain systems.
33
36
 
34
37
  ---
@@ -112,6 +115,8 @@ bg.plot()
112
115
 
113
116
  # derive system equations in linear state space form
114
117
  A, B, C, D, x, n_states, n_inputs, n_outputs = bg.get_state_space()
118
+
119
+ bg.to_fmu("rc_filter")
115
120
  ```
116
121
 
117
122
  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.
@@ -199,6 +204,4 @@ Bond graph modeling is particularly useful for:
199
204
 
200
205
  # Planned Features
201
206
 
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
207
  * **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
@@ -6,6 +6,7 @@ pyBondGraph/__init__.py
6
6
  pyBondGraph/bondgraph.py
7
7
  pyBondGraph/core.py
8
8
  pyBondGraph/elements.py
9
+ pyBondGraph/numerics.py
9
10
  pyBondGraph/sensors.py
10
11
  pyBondGraph/subbondgraph.py
11
12
  pyBondGraph.egg-info/PKG-INFO
@@ -3,6 +3,7 @@ networkx>=3.5
3
3
  numpy>=2.3.0
4
4
  matplotlib>=3.10.3
5
5
  control>=0.10.2
6
+ pythonfmu3>=0.3.4
6
7
 
7
8
  [streamlit]
8
9
  streamlit>=1.47.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyBondGraph"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "Modelling tool for linear bond graph systems in Python"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -2,4 +2,5 @@ sympy>=1.14.0
2
2
  networkx>=3.5
3
3
  numpy>=2.3.0
4
4
  matplotlib>=3.10.3
5
- control>=0.10.2
5
+ control>=0.10.2
6
+ pythonfmu3>=0.3.4
File without changes
File without changes