popt.loop.ensemble

  1# External imports
  2import numpy as np
  3import sys
  4import warnings
  5
  6from copy import deepcopy
  7
  8
  9# Internal imports
 10from popt.misc_tools import optim_tools as ot
 11from pipt.misc_tools import analysis_tools as at
 12from ensemble.ensemble import Ensemble as PETEnsemble
 13from popt.loop.dist import GenOptDistribution
 14
 15
 16class Ensemble(PETEnsemble):
 17    """
 18    Class to store control states and evaluate objective functions.
 19
 20    Methods
 21    -------
 22    get_state()
 23        Returns control vector as ndarray
 24
 25    get_final_state(return_dict)
 26        Returns final control vector between [lb,ub]
 27
 28    get_cov()
 29        Returns the ensemble covariance matrix
 30
 31    function(x,*args)
 32        Objective function called during optimization
 33
 34    gradient(x,*args)
 35        Ensemble gradient
 36
 37    hessian(x,*args)
 38        Ensemble hessian
 39
 40    calc_ensemble_weights(self,x,*args):
 41        Calculate weights used in sequential monte carlo optimization
 42
 43    """
 44
 45    def __init__(self, keys_en, sim, obj_func):
 46        """
 47        Parameters
 48        ----------
 49        keys_en : dict
 50            Options for the ensemble class
 51
 52            - disable_tqdm: supress tqdm progress bar for clean output in the notebook
 53            - ne: number of perturbations used to compute the gradient
 54            - state: name of state variables passed to the .mako file
 55            - prior_<name>: the prior information the state variables, including mean, variance and variable limits
 56            - num_models: number of models (if robust optimization) (default 1)
 57            - transform: transform variables to [0,1] if true (default true)
 58
 59        sim : callable
 60            The forward simulator (e.g. flow)
 61
 62        obj_func : callable
 63            The objective function (e.g. npv)
 64        """
 65
 66        # Initialize PETEnsemble
 67        super(Ensemble, self).__init__(keys_en, sim)
 68        
 69        def __set__variable(var_name=None, defalut=None):
 70            if var_name in keys_en:
 71                return keys_en[var_name]
 72            else:
 73                return defalut
 74
 75        # Set number of models (default 1)
 76        self.num_models = __set__variable('num_models', 1)
 77
 78        # Set transform flag (defalult True)
 79        self.transform = __set__variable('transform', True)
 80
 81        # Number of samples to compute gradient
 82        self.num_samples = self.ne
 83
 84        # We need the limits to convert between [0, 1] and [lb, ub],
 85        # and we need the bounds as list of (min, max) pairs
 86        # Also set the state and covarianve equal to the values provided in the input.
 87        self.upper_bound = []
 88        self.lower_bound = []
 89        self.bounds = []
 90        self.cov = np.array([])
 91        for name in self.prior_info.keys():
 92            self.state[name] = np.asarray(self.prior_info[name]['mean'])
 93            num_state_var = len(self.state[name])
 94            value_cov = self.prior_info[name]['variance'] * np.ones((num_state_var,))
 95            if 'limits' in self.prior_info[name].keys():
 96                lb = self.prior_info[name]['limits'][0]
 97                ub = self.prior_info[name]['limits'][1]
 98                self.lower_bound.append(lb)
 99                self.upper_bound.append(ub)
