|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# Copyright (c) Facebook, Inc. and its affiliates. |
| 3 | +# |
| 4 | +# This source code is licensed under the MIT license found in the |
| 5 | +# LICENSE file in the root directory of this source tree. |
| 6 | + |
| 7 | +from __future__ import annotations |
| 8 | + |
| 9 | +from typing import Callable, List, Optional, Tuple |
| 10 | + |
| 11 | +import botorch.models.model as model |
| 12 | +import torch |
| 13 | +from torch import Tensor |
| 14 | + |
| 15 | +from ..logging import _get_logger |
| 16 | +from .sampling import manual_seed |
| 17 | + |
| 18 | + |
| 19 | +logger = _get_logger(name="Feasibility") |
| 20 | + |
| 21 | + |
| 22 | +def get_feasible_samples( |
| 23 | + samples: Tensor, |
| 24 | + inequality_constraints: Optional[List[Tuple[Tensor, Tensor, float]]] = None, |
| 25 | +) -> Tuple[Tensor, float]: |
| 26 | + r""" |
| 27 | + Checks which of the samples satisfy all of the inequality constraints. |
| 28 | +
|
| 29 | + Args: |
| 30 | + samples: A `sample size x d` size tensor of feature samples, |
| 31 | + where d is a feature dimension. |
| 32 | + inequality constraints: A list of tuples (indices, coefficients, rhs), |
| 33 | + with each tuple encoding an inequality constraint of the form |
| 34 | + `\sum_i (X[indices[i]] * coefficients[i]) >= rhs`. |
| 35 | + Returns: |
| 36 | + 2-element tuple containing |
| 37 | +
|
| 38 | + - Samples satisfying the linear constraints. |
| 39 | + - Estimated proportion of samples satisfying the linear constraints. |
| 40 | + """ |
| 41 | + |
| 42 | + if inequality_constraints is None: |
| 43 | + return samples, 1.0 |
| 44 | + |
| 45 | + nsamples = samples.size(0) |
| 46 | + |
| 47 | + feasible = torch.ones(nsamples, device=samples.device, dtype=torch.bool) |
| 48 | + |
| 49 | + for (indices, coefficients, rhs) in inequality_constraints: |
| 50 | + feasible &= samples.index_select(1, indices) @ coefficients >= rhs |
| 51 | + |
| 52 | + feasible_samples = samples[feasible] |
| 53 | + |
| 54 | + p_linear = feasible_samples.size(0) / nsamples |
| 55 | + |
| 56 | + return feasible_samples, p_linear |
| 57 | + |
| 58 | + |
| 59 | +def get_outcome_feasibility_probability( |
| 60 | + model: model.Model, |
| 61 | + X: Tensor, |
| 62 | + outcome_constraints: List[Callable[[Tensor], Tensor]], |
| 63 | + threshold: float = 0.1, |
| 64 | + nsample_outcome: int = 1000, |
| 65 | + seed: Optional[int] = None, |
| 66 | +) -> float: |
| 67 | + r""" |
| 68 | + Monte Carlo estimate of the feasible volume with respect to the outcome constraints. |
| 69 | +
|
| 70 | + Args: |
| 71 | + model: The model used for sampling the posterior. |
| 72 | + X: A tensor of dimension `batch-shape x 1 x d`, where d is feature dimension. |
| 73 | + outcome_constraints: A list of callables, each mapping a Tensor of dimension |
| 74 | + `sample_shape x batch-shape x q x m` to a Tensor of dimension |
| 75 | + `sample_shape x batch-shape x q`, where negative values imply feasibility. |
| 76 | + threshold: A lower limit for the probability of posterior samples feasibility. |
| 77 | + nsample_outcome: The number of samples from the model posterior. |
| 78 | + seed: The seed for the posterior sampler. If omitted, use a random seed. |
| 79 | +
|
| 80 | + Returns: |
| 81 | + Estimated proportion of features for which posterior samples satisfy |
| 82 | + given outcome constraints with probability above or equal to |
| 83 | + the given threshold. |
| 84 | + """ |
| 85 | + from botorch.sampling import SobolQMCNormalSampler |
| 86 | + |
| 87 | + seed = seed if seed is not None else torch.randint(0, 1000000, (1,)).item() |
| 88 | + |
| 89 | + posterior = model.posterior(X) # posterior consists of batch_shape marginals |
| 90 | + sampler = SobolQMCNormalSampler(num_samples=nsample_outcome, seed=seed) |
| 91 | + # size of samples: (num outcome samples, batch_shape, 1, outcome dim) |
| 92 | + samples = sampler(posterior) |
| 93 | + |
| 94 | + feasible = torch.ones(samples.shape[:-1], dtype=torch.bool, device=samples.device) |
| 95 | + |
| 96 | + # a sample passes if each constraint applied to the sample |
| 97 | + # produces a non-negative tensor |
| 98 | + for oc in outcome_constraints: |
| 99 | + # broadcasted evaluation of the outcome constraints |
| 100 | + feasible &= oc(samples) <= 0 |
| 101 | + |
| 102 | + # proportion of feasibile samples for each of the elements of X |
| 103 | + # summation is done across feasible outcome samples |
| 104 | + p_feas = feasible.sum(0).float() / feasible.size(0) |
| 105 | + |
| 106 | + # proportion of features leading to the posterior outcome |
| 107 | + # satisfying the given outcome constraints |
| 108 | + # with at probability above a given threshold |
| 109 | + p_outcome = (p_feas >= threshold).sum().item() / X.size(0) |
| 110 | + |
| 111 | + return p_outcome |
| 112 | + |
| 113 | + |
| 114 | +def estimate_feasible_volume( |
| 115 | + bounds: Tensor, |
| 116 | + model: model.Model, |
| 117 | + outcome_constraints: List[Callable[[Tensor], Tensor]], |
| 118 | + inequality_constraints: Optional[List[Tuple[Tensor, Tensor, float]]] = None, |
| 119 | + nsample_feature: int = 1000, |
| 120 | + nsample_outcome: int = 1000, |
| 121 | + threshold: float = 0.1, |
| 122 | + verbose: bool = False, |
| 123 | + seed: Optional[int] = None, |
| 124 | + device: Optional[torch.device] = None, |
| 125 | + dtype: Optional[torch.dtype] = None, |
| 126 | +) -> Tuple[float, float]: |
| 127 | + r""" |
| 128 | + Monte Carlo estimate of the feasible volume with respect |
| 129 | + to feature constraints and outcome constraints. |
| 130 | +
|
| 131 | + Args: |
| 132 | + bounds: A `2 x d` tensor of lower and upper bounds |
| 133 | + for each column of `X`. |
| 134 | + model: The model used for sampling the outcomes. |
| 135 | + outcome_constraints: A list of callables, each mapping a Tensor of dimension |
| 136 | + `sample_shape x batch-shape x q x m` to a Tensor of dimension |
| 137 | + `sample_shape x batch-shape x q`, where negative values imply |
| 138 | + feasibility. |
| 139 | + inequality constraints: A list of tuples (indices, coefficients, rhs), |
| 140 | + with each tuple encoding an inequality constraint of the form |
| 141 | + `\sum_i (X[indices[i]] * coefficients[i]) >= rhs`. |
| 142 | + nsample_feature: The number of feature samples satisfying the bounds. |
| 143 | + nsample_outcome: The number of outcome samples from the model posterior. |
| 144 | + threshold: A lower limit for the probability of outcome feasibility |
| 145 | + seed: The seed for both feature and outcome samplers. If omitted, |
| 146 | + use a random seed. |
| 147 | + verbose: An indicator for whether to log the results. |
| 148 | +
|
| 149 | + Returns: |
| 150 | + 2-element tuple containing: |
| 151 | +
|
| 152 | + - Estimated proportion of volume in feature space that is |
| 153 | + feasible wrt the bounds and the inequality constraints (linear). |
| 154 | + - Estimated proportion of feasible features for which |
| 155 | + posterior samples (outcome) satisfies the outcome constraints |
| 156 | + with probability above the given threshold. |
| 157 | + """ |
| 158 | + |
| 159 | + seed = seed if seed is not None else torch.randint(0, 1000000, (1,)).item() |
| 160 | + |
| 161 | + with manual_seed(seed=seed): |
| 162 | + box_samples = bounds[0] + (bounds[1] - bounds[0]) * torch.rand( |
| 163 | + (nsample_feature, bounds.size(1)), dtype=dtype, device=device |
| 164 | + ) |
| 165 | + |
| 166 | + features, p_feature = get_feasible_samples( |
| 167 | + samples=box_samples, inequality_constraints=inequality_constraints |
| 168 | + ) # each new feature sample is a row |
| 169 | + |
| 170 | + p_outcome = get_outcome_feasibility_probability( |
| 171 | + model=model, |
| 172 | + X=features.unsqueeze(-2), |
| 173 | + outcome_constraints=outcome_constraints, |
| 174 | + threshold=threshold, |
| 175 | + nsample_outcome=nsample_outcome, |
| 176 | + seed=seed, |
| 177 | + ) |
| 178 | + |
| 179 | + if verbose: # pragma: no cover |
| 180 | + logger.info( |
| 181 | + "Proportion of volume that satisfies linear constraints: " |
| 182 | + + f"{p_feature:.4e}" |
| 183 | + ) |
| 184 | + if p_feature <= 0.01: |
| 185 | + logger.warning( |
| 186 | + "The proportion of satisfying volume is very low and may lead to " |
| 187 | + + "very long run times. Consider making your constraints less " |
| 188 | + + "restrictive." |
| 189 | + ) |
| 190 | + logger.info( |
| 191 | + "Proportion of linear-feasible volume that also satisfies each " |
| 192 | + + f"outcome constraint with probability > 0.1: {p_outcome:.4e}" |
| 193 | + ) |
| 194 | + if p_outcome <= 0.001: |
| 195 | + logger.warning( |
| 196 | + "The proportion of volume that also satisfies the outcome constraint " |
| 197 | + + "is very low. Consider making your parameter and outcome constraints " |
| 198 | + + "less restrictive." |
| 199 | + ) |
| 200 | + return p_feature, p_outcome |
0 commit comments