Repository: python-control/python-control

Issue:
`solve_flat_ocp` and scalar `timepts`
flowing from gh-1101, which complains about the code in question,

`solve_flat_ocp` argument `timepts` is advertised in the docstring as being allowed to be scalar:

```
    timepts : float or 1D array_like
        The list of points for evaluating cost and constraints, as well as
        the time horizon.  If given as a float, indicates the final time for
        the trajectory (corresponding to xf)
```

but this doesn't work:

```python
import numpy as np
import control as ct
import control.flatsys as fs

f = fs.LinearFlatSystem(ct.ss(ct.tf([1],[1,1])))

def terminal_cost(x, u):
    return (x-5).dot(x-5)+u.dot(u)

traj1 = fs.solve_flat_ocp(f, [0, 1], x0=[23],
                          terminal_cost=terminal_cost)

traj2 = fs.solve_flat_ocp(f, 1, x0=[23],
                          terminal_cost=terminal_cost)

teval = np.linspace(0,1,101)

r1 = traj1.response(teval)
r2 = traj2.response(teval)

assert np.max(abs(r1.x-r2.x)) == 0
assert np.max(abs(r1.u-r2.u)) == 0
assert np.max(abs(r1.y-r2.y)) == 0
```

produces with master @ ebff1259

```
Traceback (most recent call last):
  File "/home/rory/projects/pycontrol/pyflakes/flatbug2.py", line 13, in <module>
    traj2 = fs.solve_flat_ocp(f, 1, x0=[23],
                              terminal_cost=terminal_cost)
  File "/home/rory/src/python-control/control/flatsys/flatsys.py", line 725, in solve_flat_ocp
    T0 = timepts[0] if len(timepts) > 1 else T0
                                             ^^
UnboundLocalError: cannot access local variable 'T0' where it is not associated with a value
```

I think the fix is as below. Diff also removes `Tf`, which is not used in the function.  With this diff the test script runs with error.

```diff
@@ -721,8 +721,7 @@ def solve_flat_ocp(
 
     # Process final time
     timepts = np.atleast_1d(timepts)
-    Tf = timepts[-1]
-    T0 = timepts[0] if len(timepts) > 1 else T0
+    T0 = timepts[0] if len(timepts) > 1 else 0
 
     # Process keyword arguments
     if trajectory_constraints is None:
```



Discussion:

Patch:
diff --git a/control/flatsys/flatsys.py b/control/flatsys/flatsys.py
index 7d76b9d78..5818d118b 100644
--- a/control/flatsys/flatsys.py
+++ b/control/flatsys/flatsys.py
@@ -721,8 +721,7 @@ def solve_flat_ocp(
 
     # Process final time
     timepts = np.atleast_1d(timepts)
-    Tf = timepts[-1]
-    T0 = timepts[0] if len(timepts) > 1 else T0
+    T0 = timepts[0] if len(timepts) > 1 else 0
 
     # Process keyword arguments
     if trajectory_constraints is None:


Test patch:
diff --git a/control/tests/flatsys_test.py b/control/tests/flatsys_test.py
index a12bf1480..5b66edaf5 100644
--- a/control/tests/flatsys_test.py
+++ b/control/tests/flatsys_test.py
@@ -452,6 +452,29 @@ def test_flat_solve_ocp(self, basis):
         np.testing.assert_almost_equal(x_const, x_nlconst)
         np.testing.assert_almost_equal(u_const, u_nlconst)
 
+    def test_solve_flat_ocp_scalar_timepts(self):
+        # scalar timepts gives expected result
+        f = fs.LinearFlatSystem(ct.ss(ct.tf([1],[1,1])))
+
+        def terminal_cost(x, u):
+            return (x-5).dot(x-5)+u.dot(u)
+
+        traj1 = fs.solve_flat_ocp(f, [0, 1], x0=[23],
+                                  terminal_cost=terminal_cost)
+
+        traj2 = fs.solve_flat_ocp(f, 1, x0=[23],
+                                  terminal_cost=terminal_cost)
+
+        teval = np.linspace(0, 1, 101)
+
+        r1 = traj1.response(teval)
+        r2 = traj2.response(teval)
+
+        np.testing.assert_array_equal(r1.x, r2.x)
+        np.testing.assert_array_equal(r1.y, r2.y)
+        np.testing.assert_array_equal(r1.u, r2.u)
+
+
     def test_bezier_basis(self):
         bezier = fs.BezierFamily(4)
         time = np.linspace(0, 1, 100)