Skip to content

Commit 8b477f2

Browse files
committed
Corrected problem with proper tracking of initial conditions when setting goals in Control
1 parent 9f288a3 commit 8b477f2

7 files changed

Lines changed: 89 additions & 61 deletions

File tree

examples/DrivingForce.fmu

86 Bytes
Binary file not shown.

examples/DrivingForce6D.fmu

86 Bytes
Binary file not shown.

examples/HarmonicOscillator.fmu

86 Bytes
Binary file not shown.

examples/HarmonicOscillator6D.fmu

86 Bytes
Binary file not shown.

src/component_model/range.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def __init__(
5959
l_rng[i] = val # type: ignore[reportArgumentType] ## l_rng is not empty # fixed display value
6060
else:
6161
assert isinstance(r, (str, int, bool, float, Enum)), f"Found type {type(r)}"
62-
check, q = unit.compatible(r, no_unit=False, strict=True) # q in base units
62+
check, q = unit.compatible(r, no_unit=False, strict=False) # q in base units
6363
if not check:
6464
raise ValueError(f"Provided range {rng}[{i}] is not conformant with unit {unit}") from None
6565
assert isinstance(q, (int, bool, float)), "Unexpected type {type(q)} in {rng}[{i}]"

src/component_model/utils/controls.py

Lines changed: 59 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -19,62 +19,6 @@ class RW(Protocol):
1919
def __call__(self, val: float | None = None, /) -> float: ...
2020

2121

22-
class Controls(object):
23-
"""Keep track of float variable changes.
24-
25-
limit_err: Determines how limit errors are dealt with.
26-
Anything below critical sets the value to the limit and provides a logger message.
27-
Critical leads to a program run error.
28-
"""
29-
30-
limit_err: int = logging.WARNING
31-
32-
def __init__(
33-
self,
34-
limit_err: int = logging.WARNING,
35-
):
36-
Controls.limit_err = limit_err
37-
self.controls: list["Control"] = []
38-
39-
@property
40-
def nogoals(self):
41-
for crl in self.controls:
42-
if len(crl.goal):
43-
return False
44-
return True
45-
46-
def extend(self, crls: tuple["Control", ...]):
47-
for crl in crls:
48-
self.append(crl)
49-
50-
def append(self, crl: "Control"):
51-
"""Append one or several Control object(s)."""
52-
for c in self.controls:
53-
if c.name == crl.name:
54-
raise KeyError(f"Control with name {c.name} already exists. Choose a unique name.") from None
55-
self.controls.append(crl)
56-
57-
def __getitem__(self, ident: int | str):
58-
"""Get the control object identified by ident (index within .controls or valid name)."""
59-
if isinstance(ident, str):
60-
for crl in self.controls:
61-
if crl.name == ident:
62-
return crl
63-
raise KeyError(f"Control with name {ident} not found.") from None
64-
elif isinstance(ident, int):
65-
if ident < 0 or ident >= len(self.controls):
66-
raise ValueError(f"Control {ident} does not exist within set of controls.") from None
67-
return self.controls[ident]
68-
else:
69-
raise TypeError(f"Integer expected as subscript. Found {ident}") from None
70-
71-
def step(self, time: float, dt: float):
72-
"""Step towards the goals (if goals are set)."""
73-
if not self.nogoals:
74-
for crl in self.controls:
75-
crl.step(time, dt)
76-
77-
7822
class Control(object):
7923
"""Keep track of the changes of a single float variable, avoiding discontinuities and abrupt changes.
8024
@@ -194,7 +138,7 @@ def check_limit(self, order: int, value: float) -> float:
194138
return lim
195139
return value
196140

197-
def setgoal(self, order: int, value: float | None, speed: float | None = 0.0, acc: float | None = 0.0):
141+
def setgoal(self, order: int, value: float | None, speed: float | None = None, acc: float | None = None):
198142
"""Set a new goal for the control, i.e. set the required time-acceleration sequence
199143
to reach value with all derivatives = 0.0.
200144
@@ -285,7 +229,6 @@ def setgoal(self, order: int, value: float | None, speed: float | None = 0.0, ac
285229
(t0 + dt1 + dt2 + dt3, acc2),
286230
(float("inf"), 0.0),
287231
]
288-
logger.info(f"New goal({self.name}): {self.goal}")
289232
self.started = False
290233

291234
@property
@@ -299,6 +242,7 @@ def step(self, time: float, dt: float):
299242
if not self.started: # not yet started. Need to add current time.
300243
for i, (t, a) in enumerate(self.goal):
301244
self.goal[i] = (t + time, a)
245+
logger.info(f"@{time}. New goal({self.name}): {self.goal}")
302246
self.started = True
303247
_t, self.acc = self.goal[0]
304248
if time > _t: # move to correct goal entry
@@ -317,4 +261,61 @@ def step(self, time: float, dt: float):
317261
_t, self.acc = self.goal[0]
318262

319263
if np.isinf(_t) and abs(self.acc) < 1e-12 and abs(self.speed) < 1e-12:
264+
logger.info(f"@{time}. Goal {self.name} finalized.")
320265
self.goal = []
266+
267+
268+
class Controls(object):
269+
"""Keep track of float variable changes.
270+
271+
limit_err: Determines how limit errors are dealt with.
272+
Anything below critical sets the value to the limit and provides a logger message.
273+
Critical leads to a program run error.
274+
"""
275+
276+
limit_err: int = logging.WARNING
277+
278+
def __init__(
279+
self,
280+
limit_err: int = logging.WARNING,
281+
):
282+
Controls.limit_err = limit_err
283+
self.controls: list[Control] = []
284+
285+
@property
286+
def nogoals(self):
287+
for crl in self.controls:
288+
if len(crl.goal):
289+
return False
290+
return True
291+
292+
def append(self, crl: "Control"):
293+
"""Append one or several Control object(s)."""
294+
for c in self.controls:
295+
if c.name == crl.name:
296+
raise KeyError(f"Control with name {c.name} already exists. Choose a unique name.") from None
297+
self.controls.append(crl)
298+
299+
def extend(self, crls: tuple[Control, ...]):
300+
for crl in crls:
301+
self.append(crl)
302+
303+
def __getitem__(self, ident: int | str):
304+
"""Get the control object identified by ident (index within .controls or valid name)."""
305+
if isinstance(ident, str):
306+
for crl in self.controls:
307+
if crl.name == ident:
308+
return crl
309+
raise KeyError(f"Control with name {ident} not found.") from None
310+
elif isinstance(ident, int):
311+
if ident < 0 or ident >= len(self.controls):
312+
raise ValueError(f"Control {ident} does not exist within set of controls.") from None
313+
return self.controls[ident]
314+
else:
315+
raise TypeError(f"Integer expected as subscript. Found {ident}") from None
316+
317+
def step(self, time: float, dt: float):
318+
"""Step towards the goals (if goals are set)."""
319+
if not self.nogoals:
320+
for crl in self.controls:
321+
crl.step(time, dt)

tests/test_controls.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
from functools import partial
3+
from typing import Sequence
34

45
import matplotlib.pyplot as plt
56
import numpy as np
@@ -71,7 +72,17 @@ def __call__(self, v: float | None = None) -> float:
7172

7273

7374
def test_goal(show: bool = False):
74-
def do_goal(order: int, value: float, current: np.ndarray | None = None, t_end: float = 10.0):
75+
def do_goal(
76+
order: int,
77+
value: float,
78+
current: np.ndarray | None = None,
79+
t_end: float = 10.0,
80+
change: tuple[float, int, float]|None = None,
81+
):
82+
if change is not None:
83+
t1, order1, val1 = change
84+
else:
85+
t1 = float("inf")
7586
time = 0.0
7687
if current is not None:
7788
_b[0].rw(current[0])
@@ -83,6 +94,8 @@ def do_goal(order: int, value: float, current: np.ndarray | None = None, t_end:
8394
assert len(_b[0].goal)
8495
res.append((time, _b[0].rw(), _b[0].speed, _b[0].acc))
8596
while time + dt < t_end:
97+
if change is not None and abs(time - t1) < dt / 2:
98+
_b["len"].setgoal(order1, val1)
8699
_b.step(time, dt)
87100
time += dt
88101
res.append((time, *_b[0].current))
@@ -99,6 +112,12 @@ def boom_setter(newval: float | None = None, idx: int = 0):
99112
boom[idx] = newval
100113
return newval
101114

115+
def i_time(res: Sequence[tuple[float, ...]], time: float, eps: float = 0.0001):
116+
for i in range(len(res)):
117+
if abs(res[i][0] - time) < eps:
118+
return i
119+
raise KeyError(f"Time {time} not found in table")
120+
102121
_b = Controls(limit_err=logging.CRITICAL)
103122
_b.append(Control("len", ((-100.0, 90.0), None, (-1.0, 0.5)), partial(boom_setter, idx=0)))
104123
# fixed velocity and the acceleration limits are not needed:
@@ -131,7 +150,15 @@ def boom_setter(newval: float | None = None, idx: int = 0):
131150
# set an acceleration (non-zero position and velocity)
132151
res = do_goal(2, -0.1, current=np.array((10.0, 1.0, 0.0), float), t_end=2.01)
133152
expected = (10 + 1.0 * 2.0 - 0.5 * 0.1 * 2.0**2, 1.0 - 0.1 * 2.0, -0.1)
134-
assert np.allclose(_b[0].current, expected), f"{_b[0].current} != {expected} "
153+
assert np.allclose(_b[0].current, expected), f"{_b[0].current} != {expected}."
154+
155+
# set a speed and overwrite it after a time with a new value
156+
res = do_goal(1, 0.45, current=np.array((10.0, 0, 0), float), t_end=10.0, change=(5.0, 1, 0.0))
157+
i_acc = i_time(res, 0.45 / 0.5)
158+
assert np.allclose(res[i_acc], (0.45 / 0.5, 10 + 0.5 * 0.5 * (0.45 / 0.5) ** 2, 0.45, 0.5)), (
159+
f"After acc.: {res[i_acc]}"
160+
)
161+
assert np.allclose(res[-1][1:], (12.14875, 0, 0)), f"Found end state {res[-1]}"
135162

136163
Controls.limit_err = logging.WARNING # allow corrections from now on
137164

0 commit comments

Comments
 (0)