100                if self.transform:
101                    value_cov = value_cov / (ub - lb)**2
102                    np.clip(value_cov, 0, 1, out=value_cov)
103                    self.bounds += num_state_var*[(0, 1)]
104                else:
105                    self.bounds += num_state_var*[(lb, ub)]
106            else:
107                self.bounds += num_state_var*[(None, None)]
108            self.cov = np.append(self.cov, value_cov)
109
110        self._scale_state()
111        self.cov = np.diag(self.cov)
112
113        # Set objective function (callable)
114        self.obj_func = obj_func
115
116        # Objective function values
117        self.state_func_values = None
118        self.ens_func_values = None
119
120        # Inflation factor used in SmcOpt
121        self.inflation_factor = None
122        self.survival_factor = None
123        self.particles = np.empty((self.cov.shape[0],0))
124        self.particle_values = np.empty((0))
125
126        # Initialize variables for bias correction
127        if 'bias_file' in self.sim.input_dict:  # use bias correction
128            self.bias_file = self.sim.input_dict['bias_file'].upper()  # mako file for simulations
129        else:
130            self.bias_file = None
131        self.bias_adaptive = None  # flag to adaptively update the bias correction (not implemented yet)
132        self.bias_factors = None  # this is J(x_j,m_j)/J(x_j,m)
133        self.bias_weights = np.ones(self.num_samples) / self.num_samples  # initialize with equal weights
134        self.bias_points = None  # this is the points used to estimate the bias correction
135
136        # Setup GenOpt
137        self.genopt = GenOptDistribution(self.get_state(), 
138                                         self.get_cov(), 
139                                         func=self.function, 
140                                         ne=self.num_samples)
141
142    def get_state(self):
143        """
144        Returns
145        -------
146        x : numpy.ndarray
147            Control vector as ndarray, shape (number of controls, number of perturbations)
148        """
149        x = ot.aug_optim_state(self.state, list(self.state.keys()))
150        return x
151
152    def get_cov(self):
153        """
154        Returns
155        -------
156        cov : numpy.ndarray
157            Covariance matrix, shape (number of controls, number of controls)
158        """
159
160        return self.cov
161
162    def get_bounds(self):
163        """
164        Returns
165        -------
166        bounds : list
167            (min, max) pairs for each element in x. None is used to specify no bound.
168        """
169
170        return self.bounds
171    
172    def get_final_state(self, return_dict=False):
173        """
174        Parameters
175        ----------
176        return_dict : bool
177            Retrun dictionary if true
178
179        Returns
180        -------
181        x : numpy.ndarray
182            Control vector as ndarray, shape (number of controls, number of perturbations)
183        """
184
185        self._invert_scale_state()
186        if return_dict:
187            x = self.state
188        else:
189            x = self.get_state()
190        return x
191
192    def function(self, x, *args, **kwargs):
193        """
194        This is the main function called during optimization.
195
196        Parameters
197        ----------
198        x : ndarray
199            Control vector, shape (number of controls, number of perturbations)
200
201        Returns
202        -------
203        obj_func_values : numpy.ndarray
204            Objective function values, shape (number of perturbations, )
205        """
206        self._aux_input()
207
208        if len(x.shape) == 1:
209            self.ne = self.num_models
210        else:
211            self.ne = x.shape[1]
212
213        self.state = ot.update_optim_state(x, self.state, list(self.state.keys()))  # go from nparray to dict
214        self._invert_scale_state()  # ensure that state is in [lb,ub]
215        run_success = self.calc_prediction()  # calculate flow data
216        self._scale_state()  # scale back to [0, 1]
217        if run_success:
218            func_values = self.obj_func(self.pred_data, input_dict=self.sim.input_dict,
219                                        true_order=self.sim.true_order, **kwargs)
220        else:
221            func_values = np.inf  # the simulations have crashed
222
223        if len(x.shape) == 1:
224            self.state_func_values = func_values
225        else:
226            self.ens_func_values = func_values
227        
228        return func_values
229
230    def gradient(self, x, *args, **kwargs):
231        r"""
232        Calculate the preconditioned gradient associated with ensemble, defined as:
233
234        .. math::
235            S \approx C_x \times G^T
236
237        where :math:`C_x` is the state covariance matrix, and :math:`G` is the standard
238        gradient. The ensemble sensitivity matrix is calculated as:
239
240        .. math::
241            S = X \times J^T /(N_e-1)
242
243        where :math:`X` and :math:`J` are ensemble matrices of :math:`x` (or control variables) and objective function
244        perturbed by their respective means. In practice (and in this method), :math:`S` is calculated by perturbing the
245        current control variable with Gaussian random numbers from :math:`N(0, C_x)` (giving :math:`X`), running
246        the generated ensemble (:math:`X`) through the simulator to give an ensemble of objective function values
247        (:math:`J`), and in the end calculate :math:`S`. Note that :math:`S` is an :math:`N_x \times 1` vector, where
248        :math:`N_x` is length of the control vector and the objective function is scalar.
249
250        Parameters
251        ----------
252        x : ndarray
253            Control vector, shape (number of controls, )
254
255        args : tuple
256            Covarice (:math:`C_x`), shape (number of controls, number of controls)
257
258        Returns
259        -------
260        gradient : numpy.ndarray
261                The gradient evaluated at x, shape (number of controls, )
262        """
263
264        # Set the ensemble state equal to the input control vector x
265        self.state = ot.update_optim_state(x, self.state, list(self.state.keys()))
266
267        # Set the covariance equal to the input
268        self.cov = args[0]
269
270        # If bias correction is used we need to temporarily store the initial state
271        initial_state = None
272        if self.bias_file is not None and self.bias_factors is None:  # first iteration
273            initial_state = deepcopy(self.state)  # store this to update current objective values
274
275        # Generate ensemble of states
276        self.ne = self.num_samples
277        nr = self._aux_input()
278        self.state = self._gen_state_ensemble()
279        
280        state_ens = at.aug_state(self.state, list(self.state.keys()))
281        self.function(state_ens, **kwargs)
282
283        # If bias correction is used we need to calculate the bias factors, J(u_j,m_j)/J(u_j,m)
284        if self.bias_file is not None:  # use bias corrections
285            self._bias_factors(self.ens_func_values, initial_state)
286
287        # Perturb state and function values with their mean
288        state_ens = at.aug_state(self.state, list(self.state.keys()))
289        pert_state = state_ens - np.dot(state_ens.mean(1)[:, None], np.ones((1, self.ne)))
290        if self.bias_file is not None:  # use bias corrections
291            self.ens_func_values *= self._bias_correction(self.state)
292            pert_obj_func = self.ens_func_values - np.mean(self.ens_func_values)
293        else:
294            pert_obj_func = self.ens_func_values - np.array(np.repeat(self.state_func_values, nr))
295
296        # Calculate the gradient
297        g_m = np.zeros(state_ens.shape[0])
298        for i in np.arange(self.ne):
299            g_m = g_m + pert_obj_func[i] * pert_state[:, i]
300
301        gradient = g_m / (self.ne - 1)
302
303        return gradient
304
305    def hessian(self, x=None, *args):
306        r"""
307        Calculate the hessian matrix associated with ensemble, defined as:
308
309        .. math::
310            H = J(XX^T - \Sigma)/ (N_e-1)
311
312        where :math:`X` and :math:`J` are ensemble matrices of :math:`x` (or control variables) and objective function
313        perturbed by their respective means.
314
315        Note: state and ens_func_values are assumed to already exist from computation of the gradient.
316        Save time by not running them again.
317
318        Parameters
319        ----------
320        x : ndarray
321            Control vector, shape (number of controls, number of perturbations)
322
323        Returns
324        -------
325        hessian: numpy.ndarray
326            The hessian evaluated at x, shape (number of controls, number of controls)
327
328        References
329        ----------
330        Zhang, Y., Stordal, A.S. & Lorentzen, R.J. A natural Hessian approximation for ensemble based optimization.
331        Comput Geosci 27, 355–364 (2023). https://doi.org/10.1007/s10596-022-10185-z
332        """
333
334        # Perturb state and function values with their mean
335        state_ens = at.aug_state(self.state, list(self.state.keys()))
336        pert_state = state_ens - np.dot(state_ens.mean(1)[:, None], np.ones((1, self.ne)))
337        nr = self._aux_input()
338        pert_obj_func = self.ens_func_values - np.array(np.repeat(self.state_func_values, nr))
339
340        # Calculate the gradient for mean and covariance matrix
341        g_c = np.zeros(self.cov.shape)
342        for i in np.arange(self.ne):
343            g_c = g_c + pert_obj_func[i] * (np.outer(pert_state[:, i], pert_state[:, i]) - self.cov)
344
345        hessian = g_c / (self.ne - 1)
346
347        return hessian
348    '''
349    def genopt_gradient(self, x, *args):
350        self.genopt.update_distribution(*args)
351        gradient = self.genopt.ensemble_gradient(func=self.function, 
352                                                 x=x, 
353                                                 ne=self.num_samples)
354        return gradient
355    
356    def genopt_mutation_gradient(self, x=None, *args, **kwargs):
357        return self.genopt.ensemble_mutation_gradient(return_ensembles=kwargs['return_ensembles'])
358    '''
359
360    def calc_ensemble_weights(self, x, *args, **kwargs):
361        r"""
362        Calculate weights used in sequential monte carlo optimization.
363
364        Parameters
365        ----------
366        x : ndarray
367            Control vector, shape (number of controls, )
368
369        args : tuple
370            Inflation factor, covariance (:math:`C_x`, shape (number of controls, number of controls)) and survival factor
371
372        Returns
373        -------
374        sens_matrix, best_ens, best_func : tuple
375                The weighted ensemble, the best ensemble member, and the best objective function value
376        """
377
378        # Set the ensemble state equal to the input control vector x
379        self.state = ot.update_optim_state(x, self.state, list(self.state.keys()))
380
381        # Set the inflation factor and covariance equal to the input
382        self.inflation_factor = args[0]
383        self.cov = args[1]
384        self.survival_factor = args[2]
385
386        # If bias correction is used we need to temporarily store the initial state
387        initial_state = None
388        if self.bias_file is not None and self.bias_factors is None:  # first iteration
389            initial_state = deepcopy(self.state)  # store this to update current objective values
390
391        # Generate ensemble of states
392        if self.particles.shape[1] == 0:
393            self.ne = self.num_samples
394        else:
395            self.ne = int(np.round(self.num_samples*self.survival_factor))
396        self._aux_input()
397        self.state = self._gen_state_ensemble()
398
399        self._invert_scale_state()  # ensure that state is in [lb,ub]
400        self.calc_prediction()  # calculate flow data
401        self._scale_state()  # scale back to [0, 1]
402
403        #self.ens_func_values = self.obj_func(self.pred_data, self.sim.input_dict, self.sim.true_order)
404        #self.ens_func_values = np.array(self.ens_func_values)
405        state_ens = at.aug_state(self.state, list(self.state.keys()))
406        self.function(state_ens, **kwargs)
407
408        self.particles = np.hstack((self.particles, state_ens))
409        self.particle_values = np.hstack((self.particle_values,self.ens_func_values))
410
411        # If bias correction is used we need to calculate the bias factors, J(u_j,m_j)/J(u_j,m)
412        if self.bias_file is not None:  # use bias corrections
413            self._bias_factors(self.ens_func_values, initial_state)
414
415        # Calculate the weights and ensemble sensitivity matrix
416        warnings.filterwarnings('ignore')  # suppress warnings
417        weights = np.zeros(self.num_samples)
418        for i in np.arange(self.num_samples):
419            weights[i] = np.exp(-(self.particle_values[i]-np.min(self.particle_values))*self.inflation_factor)
420
421        weights = weights + 0.000001
422        weights = weights/np.sum(weights)  # TODO: Sjekke at disse er riktig
423
424        sens_matrix = self.particles @ weights
425        index = np.argmin(self.particle_values)
426        best_ens = self.particles[:, index]
427        best_func = self.particle_values[index]
428        resample_index = np.random.choice(self.num_samples,int(np.round(self.num_samples-
429                                          self.num_samples*self.survival_factor)),replace=True,p=weights)
430        self.particles = self.particles[:, resample_index]
431        self.particle_values = self.particle_values[resample_index]
432        return sens_matrix, best_ens, best_func
433
434    def _gen_state_ensemble(self):
435        """
436        Generate ensemble with the current state (control variable) as the mean and using the covariance matrix
437        """
438
439        state_en = {}
440        cov_blocks = ot.corr2BlockDiagonal(self.state, self.cov)
441        for i, statename in enumerate(self.state.keys()):
442            mean = self.state[statename]
443            cov = cov_blocks[i]
444            temp_state_en = np.random.multivariate_normal(mean, cov, self.ne).transpose()
445            shifted_ensemble = np.array([mean]).T + temp_state_en - np.array([np.mean(temp_state_en, 1)]).T
446            if self.upper_bound and self.lower_bound:
447                if self.transform:
448                    np.clip(shifted_ensemble, 0, 1, out=shifted_ensemble)
449                else:
450                    np.clip(shifted_ensemble, self.lower_bound[i], self.upper_bound[i], out=shifted_ensemble)
451            state_en[statename] = shifted_ensemble
452
453        return state_en
454
455    def _aux_input(self):
456        """
457        Set the auxiliary input used for multiple geological realizations
458        """
459
460        nr = 1  # nr is the ratio of samples over models
461        if self.num_models > 1:
462            if np.remainder(self.num_samples, self.num_models) == 0:
463                nr = int(self.num_samples / self.num_models)
464                self.aux_input = list(np.repeat(np.arange(self.num_models), nr))
465            else:
466                print('num_samples must be a multiplum of num_models!')
467                sys.exit(0)
468        return nr
469
470    def _scale_state(self):
471        """
472        Transform the internal state from [lb, ub] to [0, 1]
473        """
474        if self.transform and (self.upper_bound and self.lower_bound):
475            for i, key in enumerate(self.state):
476                self.state[key] = (self.state[key] - self.lower_bound[i])/(self.upper_bound[i] - self.lower_bound[i])
477                np.clip(self.state[key], 0, 1, out=self.state[key])
478
479    def _invert_scale_state(self):
480        """
481        Transform the internal state from [0, 1] to [lb, ub]
482        """
483        if self.transform and (self.upper_bound and self.lower_bound):
484            for i, key in enumerate(self.state):
485                if self.transform:
486                    self.state[key] = self.lower_bound[i] + self.state[key]*(self.upper_bound[i] - self.lower_bound[i])
487                np.clip(self.state[key], self.lower_bound[i], self.upper_bound[i], out=self.state[key])
488
489    def _bias_correction(self, state):
490        """
491        Calculate bias correction. Currently, the bias correction is a constant independent of the state
492        """
493        if self.bias_factors is not None:
494            return np.sum(self.bias_weights * self.bias_factors)
495        else:
496            return 1
497
498    def _bias_factors(self, obj_func_values, initial_state):
499        """
500        Function for computing the bias factors
501        """
502
503        if self.bias_factors is None:  # first iteration
504            currentfile = self.sim.file
505            self.sim.file = self.bias_file
506            self.ne = self.num_samples
507            self.aux_input = list(np.arange(self.ne))
508            self.calc_prediction()
509            self.sim.file = currentfile
510            bias_func_values = self.obj_func(self.pred_data, self.sim.input_dict, self.sim.true_order)
511            bias_func_values = np.array(bias_func_values)
512            self.bias_factors = bias_func_values / obj_func_values
513            self.bias_points = deepcopy(self.state)
514            self.state_func_values *= self._bias_correction(initial_state)
515        elif self.bias_adaptive is not None and self.bias_adaptive > 0:  # update factors to account for new information
516            pass  # not implemented yet
517
518
519
520
521        
class Ensemble(ensemble.ensemble.Ensemble):
 17class Ensemble(PETEnsemble):
 18    """
 19    Class to store control states and evaluate objective functions.
 20
 21    Methods
 22    -------
 23    get_state()
 24        Returns control vector as ndarray
 25
 26    get_final_state(return_dict)
 27        Returns final control vector between [lb,ub]
 28
 29    get_cov()
 30        Returns the ensemble covariance matrix
 31
 32    function(x,*args)
 33        Objective function called during optimization
 34
 35    gradient(x,*args)
 36        Ensemble gradient
 37
 38    hessian(x,*args)
 39        Ensemble hessian
 40
 41    calc_ensemble_weights(self,x,*args):
 42        Calculate weights used in sequential monte carlo optimization
 43
 44    """
 45
 46    def __init__(self, keys_en, sim, obj_func):
 47        """
 48        Parameters
 49        ----------
 50        keys_en : dict
 51            Options for the ensemble class
 52
 53            - disable_tqdm: supress tqdm progress bar for clean output in the notebook
 54            - ne: number of perturbations used to compute the gradient
 55            - state: name of state variables passed to the .mako file
 56            - prior_<name>: the prior information the state variables, including mean, variance and variable limits
 57            - num_models: number of models (if robust optimization) (default 1)
 58            - transform: transform variables to [0,1] if true (default true)
 59
 60        sim : callable
 61            The forward simulator (e.g. flow)
 62
 63        obj_func : callable
 64            The objective function (e.g. npv)
 65        """
 66
 67        # Initialize PETEnsemble
 68        super(Ensemble, self).__init__(keys_en, sim)
 69        
 70        def __set__variable(var_name=None, defalut=None):
 71            if var_name in keys_en:
 72                return keys_en[var_name]
 73            else:
 74                return defalut
 75
 76        # Set number of models (default 1)
 77        self.num_models = __set__variable('num_models', 1)
 78
 79        # Set transform flag (defalult True)
 80        self.transform = __set__variable('transform', True)
 81
 82        # Number of samples to compute gradient
 83        self.num_samples = self.ne
 84
 85        # We need the limits to convert between [0, 1] and [lb, ub],
 86        # and we need the bounds as list of (min, max) pairs
 87        # Also set the state and covarianve equal to the values provided in the input.
 88        self.upper_bound = []
 89        self.lower_bound = []
 90        self.bounds = []
 91        self.cov = np.array([])
 92        for name in self.prior_info.keys():
 93            self.state[name] = np.asarray(self.prior_info[name]['mean'])
 94            num_state_var = len(self.state[name])
 95            value_cov = self.prior_info[name]['variance'] * np.ones((num_state_var,))
 96            if 'limits' in self.prior_info[name].keys():
 97                lb = self.prior_info[name]['limits'][0]
 98                ub = self.prior_info[name]['limits'][1]
 99                self.lower_bound.append(lb)
