libminizinc icon indicating copy to clipboard operation
libminizinc copied to clipboard

Flattening of count_{leq, lt, geq, gt} is broken

Open informarte opened this issue 3 years ago • 3 comments

The flattening of count_{leq, lt, geq, gt} is broken. For example:

include "count.mzn";

array [1..3] of var 1..10: x;
var 1..10: y;
var 1..3: c;

constraint count(x, y) <= c;

gets compiled by MiniZinc 2.6.4 to

predicate int_lin_ne_imp(array [int] of int: as,array [int] of var int: bs,int: c,var bool: r);
array [1..2] of int: X_INTRODUCED_3_ = [1,-1];
array [1..4] of int: X_INTRODUCED_14_ = [-1,-1,-1,-1];
var 1..10: X_INTRODUCED_0_;
var 1..10: X_INTRODUCED_1_;
var 1..10: X_INTRODUCED_2_;
var 1..10: y:: output_var;
var 1..3: c:: output_var;
var bool: X_INTRODUCED_4_ ::var_is_introduced :: is_defined_var;
var bool: X_INTRODUCED_5_ ::var_is_introduced :: is_defined_var;
var bool: X_INTRODUCED_6_ ::var_is_introduced :: is_defined_var;
var 0..1: X_INTRODUCED_8_ ::var_is_introduced :: is_defined_var;
var 0..1: X_INTRODUCED_9_ ::var_is_introduced :: is_defined_var;
var 0..1: X_INTRODUCED_10_ ::var_is_introduced :: is_defined_var;
array [1..3] of var int: x:: output_array([1..3]) = [X_INTRODUCED_0_,X_INTRODUCED_1_,X_INTRODUCED_2_];
constraint int_lin_le(X_INTRODUCED_14_,[X_INTRODUCED_8_,X_INTRODUCED_9_,X_INTRODUCED_10_,c],-3);
constraint int_lin_ne_imp(X_INTRODUCED_3_,[X_INTRODUCED_0_,y],0,X_INTRODUCED_4_):: defines_var(X_INTRODUCED_4_);
constraint int_lin_ne_imp(X_INTRODUCED_3_,[X_INTRODUCED_1_,y],0,X_INTRODUCED_5_):: defines_var(X_INTRODUCED_5_);
constraint int_lin_ne_imp(X_INTRODUCED_3_,[X_INTRODUCED_2_,y],0,X_INTRODUCED_6_):: defines_var(X_INTRODUCED_6_);
constraint bool2int(X_INTRODUCED_4_,X_INTRODUCED_8_):: defines_var(X_INTRODUCED_8_);
constraint bool2int(X_INTRODUCED_5_,X_INTRODUCED_9_):: defines_var(X_INTRODUCED_9_);
constraint bool2int(X_INTRODUCED_6_,X_INTRODUCED_10_):: defines_var(X_INTRODUCED_10_);
solve  satisfy;

Expected output (produced by MiniZinc 2.5.5):

predicate fzn_count_eq(array [int] of var int: x,var int: y,var int: c);
array [1..2] of int: X_INTRODUCED_4_ = [1,-1];
var 1..10: X_INTRODUCED_0_;
var 1..10: X_INTRODUCED_1_;
var 1..10: X_INTRODUCED_2_;
var 1..10: y:: output_var;
var 1..3: c:: output_var;
var 0..3: X_INTRODUCED_3_ ::var_is_introduced :: is_defined_var;
array [1..3] of var int: x:: output_array([1..3]) = [X_INTRODUCED_0_,X_INTRODUCED_1_,X_INTRODUCED_2_];
constraint fzn_count_eq(x,y,X_INTRODUCED_3_):: defines_var(X_INTRODUCED_3_);
constraint int_lin_le(X_INTRODUCED_4_,[X_INTRODUCED_3_,c],0);
solve  satisfy;

I think the issue was introduced by this commit:

commit ce350a29a6208bfd22655ea3771a13abc1a84a73
Author: Jip J. Dekker <[email protected]>
Date:   Thu Feb 18 14:38:22 2021 +1100

    Add initial promised context annotations

diff --git a/share/minizinc/std/fzn_count_leq.mzn b/share/minizinc/std/fzn_count_leq.mzn
index b72b435f..1d06518c 100644
--- a/share/minizinc/std/fzn_count_leq.mzn
+++ b/share/minizinc/std/fzn_count_leq.mzn
@@ -1,7 +1,7 @@
 include "count_fn.mzn";
 
 predicate fzn_count_leq(array[int] of var int: x, var int: y, var int: c) =
