GitHub

romtools.vector_space.utils.truncater

Constructing a basis via POD typically entails computing the SVD of a snapshot matrix, $$ \mathbf{U} ,\mathbf{\Sigma} = \mathrm{svd}(\mathbf{S})$$ and then selecting the first $K$ left singular vectors (i.e., the first $K$ columns of $\mathbf{U}$). Typically, $K$ is determined through the decay of the singular values.

The truncater class is desined to truncate a basis. We provide concrete implementations that truncate based on a specified number of basis vectors and the decay of the singular values

  1#
  2# ************************************************************************
  3#
  4#                         ROM Tools and Workflows
  5# Copyright 2019 National Technology & Engineering Solutions of Sandia,LLC
  6#                              (NTESS)
  7#
  8# Under the terms of Contract DE-NA0003525 with NTESS, the
  9# U.S. Government retains certain rights in this software.
 10#
 11# ROM Tools and Workflows is licensed under BSD-3-Clause terms of use:
 12#
 13# Redistribution and use in source and binary forms, with or without
 14# modification, are permitted provided that the following conditions
 15# are met:
 16#
 17# 1. Redistributions of source code must retain the above copyright
 18# notice, this list of conditions and the following disclaimer.
 19#
 20# 2. Redistributions in binary form must reproduce the above copyright
 21# notice, this list of conditions and the following disclaimer in the
 22# documentation and/or other materials provided with the distribution.
 23#
 24# 3. Neither the name of the copyright holder nor the names of its
 25# contributors may be used to endorse or promote products derived
 26# from this software without specific prior written permission.
 27#
 28# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 29# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 30# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 31# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 32# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 33# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 34# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 35# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 36# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 37# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
 38# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 39# POSSIBILITY OF SUCH DAMAGE.
 40#
 41# Questions? Contact Eric Parish (ejparis@sandia.gov)
 42#
 43# ************************************************************************
 44#
 45
 46'''
 47Constructing a basis via POD typically entails computing the SVD of a snapshot matrix,
 48$$ \\mathbf{U} ,\\mathbf{\\Sigma} = \\mathrm{svd}(\\mathbf{S})$$
 49and then selecting the first $K$ left singular vectors (i.e., the first $K$
 50columns of $\\mathbf{U}$). Typically, $K$ is determined through the decay of
 51the singular values.
 52
 53The truncater class is desined to truncate a basis.
 54We provide concrete implementations that truncate based on a specified number
 55of basis vectors and the decay of the singular values
 56'''
 57
 58from typing import Protocol
 59import numpy as np
 60import romtools.linalg.linalg as la
 61
 62
 63class LeftSingularVectorTruncater(Protocol):
 64    '''
 65    Interface for the Truncater class.
 66    '''
 67
 68    def truncate(self, basis: np.ndarray, singular_values: np.ndarray) -> np.ndarray:
 69        '''
 70        Truncate left singular vectors
 71        '''
 72        ...
 73
 74
 75class NoOpTruncater():
 76    '''
 77    No op implementation
 78
 79    This class conforms to `LeftSingularVectorTruncater` protocol.
 80    '''
 81    def __init__(self) -> None:
 82        pass
 83
 84    def truncate(self, basis: np.ndarray, singular_values: np.ndarray) -> np.ndarray: # pylint: disable=unused-argument
 85        return basis
 86
 87
 88class BasisSizeTruncater():
 89    '''
 90    Truncates to a specified number of singular vectors, as specified in the constructor
 91
 92    This class conforms to `LeftSingularVectorTruncater` protocol.
 93    '''
 94    def __init__(self, basis_dimension: int) -> None:
 95        '''
 96        Constructor for the BasisSizeTruncater class.
 97
 98        Args:
 99            basis_dimension (int): The desired dimension of the truncated basis.
100        '''
101        # Check if basis dimension is less than or equal to zero
102        if basis_dimension <= 0:
103            raise ValueError('Given basis dimension is <= 0: ', basis_dimension)
104
105        self.__basis_dimension = basis_dimension
106        self.__singular_values = None
107
108    def truncate(self, basis: np.ndarray, singular_values: np.ndarray) -> np.ndarray: # pylint: disable=unused-argument
109        '''
110        Truncate the basis based on the specified dimension.
111
112        Args:
113            basis (np.ndarray): The original basis matrix.
114            singular_values (np.ndarray): The array of singular values associated with the basis matrix.
115
116        Returns:
117            np.ndarray: The truncated basis matrix with the specified dimension.
118        '''
119        # Check if basis dimension is larger than array and give error.
120        self.__singular_values = singular_values
121        if self.__basis_dimension > np.shape(basis)[1]:
122            raise ValueError('Given basis dimension is greater than size of basis array: ',
123                             self.__basis_dimension, ' > ', np.shape(basis)[1])
124
125        return basis[:, :self.__basis_dimension]
126
127
128    def get_energy(self):
129        '''
130        Returns:
131            float: The energy criteria corresponding to the truncated basis 
132        '''
133
134        if self.__singular_values is None:
135            raise ValueError('Error, singular values not yet initialized. Must call truncate before calling get_energy')
136
137        energy = np.cumsum(self.__singular_values**2)/(np.sum(self.__singular_values**2) + 1.e-30)
138        energy = energy[self.__basis_dimension-1]
139        return energy
140
141class EnergyBasedTruncater():
142    '''
143    Truncates based on the decay of singular values, i.e., will define $K$ to
144    be the number of singular values such that the cumulative energy retained
145    is greater than some threshold.
146
147    This class conforms to `LeftSingularVectorTruncater` protocol.
148    '''
149    def __init__(self, threshold: float) -> None:
150        '''
151        Constructor for the EnergyTruncater class.
152
153        Args:
154            threshold (float): The cumulative energy threshold.
155        '''
156        self.__energy_threshold_ = threshold
157        self.__energy = None
158
159    def truncate(self, basis: np.ndarray, singular_values: np.ndarray) -> np.ndarray:
160        '''
161        Truncate the basis based on the energy threshold.
162
163        Args:
164            basis (np.ndarray): The original basis matrix.
165            singular_values (np.ndarray): The array of singular values associated with the basis matrix.
166
167        Returns:
168            np.ndarray: The truncated basis matrix based on the energy threshold.
169        '''
170        energy = np.cumsum(singular_values**2)/(np.sum(singular_values**2) + 1.e-30)
171        basis_dimension = la.argmax(energy > self.__energy_threshold_) + 1
172        self.__energy = energy[basis_dimension]
173        return basis[:, 0:basis_dimension]
174
175    def get_energy(self):
176        '''
177        Returns:
178            float: The energy criteria corresponding to the truncated basis 
179        '''
180
181        if self.__energy is None:
182            raise ValueError('Error, energy not yet computed. Must call truncate before calling get_energy')
183        return self.__energy
class LeftSingularVectorTruncater(typing.Protocol):
64class LeftSingularVectorTruncater(Protocol):
65    '''
66    Interface for the Truncater class.
67    '''
68
69    def truncate(self, basis: np.ndarray, singular_values: np.ndarray) -> np.ndarray:
70        '''
71        Truncate left singular vectors
72        '''
73        ...