100                self.upper_bound.append(ub)
101                if self.transform:
102                    value_cov = value_cov / (ub - lb)**2
103                    np.clip(value_cov, 0, 1, out=value_cov)
104                    self.bounds += num_state_var*[(0, 1)]
105                else:
106                    self.bounds += num_state_var*[(lb, ub)]
107            else:
108                self.bounds += num_state_var*[(None, None)]
109            self.cov = np.append(self.cov, value_cov)
110
111        self._scale_state()
112        self.cov = np.diag(self.cov)
113
114        # Set objective function (callable)
115        self.obj_func = obj_func
116
117        # Objective function values
118        self.state_func_values = None
119        self.ens_func_values = None
120
121        # Inflation factor used in SmcOpt
122        self.inflation_factor = None
123        self.survival_factor = None
124        self.particles = np.empty((self.cov.shape[0],0))
125        self.particle_values = np.empty((0))
126
127        # Initialize variables for bias correction
128        if 'bias_file' in self.sim.input_dict:  # use bias correction
129            self.bias_file = self.sim.input_dict['bias_file'].upper()  # mako file for simulations
130        else:
131            self.bias_file = None
132        self.bias_adaptive = None  # flag to adaptively update the bias correction (not implemented yet)
133        self.bias_factors = None  # this is J(x_j,m_j)/J(x_j,m)
134        self.bias_weights = np.ones(self.num_samples) / self.num_samples  # initialize with equal weights
135        self.bias_points = None  # this is the points used to estimate the bias correction
136
137        # Setup GenOpt
138        self.genopt = GenOptDistribution(self.get_state(), 
139                                         self.get_cov(), 
140                                         func=self.function, 
141                                         ne=self.num_samples)
142
143    def get_state(self):
144        """
145        Returns
146        -------
147        x : numpy.ndarray
148            Control vector as ndarray, shape (number of controls, number of perturbations)
149        """
150        x = ot.aug_optim_state(self.state, list(self.state.keys()))
151        return x
152
153    def get_cov(self):
154        """
155        Returns
156        -------
157        cov : numpy.ndarray
158            Covariance matrix, shape (number of controls, number of controls)
159        """
160
161        return self.cov
162
163    def get_bounds(self):
164        """
165        Returns
166        -------
167        bounds : list
168            (min, max) pairs for each element in x. None is used to specify no bound.
169        """
170
171        return self.bounds
172    
173    def get_final_state(self, return_dict=False):
174        """
175        Parameters
176        ----------
177        return_dict : bool
178            Retrun dictionary if true
179
180        Returns
181        -------
182        x : numpy.ndarray
183            Control vector as ndarray, shape (number of controls, number of perturbations)
184        """
185
186        self._invert_scale_state()
187        if return_dict:
188            x = self.state
189        else:
190            x = self.get_state()
191        return x
192
193    def function(self, x, *args, **kwargs):
194        """
195        This is the main function called during optimization.
196
197        Parameters
198        ----------
199        x : ndarray
200            Control vector, shape (number of controls, number of perturbations)
201
202        Returns
203        -------
204        obj_func_values : numpy.ndarray
205            Objective function values, shape (number of perturbations, )
206        """
207        self._aux_input()
208
209        if len(x.shape) == 1:
210            self.ne = self.num_models
211        else:
212            self.ne = x.shape[1]
213
214        self.state = ot.update_optim_state(x, self.state, list(self.state.keys()))  # go from nparray to dict
215        self._invert_scale_state()  # ensure that state is in [lb,ub]
216        run_success = self.calc_prediction()  # calculate flow data
217        self._scale_state()  # scale back to [0, 1]
218        if run_success:
219            func_values = self.obj_func(self.pred_data, input_dict=self.sim.input_dict,
220                                        true_order=self.sim.true_order, **kwargs)
221        else:
222            func_values = np.inf  # the simulations have crashed
223
224        if len(x.shape) == 1:
225            self.state_func_values = func_values
226        else:
227            self.ens_func_values = func_values
228        
229        return func_values
230
231    def gradient(self, x, *args, **kwargs):
232        r"""
233        Calculate the preconditioned gradient associated with ensemble, defined as:
234
235        .. math::
236            S \approx C_x \times G^T
237
238        where :math:`C_x` is the state covariance matrix, and :math:`G` is the standard
239        gradient. The ensemble sensitivity matrix is calculated as:
240
241        .. math::
242            S = X \times J^T /(N_e-1)
243
244        where :math:`X` and :math:`J` are ensemble matrices of :math:`x` (or control variables) and objective function
245        perturbed by their respective means. In practice (and in this method), :math:`S` is calculated by perturbing the
246        current control variable with Gaussian random numbers from :math:`N(0, C_x)` (giving :math:`X`), running
247        the generated ensemble (:math:`X`) through the simulator to give an ensemble of objective function values
248        (:math:`J`), and in the end calculate :math:`S`. Note that :math:`S` is an :math:`N_x \times 1` vector, where
249        :math:`N_x` is length of the control vector and the objective function is scalar.
250
251        Parameters
252        ----------
253        x : ndarray
254            Control vector, shape (number of controls, )
255
256        args : tuple
257            Covarice (:math:`C_x`), shape (number of controls, number of controls)
258
259        Returns
260        -------
261        gradient : numpy.ndarray
262                The gradient evaluated at x, shape (number of controls, )
263        """
264
265        # Set the ensemble state equal to the input control vector x
266        self.state = ot.update_optim_state(x, self.state, list(self.state.keys()))
267
268        # Set the covariance equal to the input
269        self.cov = args[0]
270
271        # If bias correction is used we need to temporarily store the initial state
272        initial_state = None
273        if self.bias_file is not None and self.bias_factors is None:  # first iteration
274            initial_state = deepcopy(self.state)  # store this to update current objective values
275
276        # Generate ensemble of states
277        self.ne = self.num_samples
278        nr = self._aux_input()
279        self.state = self._gen_state_ensemble()
280        
281        state_ens = at.aug_state(self.state, list(self.state.keys()))
282        self.function(state_ens, **kwargs)
283
284        # If bias correction is used we need to calculate the bias factors, J(u_j,m_j)/J(u_j,m)
285        if self.bias_file is not None:  # use bias corrections
286            self._bias_factors(self.ens_func_values, initial_state)
287
288        # Perturb state and function values with their mean
289        state_ens = at.aug_state(self.state, list(self.state.keys()))
290        pert_state = state_ens - np.dot(state_ens.mean(1)[:, None], np.ones((1, self.ne)))
291        if self.bias_file is not None:  # use bias corrections
292            self.ens_func_values *= self._bias_correction(self.state)
293            pert_obj_func = self.ens_func_values - np.mean(self.ens_func_values)
294        else:
295            pert_obj_func = self.ens_func_values - np.array(np.repeat(self.state_func_values, nr))
296
297        # Calculate the gradient
298        g_m = np.zeros(state_ens.shape[0])
299        for i in np.arange(self.ne):
300            g_m = g_m + pert_obj_func[i] * pert_state[:, i]
301
302        gradient = g_m / (self.ne - 1)
303
304        return gradient
305
306    def hessian(self, x=None, *args):
307        r"""
308        Calculate the hessian matrix associated with ensemble, defined as:
309
310        .. math::
311            H = J(XX^T - \Sigma)/ (N_e-1)
312
313        where :math:`X` and :math:`J` are ensemble matrices of :math:`x` (or control variables) and objective function
314        perturbed by their respective means.
315
316        Note: state and ens_func_values are assumed to already exist from computation of the gradient.
317        Save time by not running them again.
318
319        Parameters
320        ----------
321        x : ndarray
322            Control vector, shape (number of controls, number of perturbations)
323
324        Returns
325        -------
326        hessian: numpy.ndarray
327            The hessian evaluated at x, shape (number of controls, number of controls)
328
329        References
330        ----------
331        Zhang, Y., Stordal, A.S. & Lorentzen, R.J. A natural Hessian approximation for ensemble based optimization.
332        Comput Geosci 27, 355–364 (2023). https://doi.org/10.1007/s10596-022-10185-z
333        """
334
335        # Perturb state and function values with their mean
336        state_ens = at.aug_state(self.state, list(self.state.keys()))
337        pert_state = state_ens - np.dot(state_ens.mean(1)[:, None], np.ones((1, self.ne)))
338        nr = self._aux_input()
339        pert_obj_func = self.ens_func_values - np.array(np.repeat(self.state_func_values, nr))
340
341        # Calculate the gradient for mean and covariance matrix
342        g_c = np.zeros(self.cov.shape)
343        for i in np.arange(self.ne):
344            g_c = g_c + pert_obj_func[i] * (np.outer(pert_state[:, i], pert_state[:, i]) - self.cov)
345
346        hessian = g_c / (self.ne - 1)
347
348        return hessian
349    '''
350    def genopt_gradient(self, x, *args):
351        self.genopt.update_distribution(*args)
352        gradient = self.genopt.ensemble_gradient(func=self.function, 
353                                                 x=x, 
354                                                 ne=self.num_samples)
355        return gradient
356    
357    def genopt_mutation_gradient(self, x=None, *args, **kwargs):
358        return self.genopt.ensemble_mutation_gradient(return_ensembles=kwargs['return_ensembles'])
359    '''
360
361    def calc_ensemble_weights(self, x, *args, **kwargs):
362        r"""
363        Calculate weights used in sequential monte carlo optimization.
364
365        Parameters
366        ----------
367        x : ndarray
368            Control vector, shape (number of controls, )
369
370        args : tuple
371            Inflation factor, covariance (:math:`C_x`, shape (number of controls, number of controls)) and survival factor
372
373        Returns
374        -------
375        sens_matrix, best_ens, best_func : tuple
376                The weighted ensemble, the best ensemble member, and the best objective function value
377        """
378
379        # Set the ensemble state equal to the input control vector x
380        self.state = ot.update_optim_state(x, self.state, list(self.state.keys()))
381
382        # Set the inflation factor and covariance equal to the input
383        self.inflation_factor = args[0]
384        self.cov = args[1]
385        self.survival_factor = args[2]
386
387        # If bias correction is used we need to temporarily store the initial state
388        initial_state = None
389        if self.bias_file is not None and self.bias_factors is None:  # first iteration
390            initial_state = deepcopy(self.state)  # store this to update current objective values
391
392        # Generate ensemble of states
393        if self.particles.shape[1] == 0:
394            self.ne = self.num_samples
395        else:
396            self.ne = int(np.round(self.num_samples*self.survival_factor))
397        self._aux_input()
398        self.state = self._gen_state_ensemble()
399
400        self._invert_scale_state()  # ensure that state is in [lb,ub]
401        self.calc_prediction()  # calculate flow data
402        self._scale_state()  # scale back to [0, 1]
403
404        #self.ens_func_values = self.obj_func(self.pred_data, self.sim.input_dict, self.sim.true_order)
405        #self.ens_func_values = np.array(self.ens_func_values)
406        state_ens = at.aug_state(self.state, list(self.state.keys()))
407        self.function(state_ens, **kwargs)
408
409        self.particles = np.hstack((self.particles, state_ens))
410        self.particle_values = np.hstack((self.particle_values,self.ens_func_values))
411
412        # If bias correction is used we need to calculate the bias factors, J(u_j,m_j)/J(u_j,m)
413        if self.bias_file is not None:  # use bias corrections
414            self._bias_factors(self.ens_func_values, initial_state)
415
416        # Calculate the weights and ensemble sensitivity matrix
417        warnings.filterwarnings('ignore')  # suppress warnings
418        weights = np.zeros(self.num_samples)
419        for i in np.arange(self.num_samples):
420            weights[i] = np.exp(-(self.particle_values[i]-np.min(self.particle_values))*self.inflation_factor)
421
422        weights = weights + 0.000001
423        weights = weights/np.sum(weights)  # TODO: Sjekke at disse er riktig
424
425        sens_matrix = self.particles @ weights
426        index = np.argmin(self.particle_values)
427        best_ens = self.particles[:, index]
428        best_func = self.particle_values[index]
429        resample_index = np.random.choice(self.num_samples,int(np.round(self.num_samples-
430                                          self.num_samples*self.survival_factor)),replace=True,p=weights)
431        self.particles = self.particles[:, resample_index]
432        self.particle_values = self.particle_values[resample_index]
433        return sens_matrix, best_ens, best_func
434
435    def _gen_state_ensemble(self):
436        """
437        Generate ensemble with the current state (control variable) as the mean and using the covariance matrix
438        """
439
440        state_en = {}
441        cov_blocks = ot.corr2BlockDiagonal(self.state, self.cov)
442        for i, statename in enumerate(self.state.keys()):
443            mean = self.state[statename]
444            cov = cov_blocks[i]
445            temp_state_en = np.random.multivariate_normal(mean, cov, self.ne).transpose()
446            shifted_ensemble = np.array([mean]).T + temp_state_en - np.array([np.mean(temp_state_en, 1)]).T
447            if self.upper_bound and self.lower_bound:
448                if self.transform:
449                    np.clip(shifted_ensemble, 0, 1, out=shifted_ensemble)
450                else:
451                    np.clip(shifted_ensemble, self.lower_bound[i], self.upper_bound[i], out=shifted_ensemble)
452            state_en[statename] = shifted_ensemble
453
454        return state_en
455
456    def _aux_input(self):
457        """
458        Set the auxiliary input used for multiple geological realizations
459        """
460
461        nr = 1  # nr is the ratio of samples over models
462        if self.num_models > 1:
463            if np.remainder(self.num_samples, self.num_models) == 0:
464                nr = int(self.num_samples / self.num_models)
465                self.aux_input = list(np.repeat(np.arange(self.num_models), nr))
466            else:
467                print('num_samples must be a multiplum of num_models!')
468                sys.exit(0)
469        return nr
470
471    def _scale_state(self):
472        """
473        Transform the internal state from [lb, ub] to [0, 1]
474        """
475        if self.transform and (self.upper_bound and self.lower_bound):
476            for i, key in enumerate(self.state):
477                self.state[key] = (self.state[key] - self.lower_bound[i])/(self.upper_bound[i] - self.lower_bound[i])
478                np.clip(self.state[key], 0, 1, out=self.state[key])
479
480    def _invert_scale_state(self):
481        """
482        Transform the internal state from [0, 1] to [lb, ub]
483        """
484        if self.transform and (self.upper_bound and self.lower_bound):
485            for i, key in enumerate(self.state):
486                if self.transform:
487                    self.state[key] = self.lower_bound[i] + self.state[key]*(self.upper_bound[i] - self.lower_bound[i])
488                np.clip(self.state[key], self.lower_bound[i], self.upper_bound[i], out=self.state[key])
489
490    def _bias_correction(self, state):
491        """
492        Calculate bias correction. Currently, the bias correction is a constant independent of the state
493        """
494        if self.bias_factors is not None:
495            return np.sum(self.bias_weights * self.bias_factors)
496        else:
497            return 1
498
499    def _bias_factors(self, obj_func_values, initial_state):
500        """
501        Function for computing the bias factors
502        """
503
504        if self.bias_factors is None:  # first iteration
505            currentfile = self.sim.file
506            self.sim.file = self.bias_file
507            self.ne = self.num_samples
508            self.aux_input = list(np.arange(self.ne))
509            self.calc_prediction()
510            self.sim.file = currentfile
511            bias_func_values = self.obj_func(self.pred_data, self.sim.input_dict, self.sim.true_order)
512            bias_func_values = np.array(bias_func_values)
513            self.bias_factors = bias_func_values / obj_func_values
514            self.bias_points = deepcopy(self.state)
515            self.state_func_values *= self._bias_correction(initial_state)
516        elif self.bias_adaptive is not None and self.bias_adaptive > 0:  # update factors to account for new information
517            pass  # not implemented yet

