pyBondGraph 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,38 @@
1
+ from .core import Bond, Causality
2
+ from .elements import (
3
+ SourceEffort,
4
+ SourceFlow,
5
+ OneJunction,
6
+ ZeroJunction,
7
+ Capacitor,
8
+ Compliance,
9
+ Inductor,
10
+ Inertance,
11
+ Resistor,
12
+ Resistance,
13
+ Transformer,
14
+ Gyrator,
15
+ )
16
+ from .sensors import IntegratedEffortSensor, IntegratedFlowSensor
17
+
18
+ from .bondgraph import BondGraph
19
+
20
+ __all__ = [
21
+ "Bond",
22
+ "Causality",
23
+ "SourceEffort",
24
+ "SourceFlow",
25
+ "OneJunction",
26
+ "ZeroJunction",
27
+ "Capacitor",
28
+ "Compliance",
29
+ "Inductor",
30
+ "Inertance",
31
+ "Resistor",
32
+ "Resistance",
33
+ "Transformer",
34
+ "Gyrator",
35
+ "BondGraph",
36
+ "IntegratedEffortSensor",
37
+ "IntegratedFlowSensor",
38
+ ]
@@ -0,0 +1,399 @@
1
+ import sympy as sp
2
+ import networkx as nx
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+
6
+ from collections.abc import Callable
7
+
8
+ from .core import (
9
+ Causality,
10
+ Node,
11
+ StatefulElement,
12
+ Bond,
13
+ ElementOnePort,
14
+ ElementTwoPort,
15
+ Junction,
16
+ )
17
+ from .elements import SourceEffort, SourceFlow, OneJunction, ZeroJunction
18
+
19
+ type SolutionType = dict[sp.Expr, sp.Expr]
20
+
21
+
22
+ class BondGraph:
23
+ """Represents a bond graph consisting of elements and the bonds that connect them."""
24
+
25
+ def __init__(self):
26
+ """Initializes a new bond graph without any elements or bonds."""
27
+
28
+ Bond.counter = 0 # Reset bond counter for future bond graphs after solution has been computed --> ToDo: fix this hacky solution
29
+
30
+ self.elements = []
31
+ self.bonds: list[Bond] = []
32
+
33
+ self.state_vars: list[sp.Expr] = []
34
+ self.equations: list[sp.Expr] = []
35
+ self.inputs: list[sp.Symbol] = []
36
+
37
+ self.solution: SolutionType = None
38
+
39
+ def add_bond(self, bond: Bond) -> None:
40
+ """Adds a bond to the bond graph by appending it to `self.bonds`.
41
+ This adds the connected elements to `self.elements` if they are not already present.
42
+ If the added bond connects to a `SourceEffort` or `SourceFlow`, its value is added to `self.inputs`.
43
+ This is needed for generating the state space representation.
44
+
45
+ Parameters
46
+ ----------
47
+ bond : Bond
48
+ The bond to be added to the bond graph.
49
+
50
+ Raises
51
+ ------
52
+ ValueError
53
+ If the bond is already part of the bond graph.
54
+ """
55
+
56
+ if bond in self.bonds:
57
+ raise ValueError(f"Bond {bond} is already part of the bond graph.")
58
+
59
+ self.bonds.append(bond)
60
+
61
+ for element in bond.elements:
62
+ if element in self.elements:
63
+ continue
64
+
65
+ self.elements.append(element)
66
+
67
+ if isinstance(element, SourceEffort) or isinstance(element, SourceFlow):
68
+ self.inputs.append(element.value)
69
+
70
+ def __handle_bonds(self) -> None:
71
+ """Handles the bonds in the bond graph by assigning them to the appropriate elements.
72
+ This assignment propagates the bond references to the elements, so that each element knows which bonds it is connected to.
73
+ """
74
+
75
+ def handle_bond_element(element: Node, bond: Bond):
76
+ """Internal helper function to handle the assignment of a bond to an element.
77
+
78
+ Parameters
79
+ ----------
80
+ element : Node
81
+ Element of the bond to handle. Must be called with both `from_element` and `to_element` of the bond.
82
+ bond : Bond
83
+ The bond to assign to the element.
84
+
85
+ Raises
86
+ ------
87
+ ValueError
88
+ If the element is a junction and it is attemped to add a second strong bond.
89
+ """
90
+
91
+ if isinstance(element, ElementOnePort):
92
+ element.bond = bond
93
+
94
+ elif isinstance(element, ElementTwoPort):
95
+ if bond.to_element == element:
96
+ element.bond1 = bond
97
+ elif bond.from_element == element:
98
+ element.bond2 = bond
99
+
100
+ elif isinstance(element, Junction):
101
+ element.bonds.append(bond)
102
+
103
+ if isinstance(element, OneJunction):
104
+ if (bond.from_element == element and bond.causality == Causality.EFFORT_OUT) or (bond.to_element == element and bond.causality == Causality.FLOW_OUT):
105
+ # bond is strong bond for one junction
106
+ if element.strong_bond is None:
107
+ element.strong_bond = bond
108
+ print(f"Assigned strong bond {bond} to OneJunction {element}.")
109
+ else:
110
+ raise ValueError(f"OneJunction {element} already has a strong bond: {element.strong_bond}. Cannot assign {bond}.")
111
+
112
+ elif isinstance(element, ZeroJunction):
113
+ if (bond.from_element == element and bond.causality == Causality.FLOW_OUT) or (bond.to_element == element and bond.causality == Causality.EFFORT_OUT):
114
+ # bond is strong bond for zero junction
115
+ if element.strong_bond is None:
116
+ element.strong_bond = bond
117
+ print(f"Assigned strong bond {bond} to ZeroJunction {element}.")
118
+ else:
119
+ raise ValueError(f"ZeroJunction {element} already has a strong bond: {element.strong_bond}. Cannot assign {bond}.")
120
+
121
+ # Call helper function for both elements of each bond
122
+ for bond in self.bonds:
123
+ handle_bond_element(bond.from_element, bond)
124
+ handle_bond_element(bond.to_element, bond)
125
+
126
+ def __handle_equations(self) -> None:
127
+ """Accumulates the equations from all elements and junctions in the bond graph.
128
+ Also collects the state variables from all stateful elements.
129
+
130
+ Raises
131
+ ------
132
+ ValueError
133
+ If an element is not fully connected with bonds.
134
+ """
135
+
136
+ for element in self.elements:
137
+ if isinstance(element, ElementOnePort):
138
+ bond = element.bond
139
+ if bond is None:
140
+ raise ValueError(f"Element {element} has no connected bond.")
141
+
142
+ # Add equations from the element to the bond graph
143
+ self.equations.extend(element.equations)
144
+
145
+ if isinstance(element, StatefulElement):
146
+ self.state_vars.append(element.state_var)
147
+
148
+ elif isinstance(element, ElementTwoPort):
149
+ bond1 = element.bond1
150
+ bond2 = element.bond2
151
+ if bond1 is None or bond2 is None:
152
+ raise ValueError(f"Element {element} has no connected bonds.")
153
+
154
+ # Add equations from the element to the bond graph
155
+ self.equations.extend(element.equations)
156
+
157
+ elif isinstance(element, Junction):
158
+ self.equations.extend(element.equations)
159
+
160
+ def get_solution_equations(self) -> SolutionType:
161
+ """Returns the symbolic equations defining the solution of the bond graph.
162
+ The solution is computed by solving the accumulated equations of the bond graph symbolically using `sympy.solve`.
163
+ The bond counter is reset after computation to allow for future bond graphs to be created without conflicts.
164
+ This is a temporary workaround and should be (urgently) improved in the future.
165
+
166
+ Returns
167
+ -------
168
+ SolutionType
169
+ A dictionary mapping each symbolic variable to its solved expression.
170
+ The keys include the time derivatives of the state variables and the efforts and flows of all bonds.
171
+ """
172
+
173
+ self.__handle_bonds()
174
+ self.__handle_equations()
175
+ Bond.counter = 0 # Reset bond counter for future bond graphs after solution has been computed --> ToDo: fix this hacky solution
176
+
177
+ state_derivatives = [sp.Derivative(var, "t") for var in self.state_vars]
178
+ self.solution = sp.solve(
179
+ self.equations,
180
+ state_derivatives
181
+ + [b.effort for b in self.bonds]
182
+ + [b.flow for b in self.bonds],
183
+ )
184
+ return self.solution
185
+
186
+ def get_state_space(self) -> tuple[sp.Matrix, sp.Matrix, sp.Matrix, sp.Matrix, sp.Matrix, int, int, int]:
187
+ """Calculates the linear state space representation of the bond graph.
188
+ This method automatically calls `get_solution_equations` if the solution has not yet been computed.
189
+
190
+ Depending on the number of state variables, inputs, and outputs, the state space representation is either SISO, MIMO or a hybrid.
191
+ The general form of a (nonlinear) state space model is given by the equations:
192
+ x_dot = f(x, u)
193
+ y = h(x, u)
194
+ where x is the state vector, u is the input vector, and y is the output vector.
195
+ As the bond graph framework currently only supports linear elements, the state space representation can be simplified to a linear form.
196
+ The linear state space representation is given by the matrices A, B, C, D in the equations:
197
+ x_dot = A*x + B*u
198
+ y = C*x + D*u
199
+ The matrices are computed by taking the Jacobians of the functions f and h with respect to the state variables and inputs.
200
+ The output y is defined to be all efforts and flows of all bonds in the bond graph, with the efforts coming first and then the flows.
201
+ The input u is defined to be all sources (i.e. `SourceEffort` and `SourceFlow` elements) in the bond graph.
202
+ The state variables x are defined to be the state variables of all `StatefulElement` elements in the bond graph.
203
+
204
+ Returns
205
+ -------
206
+ tuple[sp.Matrix, sp.Matrix, sp.Matrix, sp.Matrix, sp.Matrix, int, int, int]
207
+ Returns the matrices A, B, C, D of the state space representation, the state vector x,
208
+ as well as the number of states, inputs, and outputs.
209
+ A in R^(n_states x n_states)
210
+ B in R^(n_states x n_inputs)
211
+ C in R^(n_outputs x n_states)
212
+ D in R^(n_outputs x n_inputs)
213
+ x in R^(n_states x 1)
214
+
215
+ Raises
216
+ ------
217
+ ValueError
218
+ If the system of equations for this bond graph could not be solved.
219
+ """
220
+
221
+ # Retrieve solution if needed
222
+ if self.solution is None:
223
+ if not self.get_solution_equations():
224
+ raise ValueError("Could not compute solution.")
225
+
226
+ n_states = len(self.state_vars)
227
+ n_inputs = len(self.inputs) # Number of inputs (sources)
228
+ n_outputs = 2 * len(self.bonds) # effort & flow for each bond
229
+
230
+ # General form of a state space model
231
+ # x_dot = f(x, u)
232
+ # y = h(x, u)
233
+ # Simplification for linear systems:
234
+ # x_dot = A*x + B*u
235
+ # y = C*x + D*u
236
+ # --> therefore
237
+ # A = ∂f/∂x, B = ∂f/∂u, C = ∂h/∂x, D = ∂h/∂u each at stationary point 0
238
+
239
+ f: sp.Matrix = sp.zeros(n_states, 1)
240
+ for i, state_var in enumerate(self.state_vars):
241
+ state_deriv = sp.Derivative(state_var, "t") # symbolic derivative dx/dt
242
+ f[i] = self.solution[state_deriv]
243
+
244
+ h: sp.Matrix = sp.zeros(n_outputs, 1) # efforts then flows
245
+ for i, bond in enumerate(self.bonds):
246
+ h[i] = self.solution[bond.effort]
247
+ h[i + n_outputs // 2] = self.solution[bond.flow]
248
+
249
+ A = f.jacobian(self.state_vars)
250
+ B = f.jacobian(self.inputs)
251
+
252
+ C = h.jacobian(self.state_vars)
253
+ D = h.jacobian(self.inputs)
254
+ # alternatively could use sp.linear_eq_to_matrix(...)
255
+
256
+ return A, B, C, D, sp.Matrix(self.state_vars), n_states, n_inputs, n_outputs
257
+
258
+ def plot(self, layout: Callable[[nx.Graph, ...], dict] = nx.spectral_layout, **kwargs) -> tuple[plt.Figure, plt.Axes]:
259
+ """Plots the bond graph as a `networkx` graph.
260
+
261
+ Parameters
262
+ ----------
263
+ layout : Callable[[nx.Graph, ...], dict], optional
264
+ `networkx` layout function for plotting the graph, by default `nx.spectral_layout`
265
+ This is only called if the graph is not a directed acyclic graph (DAG).
266
+ If the graph is a DAG, a `multipartite_layout` is used instead.
267
+
268
+ Returns
269
+ -------
270
+ tuple[plt.Figure, plt.Axes]
271
+ Matplotlib Figure and axes objects for the plot.
272
+ Raises
273
+ ------
274
+ ValueError
275
+ If an edge has no valid causality assigned.
276
+ """
277
+
278
+ G = nx.DiGraph()
279
+
280
+ for elem in self.elements:
281
+ G.add_node(elem.name, label=elem.name)
282
+
283
+ for bond in self.bonds:
284
+ G.add_edge(
285
+ bond.from_element.name,
286
+ bond.to_element.name,
287
+ label=bond.num,
288
+ causality=bond.causality,
289
+ )
290
+
291
+ # https://networkx.org/documentation/stable/auto_examples/graph/plot_dag_layout.html
292
+ if nx.is_directed_acyclic_graph(G):
293
+ for layer, nodes in enumerate(nx.topological_generations(G)):
294
+ # `multipartite_layout` expects the layer as a node attribute, so add the
295
+ # numeric layer value as a node attribute
296
+ for node in nodes:
297
+ G.nodes[node]["layer"] = layer
298
+
299
+ pos = nx.multipartite_layout(G, subset_key="layer")
300
+ else:
301
+ pos = layout(G, **kwargs)
302
+
303
+ fig, ax = plt.subplots(figsize=(10, 8))
304
+ ax.set_axis_off()
305
+ nx.draw_networkx(
306
+ G,
307
+ pos,
308
+ ax=ax,
309
+ with_labels=True,
310
+ node_size=2000,
311
+ node_color="lightblue",
312
+ font_size=10,
313
+ font_color="black",
314
+ arrows=True,
315
+ )
316
+ nx.draw_networkx_edge_labels(
317
+ G,
318
+ pos,
319
+ edge_labels=nx.get_edge_attributes(G, "label"),
320
+ font_size=8,
321
+ ax=ax,
322
+ )
323
+
324
+ # Function to add a perpendicular line
325
+ def draw_causal_stroke(ax, p1, p2, at="head", length=20, node_size=2000, padding=2):
326
+ """Draw a causal stroke perpendicular to the edge (p1 -> p2).
327
+
328
+ Parameters
329
+ ----------
330
+ ax : matplotlib.axes.Axes
331
+ The axes to draw on.
332
+ p1 : tuple[float, float]
333
+ The (x, y) coordinates of the first node.
334
+ p2 : tuple[float, float]
335
+ The (x, y) coordinates of the second node.
336
+ at : str, optional
337
+ Where to draw the stroke, by default "head"
338
+ length : int, optional
339
+ The length of the stroke, by default 20
340
+ node_size : int, optional
341
+ The size of the nodes, by default 2000
342
+ padding : int, optional
343
+ The padding between the node and the stroke, by default 2
344
+
345
+ Raises
346
+ ------
347
+ ValueError
348
+ If the 'at' parameter is not "head" or "tail".
349
+ """
350
+
351
+ # --- Convert node size (points^2) to radius in pixels ---
352
+ radius_points = np.sqrt(node_size / np.pi)
353
+ radius_pixels = radius_points * ax.figure.dpi / 72.0 # 1 point = 1/72 inch
354
+ offset = radius_pixels + padding # + padding (in px) so stroke sits outside node
355
+
356
+ # Transform points to display (pixel) coordinates
357
+ p1_disp = ax.transData.transform(p1)
358
+ p2_disp = ax.transData.transform(p2)
359
+
360
+ # Edge vector in display coords
361
+ vec = p2_disp - p1_disp
362
+ norm = np.linalg.norm(vec)
363
+ if norm == 0:
364
+ return
365
+ uvec = vec / norm
366
+
367
+ # Perpendicular in display coords
368
+ perp = np.array([-uvec[1], uvec[0]])
369
+
370
+ # Base point (head or tail), offset in pixels
371
+ if at == "head":
372
+ base_disp = p2_disp - uvec * offset
373
+ elif at == "tail":
374
+ base_disp = p1_disp + uvec * offset
375
+ else:
376
+ raise ValueError(f"Invalid value for 'at': {at}")
377
+
378
+ # Stroke endpoints in display coords
379
+ p_start_disp = base_disp - perp * (length / 2)
380
+ p_end_disp = base_disp + perp * (length / 2)
381
+
382
+ # Transform back to data coords for plotting
383
+ p_start = ax.transData.inverted().transform(p_start_disp)
384
+ p_end = ax.transData.inverted().transform(p_end_disp)
385
+
386
+ ax.plot([p_start[0], p_end[0]], [p_start[1], p_end[1]], color="k", lw=1.0)
387
+
388
+ # Add perpendicular lines to edges
389
+ for u, v, data in G.edges(data=True):
390
+ causality = data.get("causality", None)
391
+
392
+ if causality is Causality.EFFORT_OUT:
393
+ draw_causal_stroke(ax, pos[u], pos[v], at="head", padding=-2)
394
+ elif causality is Causality.FLOW_OUT:
395
+ draw_causal_stroke(ax, pos[u], pos[v], at="tail", padding=-2)
396
+ else:
397
+ raise ValueError(f"Edge {u}->{v} has no valid causality: {causality} --> this should never happen!")
398
+
399
+ return fig, ax
pyBondGraph/core.py ADDED
@@ -0,0 +1,188 @@
1
+ from abc import ABC, abstractmethod
2
+ from enum import Enum
3
+
4
+ import sympy as sp
5
+
6
+
7
+ class Causality(Enum):
8
+ """Enumeration for bond causality types.
9
+ This definition is always from the perspective of the `from_element`.
10
+ Therefore a `Bond(OneJunction(...), Inductor(...), "effort_out")` means that the
11
+ `OneJunction` imposes effort on the `Inductor`, meaning it has an equivalent `effort_in` causality.
12
+ Likewise a `Bond(OneJunction(...), Capacitor(...), "flow_out")` means that the `OneJunction` imposes
13
+ flow on the `Capacitor`, meaning it has an equivalent `flow_in`/`effort_out` causality.
14
+ """
15
+
16
+ EFFORT_OUT = "effort_out"
17
+ FLOW_OUT = "flow_out"
18
+
19
+
20
+ class StatefulElement(ABC):
21
+ """Base class for all stateful elements (capacitor, inductor) in the bond graph.
22
+ Requires implementation of a `state_var` property that returns the symbolic state variable associated with the element.
23
+ """
24
+
25
+ @property
26
+ @abstractmethod
27
+ def state_var(self) -> sp.Symbol:
28
+ """sp.Symbol: Returns the symbolic state variable associated with the stateful element."""
29
+ raise NotImplementedError("Subclasses should implement this method")
30
+
31
+
32
+ class Node(ABC):
33
+ """Base class for all nodes in the bond graph."""
34
+
35
+ def __init__(self, name: str):
36
+ """Base class for all nodes in the bond graph.
37
+
38
+ Parameters
39
+ ----------
40
+ name : str
41
+ The name of the node. Will be shown on the bond graph plot.
42
+ """
43
+ self.name = name
44
+
45
+ def __repr__(self) -> str:
46
+ return f"{self.__class__.__name__}({self.name})"
47
+
48
+
49
+ class Bond:
50
+ """Represents a bond between two elements in the bond graph."""
51
+
52
+ counter = 0
53
+
54
+ def __init__(self, from_element: Node, to_element: Node, causality: str | Causality):
55
+ """Create a bond between two elements with specified causality.
56
+ The positive direction of this power bond is from `from_element` to `to_element`.
57
+ Efforts and flows are represented by symbolic `sympy.Symbol`s that are strictly real-valued.
58
+
59
+ Parameters
60
+ ----------
61
+ from_element : Node
62
+ The element where the bond originates.
63
+ to_element : Node
64
+ The element where the bond terminates.
65
+ causality : str | Causality
66
+ The causality of the bond, either `effort_out` or `flow_out`.
67
+ If a string is provided, it is converted to the corresponding `Causality` enum.
68
+ This definition is always from the perspective of the `from_element`.
69
+ Therefore a `Bond(OneJunction(...), Inductor(...), "effort_out")` means that the
70
+ `OneJunction` imposes effort on the `Inductor`, meaning it has an equivalent `effort_in` causality.
71
+ Likewise a `Bond(OneJunction(...), Capacitor(...), "flow_out")` means that the `OneJunction` imposes
72
+ flow on the `Capacitor`, meaning it has an equivalent `flow_in`/`effort_out` causality.
73
+
74
+ Raises
75
+ ------
76
+ ValueError
77
+ If the causality is not 'effort_out' or 'flow_out'.
78
+ """
79
+
80
+ self.from_element = from_element
81
+ self.to_element = to_element
82
+
83
+ if isinstance(causality, str):
84
+ causality = Causality(causality.lower()) # Convert string to Causality enum, case insensitive
85
+ # --> automatically raises ValueError if string is not valid
86
+
87
+ self.causality: Causality = causality
88
+
89
+ self.num = Bond.counter
90
+ Bond.counter += 1
91
+
92
+ self.effort = sp.Symbol(f"e_{self.num}", real=True)
93
+ self.flow = sp.Symbol(f"f_{self.num}", real=True)
94
+
95
+ @property
96
+ def elements(self) -> tuple[Node, Node]:
97
+ """tuple[Node, Node]: The two elements connected by the bond. First element is `from_element`, second is `to_element`."""
98
+ return (self.from_element, self.to_element)
99
+
100
+ def __repr__(self) -> str:
101
+ return f"Bond(from={self.from_element}, to={self.to_element}, causality={self.causality})"
102
+
103
+
104
+ class Junction(Node, ABC):
105
+ """Base class for all junctions in the bond graph."""
106
+
107
+ def __init__(self, name: str):
108
+ """Base class for all junctions in the bond graph.
109
+ Stores a list of associated bonds and a reference to the "strong" bond.
110
+
111
+ Parameters
112
+ ----------
113
+ name : str
114
+ The name of the junction. Will be shown on the bond graph plot.
115
+ """
116
+ super().__init__(name)
117
+ self.bonds: list[Bond] = []
118
+ self.strong_bond = None
119
+
120
+ @property
121
+ @abstractmethod
122
+ def equations(self) -> list[sp.Expr]:
123
+ """list[sp.Expr]: Returns the symbolic equations defining the behavior of the junction."""
124
+ raise NotImplementedError("Subclasses should implement this method")
125
+
126
+
127
+ class ElementOnePort(Node, ABC):
128
+ """Base class for all one-port elements in the bond graph.
129
+ Requires implementation of an `equations` property that returns the list of symbolic equations defining the element's behavior.
130
+ This can also be just one equation.
131
+ """
132
+
133
+ def __init__(self, name: str, value: str):
134
+ """ABC for a one-port element. Acts as base class for all one-port elements like Inductor, Capacitor, Resistor, SourceEffort, SourceFlow.
135
+ Stores an associated bond and a symbolic value (real and positive) for its defining characteristics e.g. resistance, capacitance, etc.
136
+
137
+ Parameters
138
+ ----------
139
+ name : str
140
+ The name of the element. Will be shown on the bond graph plot.
141
+ value : str
142
+ The name of the value associated with the element, is internally used for creating a `sympy.Symbol`.
143
+ """
144
+
145
+ super().__init__(name)
146
+ self.value = sp.Symbol(value, real=True, positive=True) # Ensure value is a positive real number
147
+ self.bond: Bond = None # bond that connects this element to a bond graph
148
+
149
+ @property
150
+ @abstractmethod
151
+ def equations(self) -> list[sp.Expr]:
152
+ """list[sp.Expr]: Returns the symbolic equations defining the behavior of the one-port element."""
153
+ raise NotImplementedError("Subclasses should implement this method")
154
+
155
+ def __repr__(self) -> str:
156
+ return f"{self.__class__.__name__}(name = {self.name}, value = {self.value})"
157
+
158
+
159
+ class ElementTwoPort(Node, ABC):
160
+ """Base class for all two-port elements in the bond graph.
161
+ Requires implementation of an `equations` property that returns the list of symbolic equations defining the element's behavior.
162
+ """
163
+
164
+ def __init__(self, name: str, value: str):
165
+ """ABC for a two-port element. Acts as base class for all two-port elements like Transformer, Gyrator.
166
+ Stores two associated bonds and a symbolic value (real and positive) for its defining characteristics e.g. conversion factor, ratio, etc.
167
+
168
+ Parameters
169
+ ----------
170
+ name : str
171
+ The name of the element. Will be shown on the bond graph plot.
172
+ value : str
173
+ The name of the value associated with the element, is internally used for creating a `sympy.Symbol`.
174
+ """
175
+
176
+ super().__init__(name)
177
+ self.value = sp.Symbol(value, real=True, positive=True)
178
+ self.bond1: Bond = None # ElementOther --(bond1)--> ElementTwoPort
179
+ self.bond2: Bond = None # ElementTwoPort --(bond2)--> ElementOther
180
+
181
+ @property
182
+ @abstractmethod
183
+ def equations(self) -> list[sp.Expr]:
184
+ """list[sp.Expr]: Returns the symbolic equations defining the behavior of the two-port element."""
185
+ raise NotImplementedError("Subclasses should implement this method")
186
+
187
+ def __repr__(self) -> str:
188
+ return f"{self.__class__.__name__}(name = {self.name}, value = {self.value})"
@@ -0,0 +1,359 @@
1
+ from .core import Causality, StatefulElement, ElementOnePort, ElementTwoPort, Junction
2
+
3
+ import sympy as sp
4
+
5
+
6
+ class SourceEffort(ElementOnePort):
7
+ """Represents a source of effort in the bond graph.
8
+ A source of effort provides a constant effort to its port, the associated flow follows automatically."""
9
+
10
+ def __init__(self, name: str, value: str):
11
+ """Create a source of effort in the bond graph.
12
+
13
+ Parameters
14
+ ----------
15
+ name : str
16
+ The name of the element. Forwarded to the `Node` base class.
17
+ value : str
18
+ The name of the element value, is internally used for creating a `sympy.Symbol`.
19
+ """
20
+ super().__init__(name, value)
21
+
22
+ @property
23
+ def equations(self) -> list[sp.Expr]:
24
+ # Source --> constant effort
25
+ return [self.bond.effort - self.value]
26
+
27
+
28
+ class SourceFlow(ElementOnePort):
29
+ """Represents a source of flow in the bond graph.
30
+ A source of flow provides a constant flow to its port, the associated effort follows automatically.
31
+ """
32
+
33
+ def __init__(self, name: str, value: str):
34
+ """Create a source of flow in the bond graph.
35
+
36
+ Parameters
37
+ ----------
38
+ name : str
39
+ The name of the element. Forwarded to the `Node` base class.
40
+ value : str
41
+ The name of the element value, is internally used for creating a `sympy.Symbol`.
42
+ """
43
+ super().__init__(name, value)
44
+
45
+ @property
46
+ def equations(self) -> list[sp.Expr]:
47
+ # Source --> constant flow
48
+ return [self.bond.flow - self.value]
49
+
50
+
51
+ class Capacitor(ElementOnePort, StatefulElement):
52
+ """Represents a linear compliance/capacitance element in the bond graph.
53
+ A capacitance relates the effort of its port with the integral of its flow by a constant capacitance value.
54
+ """
55
+
56
+ def __init__(self, name: str, value: str):
57
+ """Create a linear compliance/capacitance element in the bond graph.
58
+
59
+ Parameters
60
+ ----------
61
+ name : str
62
+ The name of the element. Forwarded to the `Node` base class.
63
+ value : str
64
+ The name of the element value, is internally used for creating a `sympy.Symbol`.
65
+ """
66
+ super().__init__(name, value)
67
+
68
+ @property
69
+ def state_var(self) -> sp.Symbol:
70
+ """Returns the symbolic state variable (generalized displacement) associated with the compliance/capacitance.
71
+
72
+ Returns
73
+ -------
74
+ sp.Symbol
75
+ The symbolic state variable representing the generalized displacement (q) of the compliance/capacitance.
76
+ """
77
+ return sp.Symbol(f"q_{self.name}", real=True)
78
+
79
+ @property
80
+ def equations(self) -> list[sp.Expr]:
81
+ """Returns the symbolic equations defining the behavior of the linear compliance/capacitance.
82
+ Formulated in the derivative of the internal state_var, stemming from integral causality.
83
+
84
+ Returns
85
+ -------
86
+ list[sp.Expr]
87
+ A list of symbolic equations representing the behavior of the compliance/capacitance.
88
+ """
89
+
90
+ # Dynamic equation: f = dq/dt, e = q / C
91
+ return [
92
+ self.bond.flow - sp.Derivative(self.state_var, "t"),
93
+ self.bond.effort - self.state_var / self.value,
94
+ ]
95
+
96
+
97
+ type Compliance = Capacitor # Alias for Compliance element
98
+
99
+
100
+ class Inductor(ElementOnePort, StatefulElement):
101
+ """Represents a linear inertia/inductance element in the bond graph.
102
+ An inductor relates the flow of its port with the integral of its effort by a constant inductance value.
103
+ """
104
+
105
+ def __init__(self, name: str, value: str):
106
+ """Create a linear inertia/inductance element in the bond graph.
107
+
108
+ Parameters
109
+ ----------
110
+ name : str
111
+ The name of the element. Forwarded to the `Node` base class.
112
+ value : str
113
+ The name of the element value, is internally used for creating a `sympy.Symbol`.
114
+ """
115
+ super().__init__(name, value)
116
+
117
+ @property
118
+ def state_var(self) -> sp.Symbol:
119
+ """Returns the symbolic state variable (generalized momentum) associated with the inertia/inductance.
120
+
121
+ Returns
122
+ -------
123
+ sp.Symbol
124
+ The symbolic state variable representing the generalized momentum (p) of the inertia/inductance.
125
+ """
126
+ return sp.Symbol(f"p_{self.name}", real=True)
127
+
128
+ @property
129
+ def equations(self) -> list[sp.Expr]:
130
+ """Returns the symbolic equations defining the behavior of the linear inertia/inductance.
131
+ Formulated in the derivative of the internal state_var, stemming from integral causality.
132
+
133
+ Returns
134
+ -------
135
+ list[sp.Expr]
136
+ A list of symbolic equations representing the behavior of the inertia/inductance.
137
+ """
138
+
139
+ # Dynamic equation: e = dp/dt, f = p / L
140
+ return [
141
+ self.bond.effort - sp.Derivative(self.state_var, "t"),
142
+ self.bond.flow - self.state_var / self.value,
143
+ ]
144
+
145
+
146
+ type Inertance = Inductor # Alias for Inertia element
147
+
148
+
149
+ class Resistor(ElementOnePort):
150
+ """Represents a linear resistance/damper element in the bond graph.
151
+ A resistor relates the effort and flow of its port by a constant resistance value.
152
+ """
153
+
154
+ def __init__(self, name: str, value: str):
155
+ """Create a linear resistance element.
156
+
157
+ Parameters
158
+ ----------
159
+ name : str
160
+ The name of the resistance. Forwarded to the `Node` base class.
161
+ value : str
162
+ The name of the resistance value, is internally used for creating a `sympy.Symbol`.
163
+ """
164
+ super().__init__(name, value)
165
+
166
+ @property
167
+ def equations(self) -> list[sp.Expr]:
168
+ """Returns the symbolic equations defining the behavior of the linear resistance.
169
+ The equation is formulated based on the causality of the associated bond.
170
+ This makes resolving causality issues easier, as there is no preferred one for resistors.
171
+
172
+ Returns
173
+ -------
174
+ list[sp.Expr]
175
+ A list of symbolic equations representing the behavior of the resistance.
176
+ """
177
+
178
+ # Resistance: e = R * f
179
+ if self.bond.causality == Causality.EFFORT_OUT:
180
+ return [self.bond.flow - 1 / self.value * self.bond.effort]
181
+ elif self.bond.causality == Causality.FLOW_OUT:
182
+ return [self.bond.effort - self.value * self.bond.flow]
183
+
184
+
185
+ type Resistance = Resistor # Alias for Resistance element
186
+
187
+
188
+ class Transformer(ElementTwoPort):
189
+ """Represents a transformer element in the bond graph.
190
+ A transformer relates the efforts of its two ports and the flows of its two ports by a constant ratio.
191
+ """
192
+
193
+ def __init__(self, name: str, value: str):
194
+ """Create a transformer element in the bond graph.
195
+
196
+ Parameters
197
+ ----------
198
+ name : str
199
+ The name of the element. Forwarded to the `Node` base class.
200
+ value : str
201
+ The name of the element value, is internally used for creating a `sympy.Symbol`.
202
+ """
203
+ super().__init__(name, value)
204
+
205
+ @property
206
+ def equations(self) -> list[sp.Expr]:
207
+ """Returns the symbolic equations defining the behavior of the transformer.
208
+ The equations are formulated based on the causality of the associated bonds.
209
+ This makes resolving causality issues easier, as there is no preferred causality for transformers.
210
+
211
+ Returns
212
+ -------
213
+ list[sp.Expr]
214
+ A list of symbolic equations representing the behavior of the transformer.
215
+
216
+ Raises
217
+ ------
218
+ ValueError
219
+ If both bonds are not assigned or if they do not have the same causality.
220
+ """
221
+
222
+ if self.bond1 is None or self.bond2 is None:
223
+ raise ValueError("Both bonds must be assigned to the transformer element.")
224
+
225
+ if self.bond1.causality != self.bond2.causality:
226
+ raise ValueError("Both bonds must have the same causality for a transformer element.")
227
+
228
+ if self.bond1.causality == Causality.EFFORT_OUT:
229
+ return [
230
+ self.bond1.flow - 1 / self.value * self.bond2.flow,
231
+ self.bond2.effort - 1 / self.value * self.bond1.effort,
232
+ ]
233
+ elif self.bond1.causality == Causality.FLOW_OUT:
234
+ return [
235
+ self.bond1.effort - self.value * self.bond2.effort,
236
+ self.bond2.flow - self.value * self.bond1.flow,
237
+ ]
238
+
239
+
240
+ class Gyrator(ElementTwoPort):
241
+ """Represents a gyrator element in the bond graph.
242
+ A gyrator relates the effort of one port with the flow of the other (and vice versa) by a constant ratio.
243
+ """
244
+
245
+ def __init__(self, name: str, value: str):
246
+ """Create a gyrator element in the bond graph.
247
+
248
+ Parameters
249
+ ----------
250
+ name : str
251
+ The name of the element. Forwarded to the `Node` base class.
252
+ value : str
253
+ The name of the element value, is internally used for creating a `sympy.Symbol`.
254
+ """
255
+ super().__init__(name, value)
256
+
257
+ @property
258
+ def equations(self) -> list[sp.Expr]:
259
+ """Returns the symbolic equations defining the behavior of the gyrator.
260
+ The equations are formulated based on the causality of the associated bonds.
261
+ This makes resolving causality issues easier, as there is no preferred causality for gyrators.
262
+
263
+ Returns
264
+ -------
265
+ list[sp.Expr]
266
+ A list of symbolic equations representing the behavior of the gyrator.
267
+
268
+ Raises
269
+ ------
270
+ ValueError
271
+ If both bonds are not assigned or if they have the same causality.
272
+ """
273
+
274
+ if self.bond1 is None or self.bond2 is None:
275
+ raise ValueError("Both bonds must be assigned to the gyrator element.")
276
+
277
+ if self.bond1.causality == self.bond2.causality:
278
+ raise ValueError("Both bonds must have the different causality for a gyrator element.")
279
+
280
+ if self.bond1.causality == Causality.EFFORT_OUT:
281
+ return [
282
+ self.bond1.flow - 1 / self.value * self.bond2.effort,
283
+ self.bond2.flow - 1 / self.value * self.bond1.effort,
284
+ ]
285
+ elif self.bond1.causality == Causality.FLOW_OUT:
286
+ return [
287
+ self.bond1.effort - self.value * self.bond2.flow,
288
+ self.bond2.effort - self.value * self.bond1.flow,
289
+ ]
290
+
291
+
292
+ class OneJunction(Junction):
293
+ """Represents a one-junction element in the bond graph.
294
+ A one-junction enforces equal flow (current, velocity, etc.) on all connected bonds and the sum of efforts to be zero .
295
+ The sign convention is that efforts of bonds going into the junction are positive, efforts of bonds going out of the junction are negative.
296
+ """
297
+
298
+ def __init__(self, name: str):
299
+ """Create a one-junction element in the bond graph.
300
+
301
+ Parameters
302
+ ----------
303
+ name : str
304
+ The name of the element. Forwarded to the `Node` base class.
305
+ """
306
+ super().__init__(name)
307
+
308
+ @property
309
+ def equations(self) -> list[sp.Expr]:
310
+ """Returns the symbolic equations defining the behavior of the one-junction.
311
+
312
+ Returns
313
+ -------
314
+ list[sp.Expr]
315
+ A list of symbolic equations representing the behavior of the one-junction.
316
+ """
317
+
318
+ effort_eq = 0
319
+ for b in self.bonds:
320
+ dir = +1 if b.to_element == self else -1
321
+ effort_eq += dir * b.effort
322
+
323
+ flow_eq = [self.bonds[0].flow - b.flow for b in self.bonds[1:]]
324
+ return [effort_eq, *flow_eq]
325
+
326
+
327
+ class ZeroJunction(Junction):
328
+ """Represents a zero-junction element in the bond graph.
329
+ A zero-junction enforces equal effort (voltage, force, etc.) on all connected bonds and the sum of flows to be zero .
330
+ The sign convention is that flows of bonds going into the junction are positive, flows of bonds going out of the junction are negative.
331
+ """
332
+
333
+ def __init__(self, name: str):
334
+ """Create a zero-junction element in the bond graph.
335
+
336
+ Parameters
337
+ ----------
338
+ name : str
339
+ The name of the element. Forwarded to the `Node` base class.
340
+ """
341
+ super().__init__(name)
342
+
343
+ @property
344
+ def equations(self) -> list[sp.Expr]:
345
+ """Returns the symbolic equations defining the behavior of the zero-junction.
346
+
347
+ Returns
348
+ -------
349
+ list[sp.Expr]
350
+ A list of symbolic equations representing the behavior of the zero-junction.
351
+ """
352
+
353
+ flow_eq = 0
354
+ for b in self.bonds:
355
+ dir = +1 if b.to_element == self else -1
356
+ flow_eq += dir * b.flow
357
+
358
+ effort_eq = [self.bonds[0].effort - b.effort for b in self.bonds[1:]]
359
+ return [flow_eq, *effort_eq]
pyBondGraph/sensors.py ADDED
@@ -0,0 +1,86 @@
1
+ from .core import StatefulElement, ElementOnePort
2
+
3
+ import sympy as sp
4
+
5
+
6
+ class IntegratedEffortSensor(ElementOnePort, StatefulElement):
7
+ """Represents a sensor that integrates effort to its internal state variable."""
8
+
9
+ def __init__(self, name: str):
10
+ """Create a sensor that integrates effort to its internal state variable.
11
+
12
+ Parameters
13
+ ----------
14
+ name : str
15
+ The name of the element. Forwarded to the `Node` base class.
16
+ """
17
+ super().__init__(name, "")
18
+
19
+ @property
20
+ def state_var(self) -> sp.Symbol:
21
+ """Returns the symbolic state variable (integrated effort) associated with the sensor.
22
+
23
+ Returns
24
+ -------
25
+ sp.Symbol
26
+ The symbolic state variable representing the integrated effort.
27
+ """
28
+ return sp.Symbol(f"e_int_{self.name}", real=True)
29
+
30
+ @property
31
+ def equations(self) -> list[sp.Expr]:
32
+ """Returns the symbolic equations defining the behavior of the sensor that integrates effort.
33
+ Formulated in the derivative of the internal state_var, stemming from integral causality.
34
+
35
+ Returns
36
+ -------
37
+ list[sp.Expr]
38
+ A list of symbolic equations representing the behavior of the sensor.
39
+ """
40
+
41
+ # Dynamic equation: e = d(state_var)/dt, f = 0
42
+ return [
43
+ self.bond.effort - sp.Derivative(self.state_var, "t"),
44
+ self.bond.flow
45
+ ]
46
+
47
+ class IntegratedFlowSensor(ElementOnePort, StatefulElement):
48
+ """Represents a sensor that integrates flow to its internal state variable."""
49
+
50
+ def __init__(self, name: str):
51
+ """Create a sensor that integrates effort to its internal state variable.
52
+
53
+ Parameters
54
+ ----------
55
+ name : str
56
+ The name of the element. Forwarded to the `Node` base class.
57
+ """
58
+ super().__init__(name, "")
59
+
60
+ @property
61
+ def state_var(self) -> sp.Symbol:
62
+ """Returns the symbolic state variable (integrated flow) associated with the sensor.
63
+
64
+ Returns
65
+ -------
66
+ sp.Symbol
67
+ The symbolic state variable representing the integrated flow.
68
+ """
69
+ return sp.Symbol(f"f_int_{self.name}", real=True)
70
+
71
+ @property
72
+ def equations(self) -> list[sp.Expr]:
73
+ """Returns the symbolic equations defining the behavior of the sensor that integrates flow.
74
+ Formulated in the derivative of the internal state_var, stemming from integral causality.
75
+
76
+ Returns
77
+ -------
78
+ list[sp.Expr]
79
+ A list of symbolic equations representing the behavior of the sensor.
80
+ """
81
+
82
+ # Dynamic equation: f = d(state_var)/dt, e = 0
83
+ return [
84
+ self.bond.flow - sp.Derivative(self.state_var, "t"),
85
+ self.bond.effort
86
+ ]
@@ -0,0 +1,170 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyBondGraph
3
+ Version: 0.1.0
4
+ Summary: Modelling tool for linear bond graph systems in Python
5
+ Author-email: Matthias Panny <matthias.panny@mci.edu>
6
+ License-Expression: CC-BY-NC-SA-4.0
7
+ Project-URL: Homepage, https://github.com/MrP123/pyBondGraph
8
+ Project-URL: Issues, https://github.com/MrP123/pyBondGraph/issues
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Classifier: Programming Language :: Python :: 3.14
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Topic :: Scientific/Engineering
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Requires-Python: >=3.13
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: sympy>=1.14.0
18
+ Requires-Dist: networkx>=3.5
19
+ Requires-Dist: numpy>=2.3.0
20
+ Requires-Dist: matplotlib>=3.10.3
21
+ Requires-Dist: control>=0.10.2
22
+ Provides-Extra: streamlit
23
+ Requires-Dist: streamlit>=1.47.0; extra == "streamlit"
24
+ Requires-Dist: streamlit-flow-component>=1.6.1; extra == "streamlit"
25
+ Dynamic: license-file
26
+
27
+ # pyBondGraph
28
+ **pyBondGraph** is a Python library for **modeling and analyzing linear bond graph systems** using symbolic computation.
29
+
30
+ 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
+ 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
+
34
+ ---
35
+
36
+ # Features
37
+
38
+ * Programmatic construction of **bond graph models**
39
+ * Automatic **symbolic equation derivation** using SymPy
40
+ * Conversion of models to **state-space systems**
41
+ * Example models for electrical and mechanical systems
42
+
43
+ ---
44
+
45
+ # Installation
46
+
47
+ ## Install directly from GitHub
48
+ The easiest way to install the library is directly via pip:
49
+ ```bash
50
+ pip install git+https://github.com/MrP123/pyBondGraph.git
51
+ ```
52
+ This installs the latest version of the package from the repository.
53
+
54
+ ---
55
+
56
+ ## Development installation
57
+ To work with the source code:
58
+ ```bash
59
+ git clone https://github.com/MrP123/pyBondGraph.git
60
+ cd pyBondGraph
61
+ pip install -e .
62
+ ```
63
+
64
+ ---
65
+
66
+ # Dependencies
67
+
68
+ The main dependencies are:
69
+
70
+ * `sympy`
71
+ * `numpy`
72
+ * `networkx`
73
+ * `matplotlib`
74
+ * `control` only needed for the examples
75
+
76
+ Optional dependencies are used for experimental visualization tools.
77
+
78
+ ---
79
+
80
+ # Basic Usage
81
+ A bond graph model is constructed by creating elements and connecting them via bonds.
82
+
83
+ Simple RC-Filter circuit:
84
+
85
+ ```python
86
+ from pyBondGraph import BondGraph, SourceEffort, Resistor, Capacitor, OneJunction, Bond, Causality
87
+
88
+ bg = BondGraph()
89
+
90
+ # create elements
91
+ voltage_source = SourceEffort("U", "u_in")
92
+ resistor = Resistor("R", "R")
93
+ capacitor = Capacitor("C", "C")
94
+ series_junction = OneJunction("J1")
95
+
96
+ # connect elements
97
+ # causalities need to be assigned manually
98
+ bg.add_bond(Bond(voltage_source, series_junction, Causality.EFFORT_OUT))
99
+ bg.add_bond(Bond(series_junction, resistor, Causality.EFFORT_OUT))
100
+ bg.add_bond(Bond(series_junction, capacitor, Causality.FLOW_OUT))
101
+
102
+ # plot the resulting BondGraph
103
+ bg.plot()
104
+
105
+ # derive system equations in linear state space form
106
+ A, B, C, D, x, n_states, n_inputs, n_outputs = bond_graph.get_state_space()
107
+ ```
108
+
109
+ The library automatically derives the **symbolic system equations** describing the dynamics of the model.
110
+
111
+ ---
112
+
113
+
114
+ # Core Concepts
115
+ Bond graphs represent **power exchange between system components**, where power is the product of **effort** and **flow** associated with the following components:
116
+
117
+ ## Elements
118
+ | Element | Meaning |
119
+ |---------|------------------------------------------|
120
+ | R | Dissipation |
121
+ | C | Energy storage (compliance, capacitance) |
122
+ | I | Energy storage (inertia, inductance) |
123
+ | Se | Effort source |
124
+ | Sf | Flow source |
125
+
126
+ ## Junctions
127
+ | Junction | Meaning |
128
+ |----------|---------------|
129
+ | 0 | Common effort |
130
+ | 1 | Common flow |
131
+
132
+ ## Sensors
133
+ | Sensor | Meaning |
134
+ |------------------------|---------------------------------------------|
135
+ | IntegratedEffortSensor | Measures integral of the effort at its bond |
136
+ | IntegratedFlowSensor | Measures integral of the flow at its bond |
137
+
138
+ In mechanical bond graph models:
139
+ * **flow** corresponds to **velocity**
140
+ * **effort** corresponds to **force**
141
+
142
+ An **integrated flow sensor** can therefore be used to compute **position**:
143
+
144
+ ---
145
+
146
+ # Example Systems
147
+ The repository contains example models illustrating typical applications of bond graphs.
148
+
149
+ ### RLC Circuit
150
+ Demonstrates modeling of an electrical circuit using bond graph elements.
151
+
152
+ ### DC Motor
153
+ A multi-domain electromechanical system coupling electrical and mechanical dynamics.
154
+
155
+ ### Transformer
156
+ Example of energy transformation between two ports.
157
+
158
+ ### Two DOF Mass–Spring–Damper System
159
+ Classical mass-spring-damper system with two degrees of freedom.
160
+
161
+ ---
162
+
163
+ # Typical Applications
164
+ Bond graph modeling is particularly useful for:
165
+
166
+ * electromechanical systems
167
+ * robotics and mechatronics
168
+ * multi-domain energy systems
169
+ * control system modeling
170
+ * teaching system dynamics
@@ -0,0 +1,10 @@
1
+ pyBondGraph/__init__.py,sha256=-C74b1kF3iyVfMe9RwvsA0DzznMe1UQa8jopn-1cSAY,717
2
+ pyBondGraph/bondgraph.py,sha256=5MqOFeNWEDSM6E5AD7TSfblAWF59IWeVwu3qOxAaP04,16656
3
+ pyBondGraph/core.py,sha256=rDT82D6_1jUzrq1uJ3YLxHGUKfOKtr313vO2uaw-CiM,7851
4
+ pyBondGraph/elements.py,sha256=yQu88NRoDbnG8r8gPZ5NoN7ydA0Oud7fzW1AiWBx3S8,13170
5
+ pyBondGraph/sensors.py,sha256=p8HdCkClxslBuMsd46mQKn2uuufWGZLBh_iV5boZRZk,2891
6
+ pybondgraph-0.1.0.dist-info/licenses/LICENSE,sha256=gVYUJmavXr6Y0lO28X9WXKiwuEz0HaTCAJRIVkXuXb8,202
7
+ pybondgraph-0.1.0.dist-info/METADATA,sha256=WG1REhQcB4itvhGW-51VsXmm9dtVxrl_fgn79fqBZXo,5459
8
+ pybondgraph-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ pybondgraph-0.1.0.dist-info/top_level.txt,sha256=N3BmEIcuMU1Cqbc6ayds0jzxyZwyOA1MAy5sCO4qZ2o,12
10
+ pybondgraph-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/
@@ -0,0 +1 @@
1
+ pyBondGraph