Interface for the Truncater class.

LeftSingularVectorTruncater(*args, **kwargs)
1771def _no_init_or_replace_init(self, *args, **kwargs):
1772    cls = type(self)
1773
1774    if cls._is_protocol:
1775        raise TypeError('Protocols cannot be instantiated')
1776
1777    # Already using a custom `__init__`. No need to calculate correct
1778    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1779    if cls.__init__ is not _no_init_or_replace_init:
1780        return
1781
1782    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1783    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1784    # searches for a proper new `__init__` in the MRO. The new `__init__`
1785    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1786    # instantiation of the protocol subclass will thus use the new
1787    # `__init__` and no longer call `_no_init_or_replace_init`.
1788    for base in cls.__mro__:
1789        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1790        if init is not _no_init_or_replace_init:
1791            cls.__init__ = init
1792            break
1793    else:
1794        # should not happen
1795        cls.__init__ = object.__init__
1796
1797    cls.__init__(self, *args, **kwargs)
def truncate( self, basis: numpy.ndarray, singular_values: numpy.ndarray) -> numpy.ndarray:
69    def truncate(self, basis: np.ndarray, singular_values: np.ndarray) -> np.ndarray:
70        '''
71        Truncate left singular vectors
72        '''
73        ...

Truncate left singular vectors