Class to store control states and evaluate objective functions.

Methods

get_state() Returns control vector as ndarray

get_final_state(return_dict) Returns final control vector between [lb,ub]

get_cov() Returns the ensemble covariance matrix

function(x,*args) Objective function called during optimization

gradient(x,*args) Ensemble gradient

hessian(x,*args) Ensemble hessian

calc_ensemble_weights(self,x,*args): Calculate weights used in sequential monte carlo optimization

Ensemble(keys_en, sim, obj_func)
 46    def __init__(self, keys_en, sim, obj_func):
 47        """
 48        Parameters
 49        ----------
 50        keys_en : dict
 51            Options for the ensemble class
 52
 53            - disable_tqdm: supress tqdm progress bar for clean output in the notebook
 54            - ne: number of perturbations used to compute the gradient
 55            - state: name of state variables passed to the .mako file
 56            - prior_<name>: the prior information the state variables, including mean, variance and variable limits
 57            - num_models: number of models (if robust optimization) (default 1)
 58            - transform: transform variables to [0,1] if true (default true)
 59
 60        sim : callable
 61            The forward simulator (e.g. flow)
 62
 63        obj_func : callable
 64            The objective function (e.g. npv)
 65        """
 66
 67        # Initialize PETEnsemble
 68        super(Ensemble, self).__init__(keys_en, sim)
 69        
 70        def __set__variable(var_name=None, defalut=None):
 71            if var_name in keys_en:
 72                return keys_en[var_name]
 73            else:
 74                return defalut
 75
 76        # Set number of models (default 1)
 77        self.num_models = __set__variable('num_models', 1)
 78
 79        # Set transform flag (defalult True)
 80        self.transform = __set__variable('transform', True)
 81
 82        # Number of samples to compute gradient
 83        self.num_samples = self.ne
 84
 85        # We need the limits to convert between [0, 1] and [lb, ub],
 86        # and we need the bounds as list of (min, max) pairs
 87        # Also set the state and covarianve equal to the values provided in the input.
 88        self.upper_bound = []
 89        self.lower_bound = []
 90        self.bounds = []
 91        self.cov = np.array([])
 92        for name in self.prior_info.keys():
 93            self.state[name] = np.asarray(self.prior_info[name]['mean'])
 94            num_state_var = len(self.state[name])
 95            value_cov = self.prior_info[name]['variance'] * np.ones((num_state_var,))
 96            if 'limits' in self.prior_info[name].keys():
 97                lb = self.prior_info[name]['limits'][0]
 98                ub = self.prior_info[name]['limits'][1]
 99                self.lower_bound.append(lb)
