From fad36cab2ab566951e6718dd9685f2114ad319b8 Mon Sep 17 00:00:00 2001
From: "Hsieh, Chiao" <chsieh16@illinois.edu>
Date: Sun, 16 Oct 2022 22:55:40 -0500
Subject: [PATCH] Add AgBotStanley teacher (not tested)

---
 agbot_stanley_teacher.py | 213 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 213 insertions(+)
 create mode 100644 agbot_stanley_teacher.py

diff --git a/agbot_stanley_teacher.py b/agbot_stanley_teacher.py
new file mode 100644
index 0000000..4343f9a
--- /dev/null
+++ b/agbot_stanley_teacher.py
@@ -0,0 +1,213 @@
+import gurobipy as gp
+import numpy as np
+
+from dtree_teacher_base import DTreeGurobiTeacherBase
+
+
+K_P = 0.1
+FORWARD_VEL = 1.0  # m/s
+CYCLE_TIME = 0.05  # s
+
+ANG_VEL_LIM = 0.5  # rad/s^2
+
+# Limits on unconstrained variables to avoid overflow and angle normalization
+ANG_LIM = np.pi / 2  # radian
+CTE_LIM = 0.76 * 0.5  # meter
+
+
+# Bounds for intermediate variables.
+# These are not necessary but can improve efficiency
+K_CTE_V_LIM = K_P * CTE_LIM / FORWARD_VEL
+ATAN_K_CTE_V_LIM = np.arctan(K_CTE_V_LIM)
+RAW_ANG_ERR_LIM = (ANG_LIM + ATAN_K_CTE_V_LIM)
+RAW_ANG_VEL_LIM = RAW_ANG_ERR_LIM / CYCLE_TIME
+
+NEW_K_CTE_V_LIM = K_CTE_V_LIM + FORWARD_VEL * CYCLE_TIME * 1.0
+NEW_ATAN_K_CTE_V_LIM = np.arctan(NEW_K_CTE_V_LIM)
+NEW_RAW_ANG_ERR_LIM = ANG_LIM + FORWARD_VEL * CYCLE_TIME
+
+
+def sensor(state):
+    """ Assuming the lane to track is aligned with x-axis (i.e., y==0 and yaw==0)
+        Hence, heading = 0-yaw = -yaw and distance = 0-y = -y."""
+    x, y, yaw = state
+    # TODO approximation instead of perfect perception
+    prcv_heading = -yaw
+    prcv_distance = -y
+    return prcv_heading, prcv_distance
+
+
+def controller(epsilon):
+    prcv_heading, prcv_distance = epsilon
+    error = prcv_heading + np.arctan2(K_P*prcv_distance, FORWARD_VEL)
+
+    # Calculate controller output
+    ang_vel = error / CYCLE_TIME
+    if ang_vel > ANG_VEL_LIM:
+        ang_vel = ANG_VEL_LIM
+    elif ang_vel < -ANG_VEL_LIM:
+        ang_vel = -ANG_VEL_LIM
+
+    # Return actuator values
+    return (ang_vel,)
+
+
+def dynamics(old_state, ang_vel):
+    """ This dynamics for state variables x, y, yaw
+
+        x[n+1] = x[n] + v*cos(yaw)*CYCLE_TIME
+        y[n+1] = y[n] + v*sin(yaw)*CYCLE_TIME
+        yaw[n+1] = yaw[n] + ang_vel*CYCLE_TIME
+    """
+    old_x, old_y, old_yaw = old_state
+    new_x = old_x + FORWARD_VEL * np.cos(old_yaw) * CYCLE_TIME
+    new_y = old_y + FORWARD_VEL * np.sin(old_yaw) * CYCLE_TIME
+    new_yaw = old_yaw + ang_vel*CYCLE_TIME
+    return new_x, new_y, new_yaw
+
+
+class DTreeAgBotStanleyGurobiTeacher(DTreeGurobiTeacherBase):
+    # Ideal perception as a linear transform from state to ground truth percept
+    PERC_GT = np.array([[0., -1., 0.],
+                        [0., 0., -1.]], float)
+
+    def __init__(self, name="agbot_stanley", norm_ord=2, ultimate_bound: float=0.0) -> None:
+        assert ultimate_bound >= 0.0
+        self._ultimate_bound = ultimate_bound
+
+        super().__init__(name=name,
+                         state_dim=3, perc_dim=2, ctrl_dim=1, norm_ord=norm_ord)
+
+    def is_positive_example(self, ex) -> bool:
+        def v(cte, psi) -> float:
+            return np.linalg.norm([cte, psi], ord=float(self._norm_ord))
+
+        def spec(x, y, theta, cte, psi) -> bool:
+            m_star = sensor
+            f = dynamics
+            g = controller
+            v_old = v(*m_star(x, y, theta))
+            v_new = v(*m_star(*f(x, y, theta, *g(cte, psi))))
+            return v_new <= max(v_old, self._ultimate_bound)
+        return spec(*ex)
+
+    def _add_system(self) -> None:
+        # Bounds on all domains
+        self._old_state.lb = (-np.inf, -CTE_LIM, -ANG_LIM)
+        self._old_state.ub = (np.inf, CTE_LIM, ANG_LIM)
+        self._percept.lb = (-CTE_LIM, -ANG_LIM)
+        self._percept.ub = (CTE_LIM, ANG_LIM)
+        self._control.lb = (-ANG_VEL_LIM,)
+        self._control.ub = (ANG_VEL_LIM,)
+
+        # Variable Aliases
+        m = self._gp_model
+        old_x, old_y, old_yaw = self._old_state.tolist()
+        new_x, new_y, new_yaw = self._new_state.tolist()
+        cte, psi = self._percept.tolist()
+        ang_vel, = self._control.tolist()
+
+        # Controller
+        K_cte_V = m.addVar(name="K*dhat/Vf", lb=-K_CTE_V_LIM, ub=K_CTE_V_LIM)
+        m.addConstr(K_cte_V == K_P*cte/FORWARD_VEL)
+        atan_K_cte_V = m.addVar(name="atan(K*dhat/Vf)",
+                                lb=-ATAN_K_CTE_V_LIM, ub=ATAN_K_CTE_V_LIM)
+        m.addGenConstrTan(xvar=atan_K_cte_V, yvar=K_cte_V)
+
+        # Clip angular velocity
+        raw_ang_vel = m.addVar(name="(φhat+atan(K*dhat/V))/T",
+                               lb=-RAW_ANG_VEL_LIM, ub=RAW_ANG_VEL_LIM)
+        m.addConstr(raw_ang_vel == (psi + atan_K_cte_V) / CYCLE_TIME)
+
+        ang_vel = m.addVar(name="ω", lb=-ANG_VEL_LIM, ub=ANG_VEL_LIM)
+        m.addGenConstrPWL(name="clip", xvar=raw_ang_vel, yvar=ang_vel,
+                          xpts=[-RAW_ANG_VEL_LIM, -ANG_VEL_LIM, ANG_VEL_LIM, RAW_ANG_VEL_LIM],
+                          ypts=[-ANG_VEL_LIM, -ANG_VEL_LIM, ANG_VEL_LIM, ANG_VEL_LIM])
+
+        # Dynamics
+        cos_yaw = m.addVar(name="cosθ", **self.TRIGVAR)
+        m.addGenConstrCos(old_yaw, cos_yaw)
+        sin_yaw = m.addVar(name="sinθ", **self.TRIGVAR)
+        m.addGenConstrSin(old_yaw, sin_yaw)
+
+        new_x = old_x + FORWARD_VEL * CYCLE_TIME * cos_yaw
+        new_y = m.addVar(name="y'", **self.FREEVAR)
+        m.addConstr(new_y == old_y + FORWARD_VEL * CYCLE_TIME * sin_yaw)
+        new_yaw = m.addVar(name="θ'", **self.FREEVAR)
+        m.addConstr(new_yaw == old_yaw + ang_vel * CYCLE_TIME)
+        m.update()
+    
+    def _add_unsafe(self) -> None:
+        assert self.PERC_GT.shape == (self.perc_dim, self.state_dim)
+        # Variable Aliases
+        m = self._gp_model
+
+        # Add nonincreasing constraints
+        old_truth = m.addMVar(shape=(self.perc_dim,), name="m(x)", **self.FREEVAR)
+        m.addConstr(old_truth == self.PERC_GT @ self._old_state)
+
+        old_cte, old_psi = old_truth.tolist()
+        K_old_cte_V = m.addVar(name="K*d/Vf", lb=-K_CTE_V_LIM, ub=K_CTE_V_LIM)
+        m.addConstr(K_old_cte_V == K_P * old_cte / FORWARD_VEL)
+        atan_K_old_cte_V = m.addVar(name="atan(K*d/Vf)", lb=-ATAN_K_CTE_V_LIM, ub=ATAN_K_CTE_V_LIM)
+        m.addGenConstrTan(xvar=atan_K_old_cte_V, yvar=K_old_cte_V)
+        old_err = m.addVar(name="ψ+atan(K*d/Vf)", **self.FREEVAR)
+        m.addConstr(old_err == old_psi + atan_K_old_cte_V)
+
+        old_lya_val = m.addVar(name="V(m(x))", **self.NNEGVAR)
+        m.addConstr(old_lya_val == gp.abs_(old_err))
+
+        # TODO What is the Lyapunov function?
+        # Add ultimate bound
+        if self._ultimate_bound > 0:
+            bound_new_lya_val = m.addVar(name=f"max(|m(x)|,{self._ultimate_bound})",
+                                         **self.NNEGVAR)
+            m.addConstr(bound_new_lya_val == gp.max_(old_lya_val, constant=self._ultimate_bound))
+        else:
+            bound_new_lya_val = old_lya_val
+
+        new_truth = m.addMVar(shape=(self.perc_dim,), name="m(x')", **self.FREEVAR)
+        m.addConstr(new_truth == self.PERC_GT @ self._new_state)
+
+        new_cte, new_psi = new_truth.tolist()
+        K_new_cte_V = m.addVar(name="K*d'/Vf", lb=-K_CTE_V_LIM, ub=K_CTE_V_LIM)
+        m.addConstr(K_new_cte_V == K_P * new_cte / FORWARD_VEL)
+        atan_K_new_cte_V = m.addVar(name="atan(K*d'/Vf)", lb=-ATAN_K_CTE_V_LIM, ub=ATAN_K_CTE_V_LIM)
+        m.addGenConstrTan(xvar=atan_K_new_cte_V, yvar=K_new_cte_V)
+        new_err = m.addVar(name="ψ'+atan(K*d'/Vf)", **self.FREEVAR)
+        m.addConstr(new_err == new_psi + atan_K_new_cte_V)
+        new_lya_val = m.addVar(name="V(m(x'))", **self.NNEGVAR)
+        m.addConstr(new_lya_val == gp.abs_(new_err))
+
+        # Negation of stability spec
+        m.addConstr(new_lya_val >= bound_new_lya_val, name="Increasing Error")  # Tracking error is increasing
+        m.update()
+
+
+    def _add_objective(self) -> None:
+        # Variable Aliases
+        m = self._gp_model
+        x = self._old_state
+        z = self._percept
+        z_diff = self._percept_diff
+
+        # Add objective
+        norm_var = m.addVar(name="|z-m(x)|", **self.NNEGVAR)
+        m.addConstr(z_diff == z - self.PERC_GT @ x)
+        m.addConstr(norm_var == gp.norm(z_diff, float(self._norm_ord)))
+
+        m.setObjective(norm_var, gp.GRB.MINIMIZE)
+        m.update()
+
+
+def test_gem_stanley_gurobi_teacher():
+    teacher = DTreeAgBotStanleyGurobiTeacher(ultimate_bound=.0)
+    teacher.set_old_state_bound(
+        lb=(-np.inf, 0.5, 0.0625),
+        ub=(np.inf, 1.0, 0.25)
+    )
+    teacher.dump_system_encoding()
+
+
+if __name__ == "__main__":
+    test_gem_stanley_gurobi_teacher()
-- 
GitLab