-  let { var int: z = count(x,y) } in z >= c;
+  let { var int: z ::promise_ctx_monotone = count(xi in x)(xi == y); } in z >= c;
   % This needs to be written with a let rather than count(x,y) >= c
   % so that the automatic rewriting of the latter doesn't kick in

informarte avatar Aug 09 '22 18:08 informarte

Thanks for finding this. This might actually affect the outcome of this years competition...

MaxOstrowski avatar Aug 09 '22 22:08 MaxOstrowski

Looking at the actual call trace it appear you have located the right commit, but the wrong predicate. fzn_count_leq seems to function fine.

It is fzn_count_geq that this model get rewritten to, and there we have made a more drastic change. Since the leq and geq versions of the count predicates never have to ensure an actual count, the fzn_count_geq was changed to push negations inward, to improve the number of positive context. It now thus counts how many values are not equal to y rather than the other way around:

  % TODO: This is probably the better way to do this, but still always gives
  % reifications (instead of half-reifications)
  % let {
  %   var int: z ::promise_ctx_antitone = count(xi in x)(xi == y);
  % } in z <= c;
  let {
    var int: z ::promise_ctx_monotone = count(xi in x)(xi != y);
  } in z >= (length(x) - c);

Looking at my TODO this was always meant to be a temporary workaround, that wasn't expected to have any real negative impact (and a positive impact on all solvers supporting half-reified equality predicates). When a fzn_count_eq predicate is available, it does of course seem like a much better idea to ensure that they get used.

I will look into this issue further to try and find the best solution.

Dekker1 avatar Aug 10 '22 00:08 Dekker1

Ah, you are right, the comparison in my example translates to fzn_count_geq.

Still, the other variants of count are broken, too. For example,

include "count.mzn";

array [1..3] of var 1..10: x;
var 1..10: y;
var 1..3: c;

constraint count(x, y) >= c;

compiles to:

array [1..4] of int: X_INTRODUCED_11_ = [-1,-1,-1,1];
var 1..10: X_INTRODUCED_0_;
var 1..10: X_INTRODUCED_1_;
var 1..10: X_INTRODUCED_2_;
var 1..10: y:: output_var;
var 1..3: c:: output_var;
var bool: X_INTRODUCED_3_ ::var_is_introduced :: is_defined_var;
var bool: X_INTRODUCED_4_ ::var_is_introduced :: is_defined_var;
var bool: X_INTRODUCED_5_ ::var_is_introduced :: is_defined_var;
var 0..1: X_INTRODUCED_7_ ::var_is_introduced :: is_defined_var;
var 0..1: X_INTRODUCED_8_ ::var_is_introduced :: is_defined_var;
var 0..1: X_INTRODUCED_9_ ::var_is_introduced :: is_defined_var;
array [1..3] of var int: x:: output_array([1..3]) = [X_INTRODUCED_0_,X_INTRODUCED_1_,X_INTRODUCED_2_];
constraint int_lin_le(X_INTRODUCED_11_,[X_INTRODUCED_7_,X_INTRODUCED_8_,X_INTRODUCED_9_,c],0);
constraint int_eq_reif(X_INTRODUCED_0_,y,X_INTRODUCED_3_):: defines_var(X_INTRODUCED_3_);
constraint int_eq_reif(X_INTRODUCED_1_,y,X_INTRODUCED_4_):: defines_var(X_INTRODUCED_4_);
constraint int_eq_reif(X_INTRODUCED_2_,y,X_INTRODUCED_5_):: defines_var(X_INTRODUCED_5_);
constraint bool2int(X_INTRODUCED_3_,X_INTRODUCED_7_):: defines_var(X_INTRODUCED_7_);
constraint bool2int(X_INTRODUCED_4_,X_INTRODUCED_8_):: defines_var(X_INTRODUCED_8_);
constraint bool2int(X_INTRODUCED_5_,X_INTRODUCED_9_):: defines_var(X_INTRODUCED_9_);
solve  satisfy;

In 2.6.4, fzn_count_leq is defined as follows:

predicate fzn_count_leq(array[int] of var int: x, var int: y, var int: c) =
  let { var int: z ::promise_ctx_monotone = count(xi in x)(xi == y); } in z >= c;
  % This needs to be written with a let rather than count(x,y) >= c
  % so that the automatic rewriting of the latter doesn't kick in

So it seems that the expected rewriting from var int: z ::promise_ctx_monotone = count(xi in x)(xi == y); to var int: z ::promise_ctx_monotone = count(x, y) (which should eventually lead to fzn_count_eq(x, y, z) :: defines_var(z)) does not work or, as the comments suggests, is not intended to happen.

informarte avatar Aug 10 '22 07:08 informarte