100                self.upper_bound.append(ub)
101                if self.transform:
102                    value_cov = value_cov / (ub - lb)**2
103                    np.clip(value_cov, 0, 1, out=value_cov)
104                    self.bounds += num_state_var*[(0, 1)]
105                else:
106                    self.bounds += num_state_var*[(lb, ub)]
107            else:
108                self.bounds += num_state_var*[(None, None)]
109            self.cov = np.append(self.cov, value_cov)
110
111        self._scale_state()
112        self.cov = np.diag(self.cov)
113
114        # Set objective function (callable)
115        self.obj_func = obj_func
116
117        # Objective function values
118        self.state_func_values = None
119        self.ens_func_values = None
120
121        # Inflation factor used in SmcOpt
122        self.inflation_factor = None
123        self.survival_factor = None
124        self.particles = np.empty((self.cov.shape[0],0))
125        self.particle_values = np.empty((0))
126
127        # Initialize variables for bias correction
128        if 'bias_file' in self.sim.input_dict:  # use bias correction
129            self.bias_file = self.sim.input_dict['bias_file'].upper()  # mako file for simulations
130        else:
131            self.bias_file = None
132        self.bias_adaptive = None  # flag to adaptively update the bias correction (not implemented yet)
133        self.bias_factors = None  # this is J(x_j,m_j)/J(x_j,m)
134        self.bias_weights = np.ones(self.num_samples) / self.num_samples  # initialize with equal weights
135        self.bias_points = None  # this is the points used to estimate the bias correction
136
137        # Setup GenOpt
138        self.genopt = GenOptDistribution(self.get_state(), 
139                                         self.get_cov(), 
140                                         func=self.function, 
141                                         ne=self.num_samples)
Parameters
  • keys_en (dict): Options for the ensemble class

    • disable_tqdm: supress tqdm progress bar for clean output in the notebook
    • ne: number of perturbations used to compute the gradient
    • state: name of state variables passed to the .mako file
    • prior_: the prior information the state variables, including mean, variance and variable limits
    • num_models: number of models (if robust optimization) (default 1)
    • transform: transform variables to [0,1] if true (default true)
  • sim (callable): The forward simulator (e.g. flow)
  • obj_func (callable): The objective function (e.g. npv)
