From 49fe1190e8d90e5da6b160476f1f7aa0703dca25 Mon Sep 17 00:00:00 2001 From: William Anderson <53445030+andersonw1@users.noreply.github.com> Date: Wed, 13 Nov 2024 11:39:44 -0800 Subject: [PATCH] Adding ability to define test space through convex hull (#18) * Track losses during training * Add convex hull parameter space option * Documentation in numpy style --- examples/burgers1d.yml | 21 ++++++++ src/lasdi/gplasdi.py | 15 +++++- src/lasdi/param.py | 111 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 142 insertions(+), 5 deletions(-) diff --git a/examples/burgers1d.yml b/examples/burgers1d.yml index 098945d..dc43164 100644 --- a/examples/burgers1d.yml +++ b/examples/burgers1d.yml @@ -41,6 +41,27 @@ parameter_space: test_space: type: grid +## An example if we want to provide training points on exterior +## of region and train in convex hull of training points +# parameter_space: +# parameters: +# - name: a +# min: 0.7 +# max: 0.9 +# test_space_type: list +# sample_size: 21 +# list: [0.70, 0.725, 0.75, 0.800, 0.85, 0.90] +# log_scale: false +# - name: w +# min: 0.9 +# max: 1.1 +# test_space_type: list +# sample_size: 21 +# list: [0.90, 0.970, 1.00, 0.925, 0.98, 1.10] +# log_scale: false +# test_space: +# type: hull + latent_space: type: ae ae: diff --git a/src/lasdi/gplasdi.py b/src/lasdi/gplasdi.py index ac3d046..3e156d4 100644 --- a/src/lasdi/gplasdi.py +++ b/src/lasdi/gplasdi.py @@ -163,6 +163,11 @@ def train(self): n_train = ps.n_train() ld = self.latent_dynamics + self.training_loss = [] + self.ae_loss = [] + self.ld_loss = [] + self.coef_loss = [] + ''' determine number of iterations. Perform n_iter iterations until overall iterations hit max_iter. @@ -184,6 +189,11 @@ def train(self): loss = loss_ae + self.ld_weight * loss_ld / n_train + self.coef_weight * loss_coef / n_train + self.training_loss.append(loss.item()) + self.ae_loss.append(loss_ae.item()) + self.ld_loss.append(loss_ld.item()) + self.coef_loss.append(loss_coef.item()) + loss.backward() self.optimizer.step() @@ -267,7 +277,8 @@ def export(self): dict_ = {'X_train': self.X_train, 'X_test': self.X_test, 'lr': self.lr, 'n_iter': self.n_iter, 'n_samples' : self.n_samples, 'best_coefs': self.best_coefs, 'max_iter': self.max_iter, 'max_iter': self.max_iter, 'ld_weight': self.ld_weight, 'coef_weight': self.coef_weight, - 'restart_iter': self.restart_iter, 'timer': self.timer.export(), 'optimizer': self.optimizer.state_dict() + 'restart_iter': self.restart_iter, 'timer': self.timer.export(), 'optimizer': self.optimizer.state_dict(), + 'training_loss' : self.training_loss, 'ae_loss' : self.ae_loss, 'ld_loss' : self.ld_loss, 'coeff_loss' : self.coef_loss } return dict_ @@ -280,4 +291,4 @@ def load(self, dict_): self.optimizer.load_state_dict(dict_['optimizer']) if (self.device != 'cpu'): optimizer_to(self.optimizer, self.device) - return \ No newline at end of file + return diff --git a/src/lasdi/param.py b/src/lasdi/param.py index a8a2223..38ca2e3 100644 --- a/src/lasdi/param.py +++ b/src/lasdi/param.py @@ -1,4 +1,5 @@ import numpy as np +from scipy.spatial import Delaunay from .inputs import InputParser def get_1dspace_from_list(config): @@ -40,12 +41,18 @@ def __init__(self, config): for param in self.param_list: self.param_name += [param['name']] - self.train_space = self.createInitialTrainSpace(self.param_list) - self.n_init = self.train_space.shape[0] - test_space_type = parser.getInput(['test_space', 'type'], datatype=str) if (test_space_type == 'grid'): + self.train_space = self.createInitialTrainSpace(self.param_list) + self.n_init = self.train_space.shape[0] + self.test_grid_sizes, self.test_meshgrid, self.test_space = self.createTestGridSpace(self.param_list) + if (test_space_type == 'hull'): + assert self.n_param >=2, 'Must have at least 2 parameters if test_space is \'hull\' ' + self.train_space = self.createInitialTrainSpaceForHull(self.param_list) + self.n_init = self.train_space.shape[0] + + self.test_grid_sizes, self.test_meshgrid, self.test_space = self.createTestHullSpace(self.param_list) return @@ -66,6 +73,38 @@ def createInitialTrainSpace(self, param_list): mesh_grids = self.createHyperMeshGrid(paramRanges) return self.createHyperGridSpace(mesh_grids) + def createInitialTrainSpaceForHull(self, param_list): + ''' + Concatenates the provided lists of training points into a 2D array. + + Arguments + --------- + param_list : :obj:`list(dict)` + A list of parameter dictionaries + + Returns + ------- + mesh_grids : :obj:`numpy.array` + np.array of size [d, k], where d is the number of points provided on the exterior of + the training space and k is the number of parameters (k == len(param_list)). + ''' + + paramRanges = [] + + for k, param in enumerate(param_list): + + _, paramRange = getParam1DSpace['list'](param) + paramRanges += [paramRange] + + if k > 0: + assert (len(paramRanges[k])==len(paramRanges[k - 1])), (f'Training parameters {k} and {k-1} have ' + 'different lengths. All training parameters ' + 'must have same length when test_space is \'hull\'.') + + + mesh_grids = np.vstack((paramRanges)).T + return mesh_grids + def createTestGridSpace(self, param_list): paramRanges = [] gridSizes = [] @@ -78,6 +117,72 @@ def createTestGridSpace(self, param_list): mesh_grids = self.createHyperMeshGrid(paramRanges) return gridSizes, mesh_grids, self.createHyperGridSpace(mesh_grids) + def createTestGridSpaceForHull(self, param_list): + ''' + Builds an initial uniform grid for the testing parameters when the test_space is 'hull'. + + Arguments + --------- + param_list : :obj:`list(dict)` + A list of parameter dictionaries + + Returns + ------- + gridSizes : :obj:`list(int)` + A list containing the number of elements on the grid in each parameter. + mesh_grids : :obj:`numpy.array` + tuple of numpy nd arrays, corresponding to each parameter. + Dimension of the array equals to the number of parameters. + param_grid : :obj:`numpy.array` + numpy 2d array of size (grid size x number of parameters). + ''' + + paramRanges = [] + gridSizes = [] + + for param in param_list: + Nx, paramRange = getParam1DSpace['uniform'](param) + gridSizes += [Nx] + paramRanges += [paramRange] + + mesh_grids = self.createHyperMeshGrid(paramRanges) + return gridSizes, mesh_grids, self.createHyperGridSpace(mesh_grids) + + def createTestHullSpace(self, param_list): + ''' + This function builds an initial uniform grid for the testing parameters, and then + returns any testing points which are within the convex hull of the provided + training parameters. + + Arguments + --------- + param_list : :obj:`list(dict)` + A list of parameter dictionaries + + Returns + ------- + gridSizes : :obj:`list(int)` + A list containing the number of elements on the grid in each parameter. + mesh_grids : :obj:`numpy.array` + tuple of numpy nd arrays, corresponding to each parameter. + Dimension of the array equals to the number of parameters. + test_space : :obj:`numpy.array` + numpy 2d array of size [d, k], where d is the number of testing points within + convex hull of the training space and k is the number of parameters (k == len(param_list)). + ''' + + # Get the initial uniform grid over the training parameters + gridSizes, mesh_grids, test_space = self.createTestGridSpaceForHull(param_list) + + # Mesh the training space. This will be slow in higher dimensions + cloud = Delaunay(self.train_space) + # Determine which test points are contained in the convex hull of training points + mask = cloud.find_simplex(test_space)>=0 + # Only keep testing points in the convex hull of training points + test_space = test_space[mask] + + return gridSizes, mesh_grids, test_space + def getParameter(self, param_vector): ''' convert numpy array parameter vector to a dict.