class NoOpTruncater:
76class NoOpTruncater():
77    '''
78    No op implementation
79
80    This class conforms to `LeftSingularVectorTruncater` protocol.
81    '''
82    def __init__(self) -> None:
83        pass
84
85    def truncate(self, basis: np.ndarray, singular_values: np.ndarray) -> np.ndarray: # pylint: disable=unused-argument
86        return basis

No op implementation

This class conforms to LeftSingularVectorTruncater protocol.

def truncate( self, basis: numpy.ndarray, singular_values: numpy.ndarray) -> numpy.ndarray:
85    def truncate(self, basis: np.ndarray, singular_values: np.ndarray) -> np.ndarray: # pylint: disable=unused-argument
86        return basis
class BasisSizeTruncater:
 89class BasisSizeTruncater():
 90    '''
 91    Truncates to a specified number of singular vectors, as specified in the constructor
 92
 93    This class conforms to `LeftSingularVectorTruncater` protocol.
 94    '''
 95    def __init__(self, basis_dimension: int) -> None:
 96        '''
 97        Constructor for the BasisSizeTruncater class.
 98
 99        Args:
100            basis_dimension (int): The desired dimension of the truncated basis.
101        '''
102        # Check if basis dimension is less than or equal to zero
103        if basis_dimension <= 0:
104            raise ValueError('Given basis dimension is <= 0: ', basis_dimension)
105
106        self.__basis_dimension = basis_dimension
107        self.__singular_values = None
108
109    def truncate(self, basis: np.ndarray, singular_values: np.ndarray) -> np.ndarray: # pylint: disable=unused-argument
110        '''
111        Truncate the basis based on the specified dimension.
112
113        Args:
114            basis (np.ndarray): The original basis matrix.
115            singular_values (np.ndarray): The array of singular values associated with the basis matrix.
116
117        Returns:
118            np.ndarray: The truncated basis matrix with the specified dimension.
119        '''
120        # Check if basis dimension is larger than array and give error.
121        self.__singular_values = singular_values
122        if self.__basis_dimension > np.shape(basis)[1]:
123            raise ValueError('Given basis dimension is greater than size of basis array: ',
124                             self.__basis_dimension, ' > ', np.shape(basis)[1])
125
126        return basis[:, :self.__basis_dimension]
127
128
129    def get_energy(self):
130        '''
131        Returns:
132            float: The energy criteria corresponding to the truncated basis 
133        '''
134
135        if self.__singular_values is None:
136            raise ValueError('Error, singular values not yet initialized. Must call truncate before calling get_energy')
137
138        energy = np.cumsum(self.__singular_values**2)/(np.sum(self.__singular_values**2) + 1.e-30)
139        energy = energy[self.__basis_dimension-1]
140        return energy

Truncates to a specified number of singular vectors, as specified in the constructor

This class conforms to LeftSingularVectorTruncater protocol.

BasisSizeTruncater(basis_dimension: int)
 95    def __init__(self, basis_dimension: int) -> None:
 96        '''
 97        Constructor for the BasisSizeTruncater class.
 98
 99        Args:
100            basis_dimension (int): The desired dimension of the truncated basis.
101        '''
102        # Check if basis dimension is less than or equal to zero
103        if basis_dimension <= 0:
104            raise ValueError('Given basis dimension is <= 0: ', basis_dimension)
105
106        self.__basis_dimension = basis_dimension
107        self.__singular_values = None

Constructor for the BasisSizeTruncater class.

Arguments:
  • basis_dimension (int): The desired dimension of the truncated basis.
def truncate( self, basis: numpy.ndarray, singular_values: numpy.ndarray) -> numpy.ndarray:
109    def truncate(self, basis: np.ndarray, singular_values: np.ndarray) -> np.ndarray: # pylint: disable=unused-argument
110        '''
111        Truncate the basis based on the specified dimension.
112
113        Args:
114            basis (np.ndarray): The original basis matrix.
115            singular_values (np.ndarray): The array of singular values associated with the basis matrix.
116
117        Returns:
118            np.ndarray: The truncated basis matrix with the specified dimension.
119        '''
120        # Check if basis dimension is larger than array and give error.
121        self.__singular_values = singular_values
122        if self.__basis_dimension > np.shape(basis)[1]:
123            raise ValueError('Given basis dimension is greater than size of basis array: ',
124                             self.__basis_dimension, ' > ', np.shape(basis)[1])
125
126        return basis[:, :self.__basis_dimension]

Truncate the basis based on the specified dimension.