num_models
transform
num_samples
upper_bound
lower_bound
bounds
cov
obj_func
state_func_values
ens_func_values
inflation_factor
survival_factor
particles
particle_values
bias_adaptive
bias_factors
bias_weights
bias_points
genopt
def get_state(self):
143    def get_state(self):
144        """
145        Returns
146        -------
147        x : numpy.ndarray
148            Control vector as ndarray, shape (number of controls, number of perturbations)
149        """
150        x = ot.aug_optim_state(self.state, list(self.state.keys()))
151        return x
Returns
  • x (numpy.ndarray): Control vector as ndarray, shape (number of controls, number of perturbations)
def get_cov(self):
153    def get_cov(self):
154        """
155        Returns
156        -------
157        cov : numpy.ndarray
158            Covariance matrix, shape (number of controls, number of controls)
159        """
160
161        return self.cov
Returns
  • cov (numpy.ndarray): Covariance matrix, shape (number of controls, number of controls)
def get_bounds(self):
163    def get_bounds(self):
164        """
165        Returns
166        -------
167        bounds : list
168            (min, max) pairs for each element in x. None is used to specify no bound.
169        """
170
171        return self.bounds
Returns
  • bounds (list): (min, max) pairs for each element in x. None is used to specify no bound.
def get_final_state(self, return_dict=False):
173    def get_final_state(self, return_dict=False):
174        """
175        Parameters
176        ----------
177        return_dict : bool
178            Retrun dictionary if true
179
180        Returns
181        -------
182        x : numpy.ndarray
183            Control vector as ndarray, shape (number of controls, number of perturbations)
184        """
185
186        self._invert_scale_state()
187        if return_dict:
188            x = self.state
189        else:
190            x = self.get_state()
191        return x
Parameters
  • return_dict (bool): Retrun dictionary if true
Returns
  • x (numpy.ndarray): Control vector as ndarray, shape (number of controls, number of perturbations)
def function(self, x, *args, **kwargs):
193    def function(self, x, *args, **kwargs):
194        """
195        This is the main function called during optimization.
196
197        Parameters
198        ----------
199        x : ndarray
200            Control vector, shape (number of controls, number of perturbations)
201
202        Returns
203        -------
204        obj_func_values : numpy.ndarray
205            Objective function values, shape (number of perturbations, )
206        """
207        self._aux_input()
208
209        if len(x.shape) == 1:
210            self.ne = self.num_models
211        else:
212            self.ne = x.shape[1]
213
214        self.state = ot.update_optim_state(x, self.state, list(self.state.keys()))  # go from nparray to dict
215        self._invert_scale_state()  # ensure that state is in [lb,ub]
216        run_success = self.calc_prediction()  # calculate flow data
217        self._scale_state()  # scale back to [0, 1]
218        if run_success:
219            func_values = self.obj_func(self.pred_data, input_dict=self.sim.input_dict,
220                                        true_order=self.sim.true_order, **kwargs)
221        else:
222            func_values = np.inf  # the simulations have crashed
223
224        if len(x.shape) == 1:
225            self.state_func_values = func_values
226        else:
227            self.ens_func_values = func_values
228        
229        return func_values

This is the main function called during optimization.

Parameters
  • x (ndarray): Control vector, shape (number of controls, number of perturbations)
Returns
  • obj_func_values (numpy.ndarray): Objective function values, shape (number of perturbations, )
def gradient(self, x, *args, **kwargs):
231    def gradient(self, x, *args, **kwargs):
232        r"""
233        Calculate the preconditioned gradient associated with ensemble, defined as:
234
235        .. math::
236            S \approx C_x \times G^T
237
238        where :math:`C_x` is the state covariance matrix, and :math:`G` is the standard
239        gradient. The ensemble sensitivity matrix is calculated as:
240
241        .. math::
242            S = X \times J^T /(N_e-1)
243
244        where :math:`X` and :math:`J` are ensemble matrices of :math:`x` (or control variables) and objective function
245        perturbed by their respective means. In practice (and in this method), :math:`S` is calculated by perturbing the
246        current control variable with Gaussian random numbers from :math:`N(0, C_x)` (giving :math:`X`), running
247        the generated ensemble (:math:`X`) through the simulator to give an ensemble of objective function values
248        (:math:`J`), and in the end calculate :math:`S`. Note that :math:`S` is an :math:`N_x \times 1` vector, where
249        :math:`N_x` is length of the control vector and the objective function is scalar.
250
251        Parameters
252        ----------
253        x : ndarray
254            Control vector, shape (number of controls, )
255
256        args : tuple
257            Covarice (:math:`C_x`), shape (number of controls, number of controls)
258
259        Returns
260        -------
261        gradient : numpy.ndarray
262                The gradient evaluated at x, shape (number of controls, )
263        """
264
265        # Set the ensemble state equal to the input control vector x
266        self.state = ot.update_optim_state(x, self.state, list(self.state.keys()))
267
268        # Set the covariance equal to the input
269        self.cov = args[0]
270
271        # If bias correction is used we need to temporarily store the initial state
272        initial_state = None
273        if self.bias_file is not None and self.bias_factors is None:  # first iteration
274            initial_state = deepcopy(self.state)  # store this to update current objective values
275
276        # Generate ensemble of states
277        self.ne = self.num_samples
278        nr = self._aux_input()
279        self.state = self._gen_state_ensemble()
280        
281        state_ens = at.aug_state(self.state, list(self.state.keys()))
282        self.function(state_ens, **kwargs)
283
284        # If bias correction is used we need to calculate the bias factors, J(u_j,m_j)/J(u_j,m)
285        if self.bias_file is not None:  # use bias corrections
286            self._bias_factors(self.ens_func_values, initial_state)
287
288        # Perturb state and function values with their mean
289        state_ens = at.aug_state(self.state, list(self.state.keys()))
290        pert_state = state_ens - np.dot(state_ens.mean(1)[:, None], np.ones((1, self.ne)))
291        if self.bias_file is not None:  # use bias corrections
292            self.ens_func_values *= self._bias_correction(self.state)
293            pert_obj_func = self.ens_func_values - np.mean(self.ens_func_values)
294        else:
295            pert_obj_func = self.ens_func_values - np.array(np.repeat(self.state_func_values, nr))
296
297        # Calculate the gradient
298        g_m = np.zeros(state_ens.shape[0])
299        for i in np.arange(self.ne):
300            g_m = g_m + pert_obj_func[i] * pert_state[:, i]
301
302        gradient = g_m / (self.ne - 1)
303
304        return gradient

Calculate the preconditioned gradient associated with ensemble, defined as:

$$S \approx C_x \times G^T$$

