From 8cc8f2aa089fb1253926b4f561f6b002db5260ee Mon Sep 17 00:00:00 2001
From: "Hsieh, Chiao" <chsieh16@illinois.edu>
Date: Mon, 29 Nov 2021 23:40:50 -0600
Subject: [PATCH] Support selecting different shapes in both teacher and
 learner

---
 firstpass_learner.py | 40 +++++++++++++++++++++++-----------------
 firstpass_teacher.py | 42 +++++++++++++++++++++++++++++++++---------
 2 files changed, 56 insertions(+), 26 deletions(-)

diff --git a/firstpass_learner.py b/firstpass_learner.py
index 9877be7..395d296 100644
--- a/firstpass_learner.py
+++ b/firstpass_learner.py
@@ -18,13 +18,13 @@ def affine_transform(state_vars: Sequence[z3.ExprRef], perc_vars: Sequence[z3.Ex
 def abs_expr(expr: z3.ExprRef) -> z3.ExprRef:
     return z3.If(expr >= 0, expr, -expr)
 
-def l1_norm(*exprs) -> z3.ExprRef:
+def l1_norm(*exprs: z3.ExprRef) -> z3.ExprRef:
     return z3.Sum(*(abs_expr(expr) for expr in exprs))
 
-def l2_norm(*exprs) -> z3.ExprRef:
+def l2_norm(*exprs: z3.ExprRef) -> z3.ExprRef:
     return z3.Sum(*(expr*expr for expr in exprs))
 
-def max_expr(*exprs) -> z3.ExprRef:
+def max_expr(*exprs: z3.ExprRef) -> z3.ExprRef:
     m = exprs[0]
     for v in exprs[1:]:
         m = z3.If(m >= v, m, v)
@@ -58,15 +58,14 @@ class FirstpassLearner(Z3LearnerBase):
         intercept_arr = np.array(z3.RealVector("b%d" % idx, self.perc_dim))
         radius = z3.Real("r%d" % idx)
 
-        self._part_affine_map[idx] = (coeff_arr, intercept_arr, radius)
+        self._part_affine_map[idx] = (shape_sel, coeff_arr, intercept_arr, radius)
 
         transformed_perc_seq = affine_transform(self._state_vars, self._percept_vars, coeff_arr, intercept_arr)
         return z3.And(
-            l2_norm(*transformed_perc_seq)  <= radius,
-            # z3.AtLeast(*shape_sel, 1),
-            # z3.Implies(shape_sel[0], l1_norm(*transformed_perc_seq)  <= radius),
-            # z3.Implies(shape_sel[1], l2_norm(*transformed_perc_seq)  <= radius),
-            # z3.Implies(shape_sel[2], loo_norm(*transformed_perc_seq) <= radius),
+            z3.AtLeast(*shape_sel, 1),
+            z3.Implies(shape_sel[0], l1_norm(*transformed_perc_seq)  <= radius),
+            z3.Implies(shape_sel[1], l2_norm(*transformed_perc_seq)  <= radius),
+            z3.Implies(shape_sel[2], loo_norm(*transformed_perc_seq) <= radius),
         )
         
     def add_positive_examples(self, *vals) -> None:
@@ -89,12 +88,13 @@ class FirstpassLearner(Z3LearnerBase):
     def learn(self) -> Optional[Tuple]:
         res = self._solver.check()
         if res == z3.sat:
-            z3_coeff_arr, z3_intercept_arr, z3_radius = self._part_affine_map[0]  # FIXME
+            z3_shape_sel, z3_coeff_arr, z3_intercept_arr, z3_radius = self._part_affine_map[0]  # FIXME
             m = self._solver.model()
-            coeff_arr = np.vectorize(extract_from_z3_model(m))(z3_coeff_arr)
-            intercept_arr = np.vectorize(extract_from_z3_model(m))(z3_intercept_arr)
-            radius = extract_from_z3_model(m)(z3_radius)
-            return coeff_arr, intercept_arr, radius
+            shape_sel = extract_shape_from_z3_model(m, z3_shape_sel)  # TODO: return integer is not that good
+            coeff_arr = np.vectorize(extract_real_from_z3_model(m))(z3_coeff_arr)
+            intercept_arr = np.vectorize(extract_real_from_z3_model(m))(z3_intercept_arr)
+            radius = extract_real_from_z3_model(m)(z3_radius)
+            return shape_sel, coeff_arr, intercept_arr, radius
         elif res == z3.unsat:
             print("(unsat)")
             return None
@@ -103,8 +103,14 @@ class FirstpassLearner(Z3LearnerBase):
             return None
 
 
-def extract_from_z3_model(m: z3.ModelRef):
-    def to_float(var):
+def extract_shape_from_z3_model(m: z3.ModelRef, vars: Sequence[z3.ExprRef]) -> int:
+    for i, var in enumerate(vars):
+        if m[var]:
+            return i
+
+
+def extract_real_from_z3_model(m: z3.ModelRef):
+    def to_float(var: z3.ExprRef) -> float:
         x2 = m[var].as_fraction()
         return float(x2.numerator) / float(x2.denominator)
     return to_float
@@ -145,7 +151,7 @@ def test_firstpass_learner():
         (9.0, 9.0, 5.0, 5.0),
     ]
     learner.add_negative_examples(*neg_examples)
-    print(learner._solver.assertions())
+    # print(learner._solver.assertions())
     print(learner.learn())
 
 if __name__ == "__main__":
diff --git a/firstpass_teacher.py b/firstpass_teacher.py
index 17d4403..45c3ea7 100755
--- a/firstpass_teacher.py
+++ b/firstpass_teacher.py
@@ -25,7 +25,7 @@ class FirstpassTeacher(GurobiTeacherBase):
         assert len(old_state) == len(new_state)
         m = self._gp_model
         m.update()
-        old_V, new_V = 0, 0
+        old_V, new_V = 0.0, 0.0
         for old_xi, new_xi in zip(old_state, new_state):
             old_var_name = "|%s|" % old_xi.VarName
             old_abs_xi = m.addVar(name=old_var_name,vtype=GRB.CONTINUOUS, lb=0, ub=np.inf)
@@ -40,7 +40,7 @@ class FirstpassTeacher(GurobiTeacherBase):
 
     def _add_candidate(self, candidate) -> None:
         # TODO parse candidate
-        coeff, intercept, radius = candidate
+        shape_sel, coeff, intercept, radius = candidate
         assert coeff.shape == (self._percept.shape[0], self._old_state.shape[0])
         assert intercept.shape == self._percept.shape
 
@@ -48,32 +48,56 @@ class FirstpassTeacher(GurobiTeacherBase):
         x = self._old_state
         z = self._percept
         # Constraints on values of xP and yP
-        z_diff = m.addMVar(name="||z-(Ax+b)||", shape=len(z.tolist()), vtype=GRB.CONTINUOUS, lb=-np.inf, ub=np.inf)
+        z_diff = m.addMVar(name="z-(Ax+b)", shape=len(z.tolist()), vtype=GRB.CONTINUOUS, lb=-np.inf, ub=np.inf)
         m.addConstr(z_diff == z - (coeff @ x + intercept))
-        m.addConstr(z_diff @ z_diff <= radius*radius, "z in M(x)")
+        m.update()
 
-        # L2-norm objective
-        m.setObjective(z_diff @ z_diff, GRB.MINIMIZE)
+        if shape_sel == 0:
+            z_dist = 0.0
+            for zi in z_diff.tolist():
+                abs_zi_name = "|%s|" % zi.VarName
+                abs_zi = m.addVar(name=abs_zi_name, vtype=GRB.CONTINUOUS, lb=0, ub=np.inf)
+                m.addConstr(abs_zi == gp.abs_(zi))
+                z_dist += abs_zi
+            m.addConstr(z_dist <= radius, "||z-(Ax+b)||_1 <= r")
+            # L1-norm objective
+            m.setObjective(z_dist, GRB.MINIMIZE)
+        elif shape_sel == 1:
+            m.addConstr(z_diff @ z_diff <= radius*radius, "||z-(Ax+b)||^2 <= r^2")
+            # L2-norm objective
+            m.setObjective(z_diff @ z_diff, GRB.MINIMIZE)
+        elif shape_sel == 2:
+            abs_zi_list = []
+            for zi in z_diff.tolist():
+                abs_zi_name = "|%s|" % zi.VarName
+                abs_zi = m.addVar(name=abs_zi_name, vtype=GRB.CONTINUOUS, lb=0, ub=np.inf)
+                m.addConstr(abs_zi == gp.abs_(zi))
+                abs_zi_list.append(abs_zi)
+            z_dist = m.addVar(name="||z-(Ax+b)||_oo", vtype=GRB.CONTINUOUS, lb=0, ub=np.inf)
+            m.addConstr(z_dist == gp.max_(abs_zi_list))
+            m.addConstr(z_dist <= radius, "||z-(Ax+b)||_oo <= r")
+
+            m.setObjective(z_dist, GRB.MINIMIZE)
 
 
 CAND0_A = np.array([[0.0, -1.0],
                     [0.0,  1.0]])
 CAND0_b = np.array([0.0, 0.0])
 CAND0_r = 2.0
-CAND0 = (CAND0_A, CAND0_b, CAND0_r)
+CAND0 = (2, CAND0_A, CAND0_b, CAND0_r)
 
 CAND1_A = np.array([[1.0, 0.0],
                    [0.0, 1.0]])
 CAND1_b = np.array([0.0, 0.0])
 CAND1_r = 2.0
-CAND1 = (CAND1_A, CAND1_b, CAND1_r)
+CAND1 = (1, CAND1_A, CAND1_b, CAND1_r)
 
 
 def test_firstpass_teacher():
     teacher = FirstpassTeacher()
     teacher.set_old_state_bound(lb=[6.0, 5.0], ub=[11.0, 10.0])
-    teacher.dump_model()
     result = teacher.check(CAND0)
+    teacher.dump_model()
     print(result)
     if result == z3.sat:
         print(teacher.model())
-- 
GitLab