File size: 8,577 Bytes
0da3abc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
"""Claim 2 (Theorem 2 / Theorem 4): given a polynomial-time (alpha, gamma)-
decomposition, the ICE algorithm achieves
    O(alpha) * rho(eta, .) + O(alpha * log^{-1}(gamma/(gamma-1)) * log k).

Definition 1 (from the source): an (alpha,gamma)-decomposition is a pair
(X_i, S_i) with Xhat = X_1 u ... u X_r such that for every i
  (A) |X_i| >= (|Xhat| - (|X_1|+...+|X_{i-1}|)) / 2
  (B) if c(S_i) <  2 c(S_{i-1}) then 8 c(S_{i-1}) < c(S_{i+1})
  (C) if c(S_i) > 10 c(S_{i-1}) then c(S_i) <= g(|Xhat|) * C_1, where C_1 is the
      min-cost solution covering ceil(|remaining|/2) elements of R_{i-1}.

The decomposition oracle below is polynomial time: each S_i is built by the
standard greedy partial-cover rule (cheapest set per newly covered element)
until half of the remaining predicted elements are covered. C_1 is computed
EXACTLY by ILP so that (C) is checked against the true optimum, not a proxy, and
alpha is *measured* as c(S_i)/C_1 rather than assumed.

ICE online phase: run a black-box online algorithm; track its spend; whenever
cumulative online cost exceeds the cost of the next decomposition piece, buy it.
"""
import json, numpy as np
from scipy.optimize import milp, LinearConstraint, Bounds
from scipy.sparse import csc_matrix

RES = {}


def make_instance(m, n, rng, p=0.18):
    A = (rng.uniform(size=(m, n)) < p).astype(np.int8)
    for e in range(m):
        if A[e].sum() == 0: A[e, rng.integers(n)] = 1
    cost = rng.uniform(1.0, 5.0, size=n)
    return A, cost


def ilp_cover(A, cost, elems):
    """Exact min-cost cover of the given elements."""
    if len(elems) == 0: return 0.0, np.zeros(A.shape[1], dtype=int)
    sub = A[list(elems)]
    c = LinearConstraint(csc_matrix(sub), lb=np.ones(len(elems)), ub=np.inf)
    r = milp(c=cost, constraints=[c], integrality=np.ones(A.shape[1]),
             bounds=Bounds(0, 1))
    if not r.success: return None, None
    return float(r.fun), np.round(r.x).astype(int)


def ilp_partial(A, cost, elems, need):
    """Exact min-cost solution covering at least `need` of `elems` (ILP with
    per-element indicators)."""
    elems = list(elems)
    if need <= 0: return 0.0
    n = A.shape[1]; m = len(elems)
    # vars: n set-vars then m element-indicators
    sub = A[elems]
    rows, cols, vals = [], [], []
    for i in range(m):                       # y_i <= sum_{S covering e_i} x_S
        for j in np.flatnonzero(sub[i]):
            rows.append(i); cols.append(j); vals.append(1.0)
        rows.append(i); cols.append(n+i); vals.append(-1.0)
    M = csc_matrix((vals, (rows, cols)), shape=(m, n+m))
    cons = [LinearConstraint(M, lb=np.zeros(m), ub=np.inf)]
    sel = np.zeros((1, n+m)); sel[0, n:] = 1.0
    cons.append(LinearConstraint(csc_matrix(sel), lb=need, ub=np.inf))
    cvec = np.concatenate([cost, np.zeros(m)])
    r = milp(c=cvec, constraints=cons, integrality=np.ones(n+m), bounds=Bounds(0, 1))
    return float(r.fun) if r.success else None


def greedy_partial(A, cost, remaining, need):
    """Polynomial-time oracle: greedy cheapest-per-new-element until `need` covered."""
    rem = set(remaining); chosen = []; covered = set()
    while len(covered) < need:
        best, bj = None, None
        for j in range(A.shape[1]):
            new = len([e for e in rem if A[e, j] and e not in covered])
            if new == 0: continue
            r = cost[j]/new
            if best is None or r < best: best, bj = r, j
        if bj is None: break
        chosen.append(bj)
        covered |= {e for e in rem if A[e, bj]}
    return chosen, covered


def decompose(A, cost, Xhat):
    """Build (X_i, S_i) satisfying (A); measure alpha from (C) and gamma from
    the realised cost growth ratio."""
    rem = list(Xhat); pieces = []
    while rem:
        need = int(np.ceil(len(rem)/2))
        S, cov = greedy_partial(A, cost, rem, need)
        Xi = sorted(cov & set(rem))
        cS = float(cost[S].sum())
        C1 = ilp_partial(A, cost, rem, need)
        pieces.append({"X_i": len(Xi), "cost_S_i": cS, "C1_exact": C1,
                       "alpha_i": (cS/C1 if C1 and C1 > 0 else None),
                       "propA": bool(len(Xi) >= need)})
        rem = [e for e in rem if e not in cov]
        if not S: break
    return pieces