where \( C_x \) is the state covariance matrix, and \( G \) is the standard gradient. The ensemble sensitivity matrix is calculated as:

$$S = X \times J^T /(N_e-1)$$

where \( X \) and \( J \) are ensemble matrices of \( x \) (or control variables) and objective function perturbed by their respective means. In practice (and in this method), \( S \) is calculated by perturbing the current control variable with Gaussian random numbers from \( N(0, C_x) \) (giving \( X \)), running the generated ensemble (\( X \)) through the simulator to give an ensemble of objective function values (\( J \)), and in the end calculate \( S \). Note that \( S \) is an \( N_x \times 1 \) vector, where \( N_x \) is length of the control vector and the objective function is scalar.

Parameters
  • x (ndarray): Control vector, shape (number of controls, )
  • args (tuple): Covarice (\( C_x \)), shape (number of controls, number of controls)
Returns
  • gradient (numpy.ndarray): The gradient evaluated at x, shape (number of controls, )
def hessian(self, x=None, *args):
306    def hessian(self, x=None, *args):
307        r"""
308        Calculate the hessian matrix associated with ensemble, defined as:
309
310        .. math::
311            H = J(XX^T - \Sigma)/ (N_e-1)
312
313        where :math:`X` and :math:`J` are ensemble matrices of :math:`x` (or control variables) and objective function
314        perturbed by their respective means.
315
316        Note: state and ens_func_values are assumed to already exist from computation of the gradient.
317        Save time by not running them again.
318
319        Parameters
320        ----------
321        x : ndarray
322            Control vector, shape (number of controls, number of perturbations)
323
324        Returns
325        -------
326        hessian: numpy.ndarray
327            The hessian evaluated at x, shape (number of controls, number of controls)
328
329        References
330        ----------
331        Zhang, Y., Stordal, A.S. & Lorentzen, R.J. A natural Hessian approximation for ensemble based optimization.
332        Comput Geosci 27, 355–364 (2023). https://doi.org/10.1007/s10596-022-10185-z
333        """
334
335        # Perturb state and function values with their mean
336        state_ens = at.aug_state(self.state, list(self.state.keys()))
337        pert_state = state_ens - np.dot(state_ens.mean(1)[:, None], np.ones((1, self.ne)))
338        nr = self._aux_input()
339        pert_obj_func = self.ens_func_values - np.array(np.repeat(self.state_func_values, nr))
340
341        # Calculate the gradient for mean and covariance matrix
342        g_c = np.zeros(self.cov.shape)
343        for i in np.arange(self.ne):
344            g_c = g_c + pert_obj_func[i] * (np.outer(pert_state[:, i], pert_state[:, i]) - self.cov)
345
346        hessian = g_c / (self.ne - 1)
347
348        return hessian

Calculate the hessian matrix associated with ensemble, defined as:

$$H = J(XX^T - \Sigma)/ (N_e-1)$$

where \( X \) and \( J \) are ensemble matrices of \( x \) (or control variables) and objective function perturbed by their respective means.

Note: state and ens_func_values are assumed to already exist from computation of the gradient. Save time by not running them again.

Parameters
  • x (ndarray): Control vector, shape (number of controls, number of perturbations)
Returns
  • hessian (numpy.ndarray): The hessian evaluated at x, shape (number of controls, number of controls)
References

Zhang, Y., Stordal, A.S. & Lorentzen, R.J. A natural Hessian approximation for ensemble based optimization. Comput Geosci 27, 355–364 (2023). https://doi.org/10.1007/s10596-022-10185-z

def calc_ensemble_weights(self, x, *args, **kwargs):
361    def calc_ensemble_weights(self, x, *args, **kwargs):
362        r"""
363        Calculate weights used in sequential monte carlo optimization.
364
365        Parameters
366        ----------
367        x : ndarray
368            Control vector, shape (number of controls, )
369
370        args : tuple
371            Inflation factor, covariance (:math:`C_x`, shape (number of controls, number of controls)) and survival factor
372
373        Returns
374        -------
375        sens_matrix, best_ens, best_func : tuple
376                The weighted ensemble, the best ensemble member, and the best objective function value
377        """
378
379        # Set the ensemble state equal to the input control vector x
380        self.state = ot.update_optim_state(x, self.state, list(self.state.keys()))
381
382        # Set the inflation factor and covariance equal to the input
383        self.inflation_factor = args[0]
384        self.cov = args[1]
385        self.survival_factor = args[2]
386
387        # If bias correction is used we need to temporarily store the initial state
388        initial_state = None
389        if self.bias_file is not None and self.bias_factors is None:  # first iteration
390            initial_state = deepcopy(self.state)  # store this to update current objective values
391
392        # Generate ensemble of states
393        if self.particles.shape[1] == 0:
394            self.ne = self.num_samples
395        else:
396            self.ne = int(np.round(self.num_samples*self.survival_factor))
397        self._aux_input()
398        self.state = self._gen_state_ensemble()
399
400        self._invert_scale_state()  # ensure that state is in [lb,ub]
401        self.calc_prediction()  # calculate flow data
402        self._scale_state()  # scale back to [0, 1]
403
404        #self.ens_func_values = self.obj_func(self.pred_data, self.sim.input_dict, self.sim.true_order)
405        #self.ens_func_values = np.array(self.ens_func_values)
406        state_ens = at.aug_state(self.state, list(self.state.keys()))
407        self.function(state_ens, **kwargs)
408
409        self.particles = np.hstack((self.particles, state_ens))
410        self.particle_values = np.hstack((self.particle_values,self.ens_func_values))
411
412        # If bias correction is used we need to calculate the bias factors, J(u_j,m_j)/J(u_j,m)
413        if self.bias_file is not None:  # use bias corrections
414            self._bias_factors(self.ens_func_values, initial_state)
415
416        # Calculate the weights and ensemble sensitivity matrix
417        warnings.filterwarnings('ignore')  # suppress warnings
418        weights = np.zeros(self.num_samples)
419        for i in np.arange(self.num_samples):
420            weights[i] = np.exp(-(self.particle_values[i]-np.min(self.particle_values))*self.inflation_factor)
421
422        weights = weights + 0.000001
423        weights = weights/np.sum(weights)  # TODO: Sjekke at disse er riktig
424
425        sens_matrix = self.particles @ weights
426        index = np.argmin(self.particle_values)
427        best_ens = self.particles[:, index]
428        best_func = self.particle_values[index]
429        resample_index = np.random.choice(self.num_samples,int(np.round(self.num_samples-
430                                          self.num_samples*self.survival_factor)),replace=True,p=weights)
431        self.particles = self.particles[:, resample_index]
432        self.particle_values = self.particle_values[resample_index]
433        return sens_matrix, best_ens, best_func

Calculate weights used in sequential monte carlo optimization.

Parameters
  • x (ndarray): Control vector, shape (number of controls, )
  • args (tuple): Inflation factor, covariance (\( C_x \), shape (number of controls, number of controls)) and survival factor
Returns
  • sens_matrix, best_ens, best_func (tuple): The weighted ensemble, the best ensemble member, and the best objective function value