diff --git a/bitten/bitten.py b/bitten/bitten.py index c641502..0a5eaa4 100644 --- a/bitten/bitten.py +++ b/bitten/bitten.py @@ -20,6 +20,12 @@ def pack(*args): return args +def lament(*args, **kwargs): + from sys import stderr + + print(*args, **kwargs, file=stderr) + + def _is_in(needle, *haystack): # like the "in" keyword, but uses "is" instead of "==". for thing in haystack: @@ -125,83 +131,95 @@ def _flatten(it): return res -def _penalize(constraints, *x, tol=1e-5, scale=1e10, growth=4.0): - # NOTE: maybe growth shouldn't be configurable unless - # it also affects the order of the penalty equation (currently 3). - # NOTE: different ordering of constraints can result in different solutions. - # NOTE: this function doesn't use numpy, so you can copypaste it elsewhere. - # growth = growth ** (1.0 / len(constraints)) - # penalties = [cons(*x) for cons in constraints] - penalties = _flatten(cons(*x) for cons in constraints) - growth = growth ** (1.0 / len(penalties)) - unsat = sum(p > tol for p in penalties) - # cubic = [p + p * p + p * p * p for p in penalties] - penalsum = 0.0 - for p in penalties: - penalsum *= growth # always happens so each penalty gets its own stratum - if p > tol: - penalsum += p + p * p + p * p * p - return scale * (unsat + penalsum) - - -def _penalize2(constraints, *x, tol=1e-5, scale=1e10, growth=3.0): - # updated from upstream (v2022.11) - penalties = _flatten(cons(*x) for cons in constraints) - growth = growth ** (1.0 / len(penalties)) - unsat = sum(p > tol for p in penalties) - penalsum = 0.0 - for p in penalties: - penalsum *= growth # always happens so each penalty gets its own stratum - if p > tol: - penalsum += p + p * p * p - return scale * (unsat + penalsum) - - -def _penalize3(constraints, *x, tol=1e-5, scale=1e10, growth=3.0): - # updated from upstream (v2022.19) - penalties = _flatten(cons(*x) for cons in constraints) +def _penalize_v2021_23(penalties, tol=1e-5, scale=1e10): n_con = len(penalties) - growth = growth ** (1.0 / n_con) # "ps" - increment = n_con**-0.5 # "pnsi" - penalty, nominal = 0.0, 0.0 # "pns", "pnsm" + unmet = sum(p > 0.0 for p in penalties) + growth = 4.0 ** (1.0 / n_con) # "ps" + penalty = 0.0 for p in penalties: - p = max(p - tol, 0.0) - penalty = penalty * growth + increment + p + p * p * p - nominal = nominal * growth + increment - return scale * (penalty - nominal + 1.0) + p = max(p, 0.0) + squared = p * p + penalty = growth * penalty + p + squared + p * squared + return scale * (unmet + penalty) -def _penalize4(constraints, *x, tol=1e-5, scale=1e9): - # updated from upstream (v2022.23) - # NOTE: i've reduced `scale` by a factor of ten - # out of concern for numeric precision. - # also, i've removed the growth keyword, since it very likely - # needs to coincide with the order of the polynomial (always 3). - penalties = _flatten(cons(*x) for cons in constraints) +def _penalize_v2022_11(penalties, tol=1e-5, scale=1e10): + n_con = len(penalties) + unmet = sum(p > 0.0 for p in penalties) + growth = 3.0 ** (1.0 / n_con) # "ps" + penalty = 0.0 + for p in penalties: + p = max(p, 0.0) + penalty = growth * penalty + p + p * p * p + return scale * (unmet + penalty) + + +def _penalize_v2022_19(penalties, tol=1e-5, scale=1e10): n_con = len(penalties) growth = 3.0 ** (1.0 / n_con) # "ps" increment = n_con**-0.5 # "pnsi" - # coeff = increment**3.0 # "pnm" penalty, nominal = 0.0, 0.0 # "pns", "pnsm" for p in penalties: - p = max(p - tol, 0.0) - v = p # * coeff # 2022.25 drops the coeff - v2 = v * v - poly = v + v2 + v * v2 - penalty = penalty * growth + increment + poly - nominal = nominal * growth + increment + p = max(p, 0.0) + penalty = growth * penalty + increment + p + p * p * p + nominal = growth * nominal + increment return scale * (penalty - nominal + 1.0) +def _penalize_v2022_22(penalties, tol=1e-5, scale=1e10): + n_con = len(penalties) + growth = 3.0 ** (1.0 / n_con) # "ps" + increment = n_con**-0.5 # "pnsi" + coeff = increment**3.0 # "pnm" + penalty, nominal = 0.0, 0.0 # "pns", "pnsm" + for p in penalties: + p = coeff * max(p, 0.0) + squared = p * p + penalty = growth * penalty + increment + p + squared + p * squared + nominal = growth * nominal + increment + return scale * (penalty - nominal + 1.0) + + +def _penalize_v2022_25(penalties, tol=1e-5, scale=1e10): + n_con = len(penalties) + growth = 3.0 ** (1.0 / n_con) # "ps" + increment = n_con**-0.5 # "pnsi" + penalty, nominal = 0.0, 0.0 # "pns", "pnsm" + for p in penalties: + p = max(p, 0.0) + squared = p * p + penalty = growth * penalty + increment + p + squared + p * squared + nominal = growth * nominal + increment + return scale * (penalty - nominal + 1.0) + + +def _penalize_v2022_25_1(penalties, tol=1e-5, scale=1e10): + n_con = len(penalties) + growth = 3.0 ** (1.0 / n_con) # "ps" + increment = n_con**-0.5 # "pnsi" + penalty = 0.0 # "pns" + for p in penalties: + p = max(p, 0.0) + penalty = growth * penalty + increment + p + p * p + return scale * (1.0 + penalty + penalty * penalty) + + +penalizers = { + 1: _penalize_v2021_23, + 2: _penalize_v2022_11, + 3: _penalize_v2022_19, + 4: _penalize_v2022_22, + 5: _penalize_v2022_25, + 6: _penalize_v2022_25_1, +} + + def penalize(x, constraints, tol=1e-5, *, scale=1e10, growth=4.0): - # DEPRECATED - return _penalize(constraints, x, tol=tol, scale=scale, growth=growth) + assert False, "deprecated; use _penalize_v2021_23 instead" def count_unsat(x, constraints, tol=1e-5): - # DEPRECATED - penalties = [cons(*x) for cons in constraints] - return sum(p > tol for p in penalties) + assert False, "deprecated; do it yourself" class Impure: # TODO: rename? volatile? aaa the word is on the tip of my tongue @@ -303,7 +321,7 @@ class Crossentropy(AbstractError): class Constrain(Objective): - def __init__(self, *constraints, tol=1e-5, version=1): + def __init__(self, *constraints, tol=1e-5, version=5): for cons in constraints: assert callable(cons) self.constraints = constraints @@ -312,17 +330,19 @@ class Constrain(Objective): @property def version(self): - return self._version + 1 + return self._version @version.setter def version(self, version): - penalizers = [_penalize, _penalize2, _penalize3, _penalize4] - assert 1 <= version <= len(penalizers), version - self._version = version - 1 + assert version in penalizers, f"unknown version of penalty function: {version}" + self._version = version self._penalize = penalizers[self._version] - def penalize(self, *args): - return self._penalize(self.constraints, *args, tol=self.tol) + def penalize(self, *x): + penalties = _flatten(cons(*x) for cons in self.constraints) + if not any(p > 0.0 for p in penalties): + return 0.0 + return self._penalize(penalties, tol=self.tol) def compute_with(self, fun, **kw_hypers): return self.penalize(*kw_hypers.values()) @@ -737,12 +757,12 @@ def _bite( cache.hash(h) cached = cache.get() if _debug: - print("HASH:", cache.hashed, sep="\n") + lament("HASH:", cache.hashed, sep="\n") if cached is None: if _debug: # dirty test for exceptions center = np.mean(linear_bounds, axis=-1) if hypers else () - print("FIRST VALUE:", objective(center), sep="\n") + lament("FIRST VALUE:", objective(center), sep="\n") if hypers: res = _biteopt(objective, linear_bounds, iters=budget, **optimizer_kwargs) @@ -753,7 +773,7 @@ def _bite( res = _evaluator(objective, budget) if _debug: - print("RES:", res, sep="\n") + lament("RES:", res, sep="\n") optimized = res.x else: