Skip to content

Commit 8fe58ca

Browse files
committed
Changes related to using libcosimpy with the ECCO algorithm. Test is included in test_oscillator_fmu
1 parent 1fc34f7 commit 8fe58ca

10 files changed

Lines changed: 163 additions & 123 deletions

examples/ForcedOscillator.xml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@
77
<Simulator name="drv" source="DrivingForce.fmu" stepSize="0.01" />
88
</Simulators>
99
<Connections>
10-
<VariableConnection>
11-
<Variable simulator="drv" name="f[2]" />
12-
<Variable simulator="osc" name="f[2]" />
10+
<VariableConnection powerBond="force-velocity">
11+
<Variable simulator="drv" name="f[2]" causality="output"/>
12+
<Variable simulator="osc" name="f[2]" causality="input"/>
13+
</VariableConnection>
14+
<VariableConnection powerBond="force-velocity">
15+
<Variable simulator="osc" name="v[2]" causality="output"/>
16+
<Variable simulator="drv" name="v_osc[2]" causality="input"/>
1317
</VariableConnection>
1418
</Connections>
1519
<EccoConfiguration>

examples/bouncing_ball_3d.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def _interface(self, name: str, start: str | float | tuple) -> Variable:
130130
variability="continuous",
131131
initial="exact",
132132
start=start,
133-
rng=((0, "100 m"), None, (0, "10 m")),
133+
rng=((0, "100 m"), None, (0, "39.371 inch")),
134134
)
135135
elif name == "speed":
136136
return Variable(

examples/driving_force_fmu.py

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,21 @@ def func(time: float, ampl: float = 1.0, omega: float = 0.1, d_omega: float = 0.
2121
"""
2222
if d_omega == 0.0:
2323
return np.array((0, 0, ampl * sin(omega * time)), float)
24-
else:
25-
return np.array((0, 0, ampl * sin((omega + d_omega*time) * time)), float)
24+
else:
25+
return np.array((0, 0, ampl * sin((omega + d_omega * time) * time)), float)
2626

2727

2828
class DrivingForce(Model):
2929
"""A driving force in 3 dimensions which produces an ouput per time and can be connected to the oscillator.
3030
31+
Note1: the FMU model is made directly (without a basic python class model), which is not recommended!
32+
Note2: the speed of the connected oscillator is added as additional connector.
33+
Since the driving is forced, the input speed is ignored, but it is needed for the ECCO algorithm (power bonds).
34+
3135
Note: This completely replaces DrivingForce (do_step and other functions are not re-used).
3236
3337
Args:
3438
func (callable)=func: The driving force function f(t).
35-
Note: The func can currently not really be handled as parameter and must be hard-coded here (see above).
36-
Soon to come: Model.build() function which honors parameters, such that function can be supplied from
37-
outside and the FMU can be re-build without changing the class.
3839
"""
3940

4041
def __init__(
@@ -51,7 +52,13 @@ def __init__(
5152
"Siegfried Eisinger",
5253
**kwargs,
5354
)
54-
# interface Variables
55+
# interface Variables. We define first their values, to help pyright, since the basic model is missing
56+
self.ampl: float = ampl
57+
self.freq: float = freq
58+
self.d_freq: float = d_freq
59+
self.function: Callable = function
60+
self.f = np.array((0, 0, 0), float)
61+
self.v_osc = np.array((0, 0, 0), float)
5562
self._ampl = Variable(self, "ampl", "The amplitude of the force in N", start=ampl)
5663
self._freq = Variable(self, "freq", "The frequency of the force in 1/s", start=freq)
5764
self._d_freq = Variable(self, "d_freq", "Change of frequency of the force in 1/s**2", start=d_freq)
@@ -64,15 +71,25 @@ def __init__(
6471
variability="continuous",
6572
start=np.array((0, 0, 0), float),
6673
)
74+
self._v_osc = Variable(
75+
self,
76+
"v_osc",
77+
"Input connector for the speed of the connected element in m/s",
78+
causality="input",
79+
variability="continuous",
80+
start=np.array((0, 0, 0), float),
81+
)
6782

6883
def do_step(self, current_time: float, step_size: float):
69-
self.f = self.func(current_time+step_size)
84+
self.f = self.func(current_time + step_size)
7085
return True # very important!
7186

7287
def exit_initialization_mode(self):
7388
"""Set internal state after initial variables are set."""
74-
self.func = partial(self.function,
75-
ampl=self.ampl,
76-
omega=2 * pi * self.freq,
77-
d_omega= 0.0 if self.d_freq == 0.0 else 2 * pi * self.d_freq)
89+
self.func = partial(
90+
self.function,
91+
ampl=self.ampl,
92+
omega=2 * pi * self.freq,
93+
d_omega=0.0 if self.d_freq == 0.0 else 2 * pi * self.d_freq,
94+
)
7895
logger.info(f"Initial settings: ampl={self.ampl}, freq={self.freq}, d_freq={self.d_freq}")

examples/oscillator.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def __init__(
3333
c: tuple[float, float, float] | tuple[str, str, str] = (0.0, 0.0, 0.0),
3434
m: float = 1.0,
3535
tolerance: float = 1e-5,
36-
f_func: Callable|None = None
36+
f_func: Callable | None = None,
3737
):
3838
self.k = np.array(k, float)
3939
self.c = np.array(c, float)
@@ -51,8 +51,8 @@ def ode_func(self, t: float, y: np.ndarray, i: int, f: float) -> np.ndarray:
5151
if self.f_func is None:
5252
if f != 0:
5353
res += np.array((f, 0), float)
54-
elif i==2: # only implemented for z
55-
res += np.array((self.f_func(t)[i], 0), float)
54+
elif i == 2: # only implemented for z
55+
res += np.array((self.f_func(t)[i], 0), float)
5656
return res
5757

5858
def do_step(self, current_time: float, step_size: int | float) -> bool:

src/component_model/variable.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def __str__(self):
6868
return txt
6969

7070
def parse_quantity(self, quantity: PyType, ureg: UnitRegistry, typ: type | None = None) -> PyType:
71-
"""Disect the provided quantity in terms of magnitude and unit, if provided as string.
71+
"""Parse the provided quantity in terms of magnitude and unit, if provided as string.
7272
If another type is provided, dimensionless units are assumed.
7373
7474
Args:

tests/test_axle_fmu.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def test_use_fmu(axle_fmu: Path, show: bool):
146146

147147

148148
if __name__ == "__main__":
149-
retcode = 0#pytest.main(["-rP -s -v", __file__])
149+
retcode = 0 # pytest.main(["-rP -s -v", __file__])
150150
assert retcode == 0, f"Return code {retcode}"
151151
import os
152152

tests/test_bouncing_ball_3d.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,8 @@ def get_status(sim):
420420
assert values[6] == 1.5, "Initial setting did not work"
421421
assert values[5] == -0.015, "Initial setting did not have the expected effect on speed"
422422

423+
manipulator.slave_real_values(ibb, [0, 1, 2], [1.0, 2.0, 3.0])
424+
423425

424426
# values = observer.last_real_values(0, list(range(11)))
425427
# print("VALUES2", values)
@@ -428,6 +430,7 @@ def get_status(sim):
428430

429431
# sim.simulate_until(target_time=3e9)
430432

433+
431434
def test_from_fmu(bouncing_ball_fmu):
432435
assert bouncing_ball_fmu.exists(), "FMU not found"
433436
model = model_from_fmu(bouncing_ball_fmu)
@@ -450,13 +453,13 @@ def test_from_fmu(bouncing_ball_fmu):
450453

451454

452455
if __name__ == "__main__":
453-
retcode = 0 # pytest.main(["-rA", "-v", "--rootdir", "../", "--show", "False", __file__])
456+
retcode = pytest.main(["-rA", "-v", "--rootdir", "../", "--show", "False", __file__])
454457
assert retcode == 0, f"Non-zero return code {retcode}"
455458
import os
456459

457460
os.chdir(Path(__file__).parent / "test_working_directory")
458461
# test_bouncing_ball_class(show=False)
459-
test_make_bouncing_ball(_bouncing_ball_fmu())
462+
# test_make_bouncing_ball(_bouncing_ball_fmu())
460463
# test_use_fmu(_bouncing_ball_fmu(), True)
461464
# test_from_fmu( _bouncing_ball_fmu())
462-
# test_from_osp( _bouncing_ball_fmu())
465+
# test_from_osp(_bouncing_ball_fmu())

tests/test_oscillator.py

Lines changed: 46 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from functools import partial
22
from math import atan2, cos, exp, pi, sin, sqrt
3+
from pathlib import Path
34

45
import matplotlib.pyplot as plt
56
import numpy as np
6-
from pathlib import Path
77

88

99
def arrays_equal(res: tuple[float, ...] | list[float], expected: tuple[float, ...] | list[float], eps=1e-7):
@@ -29,9 +29,9 @@ def do_show(time: list, z: list, v: list, compare1: list | None = None, compare2
2929

3030
def force(t: float, ampl: float = 1.0, omega: float = 0.1, d_omega: float = 0.0):
3131
if d_omega == 0.0:
32-
return np.array((0, 0, ampl * sin(omega * t)), float) # fixed frequency
32+
return np.array((0, 0, ampl * sin(omega * t)), float) # fixed frequency
3333
else:
34-
return np.array((0, 0, ampl * sin((omega + d_omega*t) * t)), float) # frequency sweep
34+
return np.array((0, 0, ampl * sin((omega + d_omega * t) * t)), float) # frequency sweep
3535

3636

3737
def forced_oscillator(
@@ -118,6 +118,7 @@ def run_oscillation_z(
118118

119119
return (osc, times, z, v)
120120

121+
121122
def sweep_oscillation_z(
122123
k: float,
123124
c: float,
@@ -136,13 +137,9 @@ def sweep_oscillation_z(
136137
and return the oscillator object and the time series for z-position and z-velocity."""
137138

138139
from examples.oscillator import Oscillator
139-
f_func = f_func=partial(force, ampl=ampl, omega=0.0, d_omega=d_omega)
140-
osc = Oscillator(k=(1.0, 1.0, k),
141-
c=(0.0, 0.0, c),
142-
m=m,
143-
tolerance=tol,
144-
f_func = f_func
145-
)
140+
141+
f_func = f_func = partial(force, ampl=ampl, omega=0.0, d_omega=d_omega)
142+
osc = Oscillator(k=(1.0, 1.0, k), c=(0.0, 0.0, c), m=m, tolerance=tol, f_func=f_func)
146143
osc.x[2] = x0 # set initial z value
147144
osc.v[2] = v0 # set initial z-speed
148145
times, z, v, f = [], [], [], []
@@ -264,70 +261,75 @@ def area(x: list[float], y: list[float]):
264261
assert abs(y[-1] - 4.0) < 1e-12, f"Found {y[-1]}"
265262
osc, x, y = run_2d(x0=(1.0, 0.0, 0.0), v0=(0.0, 1.0, 0.0), k=(1.0, 1.0 / 15.8, 0), end=20 * np.pi)
266263
if show:
267-
show_2d(x,y)
264+
show_2d(x, y)
265+
268266

269267
def test_sweep_oscillator(show: bool = True):
270268
"""A forced oscillator where the force frequency is changed linearly as d_omega*time.
271269
The test demonstrates that a monolithic simulation provides accurate results in all ranges of the force frequency.
272270
Co-simulating the oscillator and the force, this does not work.
273271
"""
274-
osc, times0, z0, v0, f0 = sweep_oscillation_z( k=1.0,
275-
c=0.1,
276-
m=1.0,
277-
ampl=1.0,
278-
d_omega=0.1,
279-
x0=0.0,
280-
v0=0.0,
281-
dt=0.1, # 'ground truth', small dt
282-
end=100.0,
283-
tol=1e-3
284-
)
285-
with open(Path.cwd() / "oscillator_sweep0.dat", 'w') as fp:
272+
osc, times0, z0, v0, f0 = sweep_oscillation_z(
273+
k=1.0,
274+
c=0.1,
275+
m=1.0,
276+
ampl=1.0,
277+
d_omega=0.1,
278+
x0=0.0,
279+
v0=0.0,
280+
dt=0.1, # 'ground truth', small dt
281+
end=100.0,
282+
tol=1e-3,
283+
)
284+
with open(Path.cwd() / "oscillator_sweep0.dat", "w") as fp:
286285
for i in range(len(times0)):
287-
fp.write( f"{times0[i]}\t{z0[i]}\t{v0[i]}\t{f0[i]}\n")
288-
286+
fp.write(f"{times0[i]}\t{z0[i]}\t{v0[i]}\t{f0[i]}\n")
287+
289288
if show:
290-
freq = [0.1*t/2/np.pi for t in times0]
289+
freq = [0.1 * t / 2 / np.pi for t in times0]
291290
fig, ax = plt.subplots()
292291
ax.plot(freq, z0, label="z0(t)")
293292
ax.plot(freq, v0, label="v0(t)")
294293
ax.plot(freq, f0, label="F0(t)")
295294
ax.legend()
296295
plt.show()
297-
298-
osc, times, z, v, f = sweep_oscillation_z( k=1.0,
299-
c=0.1,
300-
m=1.0,
301-
ampl=1.0,
302-
d_omega=0.1,
303-
x0=0.0,
304-
v0=0.0,
305-
dt=1, # dt similar to resonance frequency
306-
end=100.0,
307-
tol=1e-3
308-
)
296+
297+
osc, times, z, v, f = sweep_oscillation_z(
298+
k=1.0,
299+
c=0.1,
300+
m=1.0,
301+
ampl=1.0,
302+
d_omega=0.1,
303+
x0=0.0,
304+
v0=0.0,
305+
dt=1, # dt similar to resonance frequency
306+
end=100.0,
307+
tol=1e-3,
308+
)
309309
i0 = 0
310-
for i in range(len(times)): # demonstrate that the results are accurate, even if dt is large
310+
for i in range(len(times)): # demonstrate that the results are accurate, even if dt is large
311311
t = times[i]
312-
while abs(times0[i0]-t) > 1e-10:
312+
while abs(times0[i0] - t) > 1e-10:
313313
i0 += 1
314314
assert times0[i0] - t < 0.1, f"Time entry for time {t} not found in times0"
315-
315+
316316
assert abs(z0[i0] - z[i]) < 2e-2, f"Time {t}. Found {z0[i0]} != {z[i]}"
317317
assert abs(v0[i0] - v[i]) < 2e-2, f"Time {t}. Found {v0[i0]} != {v[i]}"
318-
318+
319319
if show:
320320
fig, ax = plt.subplots()
321321
ax.plot(times0, z0, label="z0(t)")
322322
ax.plot(times, z, label="z(t)")
323323
ax.legend()
324324
plt.show()
325325

326+
326327
if __name__ == "__main__":
327-
retcode = 0 # pytest.main(["-rA", "-v", "--rootdir", "../", "--show", "False", __file__])
328+
retcode = pytest.main(["-rA", "-v", "--rootdir", "../", "--show", "False", __file__])
328329
assert retcode == 0, f"Non-zero return code {retcode}"
329330
import os
331+
330332
os.chdir(Path(__file__).parent.absolute() / "test_working_directory")
331333
# test_oscillator_class(show=True)
332334
# test_2d(show=True)
333-
test_sweep_oscillator()
335+
# test_sweep_oscillator()

0 commit comments

Comments
 (0)