Arguments:
  • basis (np.ndarray): The original basis matrix.
  • singular_values (np.ndarray): The array of singular values associated with the basis matrix.
Returns:

np.ndarray: The truncated basis matrix with the specified dimension.

def get_energy(self):
129    def get_energy(self):
130        '''
131        Returns:
132            float: The energy criteria corresponding to the truncated basis 
133        '''
134
135        if self.__singular_values is None:
136            raise ValueError('Error, singular values not yet initialized. Must call truncate before calling get_energy')
137
138        energy = np.cumsum(self.__singular_values**2)/(np.sum(self.__singular_values**2) + 1.e-30)
139        energy = energy[self.__basis_dimension-1]
140        return energy
Returns:

float: The energy criteria corresponding to the truncated basis

class EnergyBasedTruncater:
142class EnergyBasedTruncater():
143    '''
144    Truncates based on the decay of singular values, i.e., will define $K$ to
145    be the number of singular values such that the cumulative energy retained
146    is greater than some threshold.
147
148    This class conforms to `LeftSingularVectorTruncater` protocol.
149    '''
150    def __init__(self, threshold: float) -> None:
151        '''
152        Constructor for the EnergyTruncater class.
153
154        Args:
155            threshold (float): The cumulative energy threshold.
156        '''
157        self.__energy_threshold_ = threshold
158        self.__energy = None
159
160    def truncate(self, basis: np.ndarray, singular_values: np.ndarray) -> np.ndarray:
161        '''
162        Truncate the basis based on the energy threshold.
163
164        Args:
165            basis (np.ndarray): The original basis matrix.
166            singular_values (np.ndarray): The array of singular values associated with the basis matrix.
167
168        Returns:
169            np.ndarray: The truncated basis matrix based on the energy threshold.
170        '''
171        energy = np.cumsum(singular_values**2)/(np.sum(singular_values**2) + 1.e-30)
172        basis_dimension = la.argmax(energy > self.__energy_threshold_) + 1
173        self.__energy = energy[basis_dimension]
174        return basis[:, 0:basis_dimension]
175
176    def get_energy(self):
177        '''
178        Returns:
179            float: The energy criteria corresponding to the truncated basis 
180        '''
181
182        if self.__energy is None:
183            raise ValueError('Error, energy not yet computed. Must call truncate before calling get_energy')
184        return self.__energy

Truncates based on the decay of singular values, i.e., will define $K$ to be the number of singular values such that the cumulative energy retained is greater than some threshold.

This class conforms to LeftSingularVectorTruncater protocol.

EnergyBasedTruncater(threshold: float)
150    def __init__(self, threshold: float) -> None:
151        '''
152        Constructor for the EnergyTruncater class.
153
154        Args:
155            threshold (float): The cumulative energy threshold.
156        '''
157        self.__energy_threshold_ = threshold
158        self.__energy = None

Constructor for the EnergyTruncater class.

Arguments:
  • threshold (float): The cumulative energy threshold.
def truncate( self, basis: numpy.ndarray, singular_values: numpy.ndarray) -> numpy.ndarray:
160    def truncate(self, basis: np.ndarray, singular_values: np.ndarray) -> np.ndarray:
161        '''
162        Truncate the basis based on the energy threshold.
163
164        Args:
165            basis (np.ndarray): The original basis matrix.
166            singular_values (np.ndarray): The array of singular values associated with the basis matrix.
167
168        Returns:
169            np.ndarray: The truncated basis matrix based on the energy threshold.
170        '''
171        energy = np.cumsum(singular_values**2)/(np.sum(singular_values**2) + 1.e-30)
172        basis_dimension = la.argmax(energy > self.__energy_threshold_) + 1
173        self.__energy = energy[basis_dimension]
174        return basis[:, 0:basis_dimension]

Truncate the basis based on the energy threshold.

Arguments:
  • basis (np.ndarray): The original basis matrix.
  • singular_values (np.ndarray): The array of singular values associated with the basis matrix.
Returns:

np.ndarray: The truncated basis matrix based on the energy threshold.

def get_energy(self):
176    def get_energy(self):
177        '''
178        Returns:
179            float: The energy criteria corresponding to the truncated basis 
180        '''
181
182        if self.__energy is None:
183            raise ValueError('Error, energy not yet computed. Must call truncate before calling get_energy')
184        return self.__energy
Returns:

float: The energy criteria corresponding to the truncated basis