-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathscaler.py
322 lines (269 loc) · 12.2 KB
/
scaler.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
#
# ************************************************************************
#
# ROM Tools and Workflows
# Copyright 2019 National Technology & Engineering Solutions of Sandia,LLC
# (NTESS)
#
# Under the terms of Contract DE-NA0003525 with NTESS, the
# U.S. Government retains certain rights in this software.
#
# ROM Tools and Workflows is licensed under BSD-3-Clause terms of use:
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Questions? Contact Eric Parish ([email protected])
#
# ************************************************************************
#
'''
---
##**Notes**
The scaler class is used to performed scaled POD. Scaling is applied to tensors of shape $\mathbb{R}^{ N_{\\mathrm{vars}} \\times N_{\\mathrm{x}} \\times N_s}$. These tensors are then reshaped into matrices when performing SVD.
___
##**Theory**
*What is scaled POD, and why would I do it?*
Standard POD computes a basis that minimizes the projection error in a standard Euclidean $\\ell^2$ inner product,
i.e., for a snapshot matrix $\\mathbf{S} \\in \\mathbb{R}^{ N_{\\mathrm{vars}} N_{\\mathrm{x}} \\times N_s}$, POD computes the basis by solving the minimization problem
(assuming no affine offset)
$$ \\boldsymbol \\Phi = \\underset{ \\boldsymbol \\Phi_{\\*} \\in \\mathbb{R}^{ N_{\\mathrm{vars}} N_{\\mathrm{x}} \\times K} | \\boldsymbol
\\Phi_{\\*}^T \\boldsymbol \\Phi_{\\*} = \\mathbf{I}}{ \\mathrm{arg \\; min} } \\| \\Phi_{\\*} \\Phi_{\\*}^T
\\mathbf{S} - \\mathbf{S} \\|_2.$$
In this minimization problem, errors are measured in a standard $\\ell^2$ norm.
For most practical applications, where our snapshot matrix involves variables of different scales,
this norm does not make sense (both intuitively, and on dimensional grounds).
As a practical example, consider fluid dynamics where the total energy is orders of magnitude larger than the density.
One of the most common approaches for mitigating this issue is to perform scaled POD.
In scaled POD, we solve a minimization problem on a scaled snapshot matrix.
Defining $\\mathbf{S}_{\\*} = \\mathbf{W}^{-1} \\mathbf{S}$, where $\\mathbf{W}$ is a weighting matrix
(e.g., a diagonal matrix containing the max absolute value of each state variable),
we compute the basis as the solution to the minimization problem
$$ \\boldsymbol \\Phi = \\mathbf{W} \\underset{ \\boldsymbol \\Phi_{\\*} \\in \\mathbb{R}^{N_{\\mathrm{vars}} N_{\\mathrm{x}} \\times K} |\\boldsymbol
\\Phi_{\\*}^T \\boldsymbol \\Phi_{\\*} = \\mathbf{I}}{ \\mathrm{arg \\; min} } \\| \\Phi_{\\*} \\Phi_{\\*}^T
\\mathbf{S}_{\\*} - \\mathbf{S}_{\\*} \\|_2.$$
The Scaler encapsulates this information.
___
##**API**
'''
import abc
import numpy as np
class Scaler(abc.ABC):
'''
Abstract base class
'''
@abc.abstractmethod
def pre_scaling(self, data_tensor: np.ndarray) -> np.ndarray:
'''
Scales the snapshot matrix before performing SVD
'''
pass
@abc.abstractmethod
def post_scaling(self, data_tensor: np.ndarray) -> np.ndarray:
'''
Scales the left singular vectors after performing SVD
'''
pass
class NoOpScaler(Scaler):
'''
No op implementation
'''
def __init__(self):
pass
def pre_scaling(self, data_tensor):
return data_tensor
def post_scaling(self, data_tensor):
return data_tensor
class VectorScaler(Scaler):
'''
Concrete implementation designed to scale snapshot matrices by a vector.
For a snapshot tensor $\\mathbf{S} \\in \\mathbb{R}^{N_{\\mathrm{u}} \\times N \\times K}$, the VectorScaler
accepts in a scaling vector $\\mathbf{v} \\in \\mathbb{R}^{N}$, and scales by
$$\\mathbf{S}^* = \\mathrm{diag}(\\mathbf{v})^{-1} \\mathbf{S}$$
before performing POD (i.e., POD is performed on $\\mathbf{S}^*$). After POD is performed, the bases
are post-scaled by $$\\boldsymbol \\Phi = \\mathrm{diag}(\\mathbf{v}) \\mathbf{U}$$
**Note that scaling can cause bases to not be orthonormal; we do not
recommend using scalers with the NoOpOrthonormalizer**
'''
def __init__(self, scaling_vector):
'''
Constructor for the VectorScaler.
Args:
scaling_vector: Array containing the scaling vector for each row
in the snapshot matrix.
This constructor initializes the VectorScaler with the specified
scaling vector.
'''
self.__scaling_vector_matrix = scaling_vector
self.__scaling_vector_matrix_inv = 1./scaling_vector
def pre_scaling(self, data_tensor):
'''
Scales the input data matrix using the inverse of the scaling vector
and returns the scaled matrix.
Args:
data_tensor (np.ndarray): The input data matrix to be scaled.
Returns:
np.ndarray: The scaled data matrix.
'''
return self.__scaling_vector_matrix_inv[None, :, None] * data_tensor
def post_scaling(self, data_tensor):
'''
Scales the input data matrix using the scaling vector and returns the
scaled matrix.
Args:
data_tensor (np.ndarray): The input data matrix to be scaled.
Returns:
np.ndarray: The scaled data matrix.
'''
return self.__scaling_vector_matrix[None, :, None] * data_tensor
class VariableScaler(Scaler):
'''
Concrete implementation designed for snapshot matrices involving multiple
state variables.
This class is designed to scale a data matrix comprising multiple states
(e.g., for the Navier--Stokes, rho, rho u, rhoE)
This scaler will scale each variable based on
- max-abs scaling: for the $i$th state variable $u_i$, we will compute the scaling as
$s_i = \\mathrm{max}( \\mathrm{abs}( S_i ) )$, where $S_i$ denotes the snapshot matrix of the $i$th variable.
- mean abs: for the $i$th state variable $u_i$, we will compute the scaling as
$s_i = \\mathrm{mean}( \\mathrm{abs}( S_i ) )$, where $S_i$ denotes the snapshot matrix of the $i$th variable.
- variance: for the $i$th state variable $u_i$, we will compute the scaling as
$s_i = \\mathrm{std}( S_i ) $, where $S_i$ denotes the snapshot matrix of the $i$th variable.
'''
def __init__(self, scaling_type):
'''
Constructor for the VariableScaler.
Args:
scaling_type (str): The scaling method to use ('max_abs',
'mean_abs', or 'variance').
This constructor initializes the VariableScaler with the specified
scaling type, variable ordering, and number of variables.
'''
self.__scaling_type = scaling_type
self.have_scales_been_initialized = False
self.var_scales_ = None
def initialize_scalings(self, data_tensor):
'''
Initializes the scaling factors for each state variable based on the
specified method.
Args:
data_tensor (np.ndarray): The input data matrix.
'''
n_var = data_tensor.shape[0]
self.var_scales_ = np.ones(n_var)
for i in range(n_var):
ith_var = data_tensor[i]
if self.__scaling_type == 'max_abs':
var_scale = np.max(abs(ith_var))
elif self.__scaling_type == 'mean_abs':
var_scale = np.mean(abs(ith_var))
elif self.__scaling_type == 'variance':
var_scale = np.sqrt(np.var(ith_var))
# in case of a zero field (e.g., 2D)
if var_scale < 1e-10:
var_scale = 1.
self.var_scales_[i] = var_scale
self.have_scales_been_initialized = True
# These are all inplace operations
def pre_scaling(self, data_tensor):
'''
Scales the input data matrix before processing, taking into account
the previously initialized scaling factors.
Args:
data_tensor (np.ndarray): The input data matrix to be scaled.
Returns:
np.ndarray: The scaled data matrix.
'''
n_var = data_tensor.shape[0]
if self.have_scales_been_initialized:
pass
else:
self.initialize_scalings(data_tensor)
# scale each field (variable scaling)
for i in range(n_var):
data_tensor[i] = data_tensor[i] / self.var_scales_[i]
return data_tensor
def post_scaling(self, data_tensor):
'''
Scales the input data matrix using the scaling vector and returns the
scaled matrix.
Args:
data_tensor (np.ndarray): The input data matrix to be scaled.
Returns:
np.ndarray: The scaled data matrix.
'''
assert self.have_scales_been_initialized, "Scales in VariableScaler have not been initialized"
# scale each field
n_var = data_tensor.shape[0]
for i in range(n_var):
data_tensor[i] = data_tensor[i]*self.var_scales_[i]
return data_tensor
class VariableAndVectorScaler(Scaler):
'''
Concrete implementation designed to scale snapshot matrices involving
multiple state variables by both the variable magnitudes and an additional
vector. This is particularly useful when wishing to perform POD for,
e.g., a finite volume method where we want to scale by the cell volumes as
well as the variable magnitudes. This implementation combines the
VectorScaler and VariableScaler classes.
'''
def __init__(self, scaling_vector, scaling_type):
'''
Constructor for the VariableAndVectorScaler.
Args:
scaling_vector: Array containing the scaling vector for each row
in the snapshot matrix.
scaling_type: Scaling method ('max_abs',
'mean_abs', or 'variance') for variable magnitudes.
This constructor initializes the `VariableAndVectorScaler` with the
specified parameters.
'''
self.__my_variable_scaler = VariableScaler(scaling_type)
self.__my_vector_scaler = VectorScaler(scaling_vector)
def pre_scaling(self, data_tensor):
'''
Scales the input data matrix before processing, first using the
`VariableScaler` and then the `VectorScaler`.
Args:
data_tensor (np.ndarray): The input data matrix to be scaled.
Returns:
np.ndarray: The scaled data matrix.
'''
data_tensor = self.__my_variable_scaler.pre_scaling(data_tensor)
return self.__my_vector_scaler.pre_scaling(data_tensor)
def post_scaling(self, data_tensor):
'''
Scales the input data matrix after processing, first using the
`VectorScaler` and then the `VariableScaler`.
Args:
data_tensor (np.ndarray): The input data matrix to be scaled.
Returns:
np.ndarray: The scaled data matrix.
'''
data_tensor = self.__my_vector_scaler.post_scaling(data_tensor)
return self.__my_variable_scaler.post_scaling(data_tensor)