def check_props(pieces):
    a = [p["alpha_i"] for p in pieces if p["alpha_i"] is not None]
    okA = all(p["propA"] for p in pieces)
    cs = [p["cost_S_i"] for p in pieces]
    okB = True
    for i in range(1, len(cs)-1):
        if cs[i] < 2*cs[i-1] and not (8*cs[i-1] < cs[i+1]): okB = False
    ratios = [cs[i]/cs[i-1] for i in range(1, len(cs)) if cs[i-1] > 0]
    return okA, okB, (max(a) if a else None), (max(ratios) if ratios else None)


def online_greedy(A, cost, requests, bought):
    """Black-box online set cover: on an uncovered request, buy cheapest covering set."""
    b = bought.copy(); spend = 0.0
    for e in requests:
        if any(b[j] and A[e, j] for j in range(A.shape[1])): continue
        cand = np.flatnonzero(A[e]); j = cand[np.argmin(cost[cand])]
        b[j] = 1; spend += cost[j]
    return b, spend


def ice(A, cost, requests, pieces_sets, piece_costs):
    """ICE: run the online algorithm; whenever its spend exceeds the next piece's
    cost, buy that piece too."""
    bought = np.zeros(A.shape[1], dtype=int); spend = 0.0; k = 0
    for e in requests:
        if not any(bought[j] and A[e, j] for j in range(A.shape[1])):
            cand = np.flatnonzero(A[e]); j = cand[np.argmin(cost[cand])]
            bought[j] = 1; spend += cost[j]
        while k < len(pieces_sets) and spend >= piece_costs[k]:
            for j in pieces_sets[k]: bought[j] = 1
            spend += piece_costs[k]; k += 1
    return bought, spend


def run():
    rows = []
    for (m, n) in ((40, 60), (60, 90), (80, 120)):
        for corrupt in (0.0, 0.2, 0.5, 1.0):
            rng = np.random.default_rng(m*10+int(corrupt*10))
            A, cost = make_instance(m, n, rng)
            req = list(rng.permutation(m)[:m//2])
            optcost, _ = ilp_cover(A, cost, req)
            # prediction: corrupted version of the true request set
            Xhat = set(req)
            nswap = int(corrupt*len(req))
            if nswap:
                drop = set(rng.choice(list(Xhat), size=min(nswap, len(Xhat)), replace=False))
                Xhat -= drop
                pool = [e for e in range(m) if e not in req]
                Xhat |= set(rng.choice(pool, size=min(nswap, len(pool)), replace=False))
            eta = min(len(req), len(set(req) ^ Xhat))
            pieces = decompose(A, cost, sorted(Xhat))
            okA, okB, amax, gmax = check_props(pieces)
            psets, pcosts = [], []
            rem = sorted(Xhat)
            for p in pieces:
                need = int(np.ceil(len(rem)/2))
                S, cov = greedy_partial(A, cost, rem, need)
                psets.append(S); pcosts.append(float(cost[S].sum()))
                rem = [e for e in rem if e not in cov]
            b_ice, _ = ice(A, cost, req, psets, pcosts)
            c_ice = float(cost[b_ice == 1].sum())
            b_base, _ = online_greedy(A, cost, req, np.zeros(n, dtype=int))
            c_base = float(cost[b_base == 1].sum())
            rows.append({"m": m, "n": n, "corrupt": corrupt, "eta": eta,
                         "OPT_ilp": round(optcost, 3),
                         "ICE_cost": round(c_ice, 3), "baseline_cost": round(c_base, 3),
                         "ICE_ratio": round(c_ice/optcost, 4),
                         "baseline_ratio": round(c_base/optcost, 4),
                         "pieces": len(pieces), "propA_holds": okA, "propB_holds": okB,
                         "measured_alpha_max": (round(amax, 4) if amax else None),
                         "max_cost_growth_gamma": (round(gmax, 3) if gmax else None)})
            print("  m=%-3d corrupt=%.1f eta=%-3d OPT=%7.2f  ICE=%7.2f (%.3f)  base=%7.2f (%.3f)  pieces=%d A=%s B=%s alpha<=%.2f"
                  % (m, corrupt, eta, optcost, c_ice, c_ice/optcost, c_base, c_base/optcost,
                     len(pieces), okA, okB, amax or -1), flush=True)
    RES["claim2_ice"] = {
        "rows": rows,
        "propA_all": all(r["propA_holds"] for r in rows),
        "propB_all": all(r["propB_holds"] for r in rows),
        "max_measured_alpha": max(r["measured_alpha_max"] for r in rows if r["measured_alpha_max"]),
        "ICE_beats_baseline_at_eta0": [r for r in rows if r["eta"] == 0],
        "n_cells": len(rows)}
    json.dump(RES, open("ice_results.json", "w"), indent=1)


if __name__ == "__main__":
    run(); print("DONE")