KratosRomApplication 10.4.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- KratosMultiphysics/.libs/KratosRomApplication.so +0 -0
- KratosMultiphysics/.libs/libKratosRomCore.dylib +0 -0
- KratosMultiphysics/RomApplication/__init__.py +9 -0
- KratosMultiphysics/RomApplication/auxiliary_functions_workflow.py +20 -0
- KratosMultiphysics/RomApplication/calculate_rom_basis_output_process.py +224 -0
- KratosMultiphysics/RomApplication/empirical_cubature_method.py +290 -0
- KratosMultiphysics/RomApplication/hrom_training_utility.py +486 -0
- KratosMultiphysics/RomApplication/numpy_output_process.py +114 -0
- KratosMultiphysics/RomApplication/parallel_svd.py +88 -0
- KratosMultiphysics/RomApplication/petrov_galerkin_training_utility.py +192 -0
- KratosMultiphysics/RomApplication/projected_residuals_output_process.py +126 -0
- KratosMultiphysics/RomApplication/python_solvers_wrapper_rom.py +78 -0
- KratosMultiphysics/RomApplication/randomized_singular_value_decomposition.py +222 -0
- KratosMultiphysics/RomApplication/rom_analysis.py +436 -0
- KratosMultiphysics/RomApplication/rom_database.py +526 -0
- KratosMultiphysics/RomApplication/rom_manager.py +1145 -0
- KratosMultiphysics/RomApplication/rom_nn_interface.py +58 -0
- KratosMultiphysics/RomApplication/rom_nn_trainer.py +330 -0
- KratosMultiphysics/RomApplication/rom_solver.py +107 -0
- KratosMultiphysics/RomApplication/rom_testing_utilities.py +69 -0
- KratosMultiphysics/RomApplication/save_rom_coefficients_process.py +120 -0
- KratosMultiphysics/RomApplication/tsqr.py +251 -0
- kratosromapplication-10.4.4.dist-info/METADATA +14 -0
- kratosromapplication-10.4.4.dist-info/RECORD +30 -0
- kratosromapplication-10.4.4.dist-info/WHEEL +6 -0
- kratosromapplication.dylibs/libKratosCore.dylib +4 -0
- kratosromapplication.dylibs/libKratosLinearSolversCore.dylib +0 -0
- kratosromapplication.dylibs/libgcc_s.1.1.dylib +0 -0
- kratosromapplication.dylibs/libgomp.1.dylib +0 -0
- kratosromapplication.dylibs/libstdc++.6.dylib +0 -0
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Application dependent names and paths
|
|
2
|
+
from KratosMultiphysics import _ImportApplication
|
|
3
|
+
import KratosMultiphysics.LinearSolversApplication
|
|
4
|
+
from KratosRomApplication import *
|
|
5
|
+
|
|
6
|
+
application = KratosRomApplication()
|
|
7
|
+
application_name = "KratosRomApplication"
|
|
8
|
+
|
|
9
|
+
_ImportApplication(application, application_name)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import dislib as ds
|
|
2
|
+
from dislib.data.array import Array
|
|
3
|
+
|
|
4
|
+
def load_blocks_array(blocks, shape, block_size):
|
|
5
|
+
if shape[0] < block_size[0] or shape[1] < block_size[1]:
|
|
6
|
+
raise ValueError("The block size is greater than the ds-array")
|
|
7
|
+
return Array(blocks, shape=shape, top_left_shape=block_size,
|
|
8
|
+
reg_shape=block_size, sparse=False)
|
|
9
|
+
|
|
10
|
+
def load_blocks_rechunk(blocks, shape, block_size, new_block_size):
|
|
11
|
+
if shape[0] < new_block_size[0] or shape[1] < new_block_size[1]:
|
|
12
|
+
raise ValueError("The block size requested for rechunk"
|
|
13
|
+
"is greater than the ds-array")
|
|
14
|
+
final_blocks = [[]]
|
|
15
|
+
# Este bucle lo puse por si los Future objects se guardan en una lista, en caso de que la forma de guardarlos cambie, también cambiará un poco este bucle.
|
|
16
|
+
# Si blocks se pasa ya como (p. ej) [[Future_object, Future_object]] no hace falta.
|
|
17
|
+
for block in blocks:
|
|
18
|
+
final_blocks[0].append(block)
|
|
19
|
+
arr = load_blocks_array(final_blocks, shape, block_size)
|
|
20
|
+
return arr.rechunk(new_block_size)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Import Python modules
|
|
2
|
+
import json
|
|
3
|
+
import numpy
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
# Importing the Kratos Library
|
|
7
|
+
import KratosMultiphysics
|
|
8
|
+
from KratosMultiphysics.RomApplication.randomized_singular_value_decomposition import RandomizedSingularValueDecomposition
|
|
9
|
+
|
|
10
|
+
def Factory(settings, model):
|
|
11
|
+
if not isinstance(settings, KratosMultiphysics.Parameters):
|
|
12
|
+
raise Exception("Expected input shall be a Parameters object, encapsulating a json string.")
|
|
13
|
+
return CalculateRomBasisOutputProcess(model, settings["Parameters"])
|
|
14
|
+
|
|
15
|
+
class CalculateRomBasisOutputProcess(KratosMultiphysics.OutputProcess):
|
|
16
|
+
"""A process to set the snapshots matrix and calculate the ROM basis from it."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, model, settings):
|
|
19
|
+
KratosMultiphysics.OutputProcess.__init__(self)
|
|
20
|
+
|
|
21
|
+
# Validate input settings against defaults
|
|
22
|
+
settings.ValidateAndAssignDefaults(self.GetDefaultParameters())
|
|
23
|
+
|
|
24
|
+
# Get the model part from which the snapshots are to be retrieved
|
|
25
|
+
if not settings["model_part_name"].GetString():
|
|
26
|
+
raise Exception("\'model_part_name\' not provided. Please specify the model part to get the snapshots from.")
|
|
27
|
+
self.model_part = model[settings["model_part_name"].GetString()]
|
|
28
|
+
|
|
29
|
+
# Set the snapshots output control and interval
|
|
30
|
+
snapshots_control_type = settings["snapshots_control_type"].GetString()
|
|
31
|
+
if snapshots_control_type == "time":
|
|
32
|
+
self.snapshots_control_is_time = True
|
|
33
|
+
elif snapshots_control_type == "step":
|
|
34
|
+
self.snapshots_control_is_time = False
|
|
35
|
+
else:
|
|
36
|
+
err_msg = "Unknown value \'{}\' for \'snapshots_control_type\'. Available options are \'time\' and \'step\'.".format(snapshots_control_type)
|
|
37
|
+
raise Exception(err_msg)
|
|
38
|
+
self.snapshots_interval = settings["snapshots_interval"].GetDouble()
|
|
39
|
+
|
|
40
|
+
# Get the variables list to be used to get the snapshots matrix information
|
|
41
|
+
# Note that we sort the snapshot variables list alphabetically
|
|
42
|
+
# This is required in order to establish a consensum for the possible visualization model part projections
|
|
43
|
+
nodal_unknowns = settings["nodal_unknowns"].GetStringArray()
|
|
44
|
+
if len(nodal_unknowns) == 0:
|
|
45
|
+
err_msg = "The snapshots matrix variables need to be specified by the user in the \'nodal_unknowns\' string array."
|
|
46
|
+
raise Exception(err_msg)
|
|
47
|
+
if any(nodal_unknowns.count(var_name) > 1 for var_name in nodal_unknowns):
|
|
48
|
+
err_msg = "There are repeated variables in the \'nodal_unknowns\' string array."
|
|
49
|
+
raise Exception(err_msg)
|
|
50
|
+
nodal_unknowns.sort()
|
|
51
|
+
|
|
52
|
+
self.snapshot_variables_list = []
|
|
53
|
+
for var_name in nodal_unknowns:
|
|
54
|
+
if not KratosMultiphysics.KratosGlobals.HasVariable(var_name):
|
|
55
|
+
err_msg = "\'{}\' variable in \'nodal_unknowns\' is not in KratosGlobals. Please check provided value.".format(var_name)
|
|
56
|
+
if not KratosMultiphysics.KratosGlobals.GetVariableType(var_name):
|
|
57
|
+
err_msg = "\'{}\' variable in \'nodal_unknowns\' is not double type. Please check provide double type variables (e.g. [\"DISPLACEMENT_X\",\"DISPLACEMENT_Y\"]).".format(var_name)
|
|
58
|
+
self.snapshot_variables_list.append(KratosMultiphysics.KratosGlobals.GetVariable(var_name))
|
|
59
|
+
|
|
60
|
+
# Set the ROM basis output settings
|
|
61
|
+
self.rom_basis_output_format = settings["rom_basis_output_format"].GetString()
|
|
62
|
+
rom_basis_output_available_formats = ["json", "numpy"]
|
|
63
|
+
if self.rom_basis_output_format not in rom_basis_output_available_formats:
|
|
64
|
+
err_msg = "Provided \'rom_basis_output_format\' is {}. Available options are \'json\' and \'numpy\'.".format(self.rom_basis_output_format)
|
|
65
|
+
raise Exception(err_msg)
|
|
66
|
+
|
|
67
|
+
self.rom_basis_output_name = settings["rom_basis_output_name"].GetString()
|
|
68
|
+
|
|
69
|
+
self.rom_basis_output_folder = Path(settings["rom_basis_output_folder"].GetString())
|
|
70
|
+
|
|
71
|
+
# Get the SVD truncation tolerance
|
|
72
|
+
self.svd_truncation_tolerance = settings["svd_truncation_tolerance"].GetDouble()
|
|
73
|
+
|
|
74
|
+
# Initialize output interval data
|
|
75
|
+
self.next_output = 0.0
|
|
76
|
+
|
|
77
|
+
# Initialize the snapshots data list
|
|
78
|
+
self.snapshots_data_list = []
|
|
79
|
+
|
|
80
|
+
# Set the flag allowing to run multiple simulations using this process #TODO cope with arbitrarily large cases (parallelism)
|
|
81
|
+
self.rom_manager = settings["rom_manager"].GetBool()
|
|
82
|
+
|
|
83
|
+
# Set the flag to print the Singular Values vector corresponding to the SVD modes
|
|
84
|
+
self.print_singular_values = settings["print_singular_values"].GetBool()
|
|
85
|
+
if self.print_singular_values and self.rom_basis_output_format == "json":
|
|
86
|
+
err_msg = 'Cannot print singular values if using the "json" output format for CalculateRomBasisOutputProcess. Please use "numpy" instead.'
|
|
87
|
+
raise Exception(err_msg)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def GetDefaultParameters(self):
|
|
92
|
+
default_settings = KratosMultiphysics.Parameters("""{
|
|
93
|
+
"help": "A process to set the snapshots matrix and calculate the ROM basis from it.",
|
|
94
|
+
"model_part_name": "",
|
|
95
|
+
"rom_manager" : false,
|
|
96
|
+
"snapshots_control_type": "step",
|
|
97
|
+
"snapshots_interval": 1.0,
|
|
98
|
+
"nodal_unknowns": [],
|
|
99
|
+
"rom_basis_output_format": "numpy",
|
|
100
|
+
"rom_basis_output_name": "RomParameters",
|
|
101
|
+
"rom_basis_output_folder" : "rom_data",
|
|
102
|
+
"svd_truncation_tolerance": 1.0e-6,
|
|
103
|
+
"print_singular_values": false
|
|
104
|
+
}""")
|
|
105
|
+
|
|
106
|
+
return default_settings
|
|
107
|
+
|
|
108
|
+
def IsOutputStep(self):
|
|
109
|
+
if self.snapshots_control_is_time:
|
|
110
|
+
time = self.__GetPrettyFloat(self.model_part.ProcessInfo[KratosMultiphysics.TIME])
|
|
111
|
+
return time >= self.__GetPrettyFloat(self.next_output)
|
|
112
|
+
else:
|
|
113
|
+
step = self.__GetPrettyFloat(self.model_part.ProcessInfo[KratosMultiphysics.STEP])
|
|
114
|
+
return step >= self.next_output
|
|
115
|
+
|
|
116
|
+
def PrintOutput(self):
|
|
117
|
+
# Save the data in the snapshots data list
|
|
118
|
+
aux_data_array = []
|
|
119
|
+
for snapshot_var in self.snapshot_variables_list:
|
|
120
|
+
aux_data_array.append( numpy.array(KratosMultiphysics.VariableUtils().GetSolutionStepValuesVector(self.model_part.Nodes, snapshot_var, 0), copy=False ))
|
|
121
|
+
self.snapshots_data_list.append(numpy.stack(aux_data_array, axis=1).reshape(-1,1))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# Schedule next snapshot output
|
|
125
|
+
if self.snapshots_interval > 0.0: # Note: if == 0, we'll just always print
|
|
126
|
+
if self.snapshots_control_is_time:
|
|
127
|
+
time = self.__GetPrettyFloat(self.model_part.ProcessInfo[KratosMultiphysics.TIME])
|
|
128
|
+
while self.__GetPrettyFloat(self.next_output) <= time:
|
|
129
|
+
self.next_output += self.snapshots_interval
|
|
130
|
+
else:
|
|
131
|
+
step = self.__GetPrettyFloat(self.model_part.ProcessInfo[KratosMultiphysics.STEP])
|
|
132
|
+
while self.next_output <= step:
|
|
133
|
+
self.next_output += self.snapshots_interval
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _GetSnapshotsMatrix(self):
|
|
137
|
+
return numpy.block(self.snapshots_data_list)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _ComputeSVD(self, snapshots_matrix):
|
|
141
|
+
|
|
142
|
+
# Calculate the randomized SVD of the snapshots matrix
|
|
143
|
+
u,sigma,_,_= RandomizedSingularValueDecomposition().Calculate(snapshots_matrix, self.svd_truncation_tolerance)
|
|
144
|
+
return u, sigma
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _PrintRomBasis(self, u, sigma):
|
|
148
|
+
# Initialize the Python dictionary with the default settings
|
|
149
|
+
# Note that this order is kept if Python 3.6 onwards is used
|
|
150
|
+
rom_basis_dict = {
|
|
151
|
+
"rom_manager" : False,
|
|
152
|
+
"train_hrom": False,
|
|
153
|
+
"run_hrom": False,
|
|
154
|
+
"projection_strategy": "galerkin",
|
|
155
|
+
"assembling_strategy": "global",
|
|
156
|
+
"rom_format": "numpy",
|
|
157
|
+
"rom_settings": {
|
|
158
|
+
"rom_bns_settings": {}
|
|
159
|
+
},
|
|
160
|
+
"hrom_settings": {},
|
|
161
|
+
"nodal_modes": {},
|
|
162
|
+
"elements_and_weights" : {}
|
|
163
|
+
}
|
|
164
|
+
#TODO: I'd rename elements_and_weights to hrom_weights
|
|
165
|
+
|
|
166
|
+
if self.rom_manager:
|
|
167
|
+
rom_basis_dict["rom_manager"] = True
|
|
168
|
+
rom_basis_dict["hrom_settings"]["hrom_format"] = self.rom_basis_output_format
|
|
169
|
+
n_nodal_unknowns = len(self.snapshot_variables_list)
|
|
170
|
+
|
|
171
|
+
# Save the nodal basis
|
|
172
|
+
rom_basis_dict["rom_settings"]["nodal_unknowns"] = [var.Name() for var in self.snapshot_variables_list]
|
|
173
|
+
rom_basis_dict["rom_settings"]["number_of_rom_dofs"] = numpy.shape(u)[1] #TODO: This is way misleading. I'd call it number_of_basis_modes or number_of_rom_modes
|
|
174
|
+
rom_basis_dict["projection_strategy"] = "galerkin" # Galerkin: (Phi.T@K@Phi dq= Phi.T@b), LSPG = (K@Phi dq= b), Petrov-Galerkin = (Psi.T@K@Phi dq = Psi.T@b)
|
|
175
|
+
rom_basis_dict["assembling_strategy"] = "global" # Assemble the ROM globally or element by element: "global" (Phi_g @ J_g @ Phi_g), "element by element" sum(Phi_e^T @ K_e @ Phi_e)
|
|
176
|
+
rom_basis_dict["rom_format"] = self.rom_basis_output_format
|
|
177
|
+
rom_basis_dict["rom_settings"]["petrov_galerkin_number_of_rom_dofs"] = 0
|
|
178
|
+
#NOTE "petrov_galerkin_number_of_rom_dofs" is not used unless a Petrov-Galerkin simulation is called, in which case it shall be modified either manually or from the RomManager
|
|
179
|
+
|
|
180
|
+
# Create the folder if it doesn't already exist
|
|
181
|
+
if not self.rom_basis_output_folder.exists():
|
|
182
|
+
self.rom_basis_output_folder.mkdir(parents=True)
|
|
183
|
+
|
|
184
|
+
if self.rom_basis_output_format == "json":
|
|
185
|
+
# Storing modes in JSON format
|
|
186
|
+
i = 0
|
|
187
|
+
for node in self.model_part.Nodes:
|
|
188
|
+
rom_basis_dict["nodal_modes"][node.Id] = u[i:i+n_nodal_unknowns].tolist()
|
|
189
|
+
i += n_nodal_unknowns
|
|
190
|
+
|
|
191
|
+
elif self.rom_basis_output_format == "numpy":
|
|
192
|
+
# Storing modes in Numpy format
|
|
193
|
+
node_ids = []
|
|
194
|
+
for node in self.model_part.Nodes:
|
|
195
|
+
node_ids.append(node.Id)
|
|
196
|
+
node_ids = numpy.array(node_ids)
|
|
197
|
+
numpy.save(self.rom_basis_output_folder / "RightBasisMatrix.npy", u)
|
|
198
|
+
numpy.save(self.rom_basis_output_folder / "NodeIds.npy", node_ids)
|
|
199
|
+
if self.print_singular_values:
|
|
200
|
+
numpy.save(self.rom_basis_output_folder / "SingularValuesVector.npy", sigma)
|
|
201
|
+
else:
|
|
202
|
+
err_msg = "Unsupported output format {}.".format(self.rom_basis_output_format)
|
|
203
|
+
raise Exception(err_msg)
|
|
204
|
+
|
|
205
|
+
# Creating the ROM JSON file containing or not the modes depending on "self.rom_basis_output_format"
|
|
206
|
+
output_filename = self.rom_basis_output_folder / f"{self.rom_basis_output_name}.json"
|
|
207
|
+
with output_filename.open('w') as f:
|
|
208
|
+
json.dump(rom_basis_dict, f, indent = 4)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def ExecuteFinalize(self):
|
|
212
|
+
# Prepare a NumPy array with the snapshots data
|
|
213
|
+
self.n_nodes = self.model_part.NumberOfNodes()
|
|
214
|
+
self.n_data_cols = len(self.snapshots_data_list)
|
|
215
|
+
self.n_nodal_unknowns = len(self.snapshot_variables_list)
|
|
216
|
+
|
|
217
|
+
if not self.rom_manager:
|
|
218
|
+
u, sigma = self._ComputeSVD(self._GetSnapshotsMatrix())
|
|
219
|
+
self._PrintRomBasis(u, sigma)
|
|
220
|
+
|
|
221
|
+
def __GetPrettyFloat(self, number):
|
|
222
|
+
float_format = "{:.12f}"
|
|
223
|
+
pretty_number = float(float_format.format(number))
|
|
224
|
+
return pretty_number
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from KratosMultiphysics import Logger
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from matplotlib import pyplot as plt
|
|
6
|
+
missing_matplotlib = False
|
|
7
|
+
except ImportError as e:
|
|
8
|
+
missing_matplotlib = True
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EmpiricalCubatureMethod():
|
|
12
|
+
"""
|
|
13
|
+
This class selects a subset of elements and corresponding positive weights necessary for the construction of a hyper-reduced order model
|
|
14
|
+
Reference: Hernandez 2020. "A multiscale method for periodic structures using domain decomposition and ECM-hyperreduction"
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
ECM_tolerance = 0,
|
|
20
|
+
Filter_tolerance = 0,
|
|
21
|
+
Plotting = False,
|
|
22
|
+
MaximumNumberUnsuccesfulIterations = 100
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
Constructor setting up the parameters for the Element Selection Strategy
|
|
26
|
+
ECM_tolerance: approximation tolerance for the element selection algorithm
|
|
27
|
+
Filter_tolerance: parameter limiting the number of candidate points (elements) to those above this tolerance
|
|
28
|
+
Plotting: whether to plot the error evolution of the element selection algorithm
|
|
29
|
+
"""
|
|
30
|
+
self.ECM_tolerance = ECM_tolerance
|
|
31
|
+
self.Filter_tolerance = Filter_tolerance
|
|
32
|
+
self.Name = "EmpiricalCubature"
|
|
33
|
+
self.Plotting = Plotting
|
|
34
|
+
self.MaximumNumberUnsuccesfulIterations = MaximumNumberUnsuccesfulIterations
|
|
35
|
+
|
|
36
|
+
def SetUp(
|
|
37
|
+
self,
|
|
38
|
+
ResidualsBasis,
|
|
39
|
+
InitialCandidatesSet = None,
|
|
40
|
+
constrain_sum_of_weights=True,
|
|
41
|
+
constrain_conditions = False,
|
|
42
|
+
number_of_conditions = 0
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
Method for setting up the element selection
|
|
46
|
+
input: - ResidualsBasis: numpy array containing a basis to the residuals projected
|
|
47
|
+
- constrain_sum_of_weights: enable the user to constrain weights to be the sum of the number of entities.
|
|
48
|
+
- constrain_conditions: enable the user to enforce weights to consider conditions (for specific boundary conditions).
|
|
49
|
+
"""
|
|
50
|
+
self.W = np.ones(np.shape(ResidualsBasis)[0])
|
|
51
|
+
self.G = ResidualsBasis.T
|
|
52
|
+
self.y = InitialCandidatesSet
|
|
53
|
+
self.add_constrain_count = None
|
|
54
|
+
total_number_of_entities = np.shape(self.G)[1]
|
|
55
|
+
elements_constraint = np.ones(total_number_of_entities)
|
|
56
|
+
conditions_begin = total_number_of_entities - number_of_conditions
|
|
57
|
+
elements_constraint[conditions_begin:] = 0
|
|
58
|
+
|
|
59
|
+
if constrain_sum_of_weights and not constrain_conditions:
|
|
60
|
+
"""
|
|
61
|
+
-This is necessary in case the sum of the columns of self.G equals the 0 vector,to avoid the trivial solution
|
|
62
|
+
-It is enforcing that the sum of the weights equals the number of columns in self.G (total number of elements)
|
|
63
|
+
"""
|
|
64
|
+
projection_of_constant_vector_elements = elements_constraint - self.G.T@( self.G @ elements_constraint)
|
|
65
|
+
projection_of_constant_vector_elements/= np.linalg.norm(projection_of_constant_vector_elements)
|
|
66
|
+
self.G = np.vstack([ self.G , projection_of_constant_vector_elements] )
|
|
67
|
+
self.add_constrain_count = -1
|
|
68
|
+
elif constrain_sum_of_weights and constrain_conditions:#Only for models which contains conditions
|
|
69
|
+
projection_of_constant_vector_elements = elements_constraint - self.G.T@( self.G @ elements_constraint)
|
|
70
|
+
projection_of_constant_vector_elements/= np.linalg.norm(projection_of_constant_vector_elements)
|
|
71
|
+
self.G = np.vstack([ self.G , projection_of_constant_vector_elements] )
|
|
72
|
+
# # # # # # # # #
|
|
73
|
+
conditions_constraint = np.ones(total_number_of_entities)
|
|
74
|
+
conditions_constraint[:conditions_begin] = 0
|
|
75
|
+
projection_of_constant_vector_conditions = conditions_constraint - self.G.T@( self.G @ conditions_constraint)
|
|
76
|
+
projection_of_constant_vector_conditions/= np.linalg.norm(projection_of_constant_vector_conditions)
|
|
77
|
+
self.G = np.vstack([ self.G , projection_of_constant_vector_conditions ] )
|
|
78
|
+
self.add_constrain_count = -2
|
|
79
|
+
self.b = self.G @ self.W
|
|
80
|
+
self.UnsuccesfulIterations = 0
|
|
81
|
+
|
|
82
|
+
def Initialize(self):
|
|
83
|
+
"""
|
|
84
|
+
Method performing calculations required before launching the Calculate method
|
|
85
|
+
"""
|
|
86
|
+
self.GnormNOONE = np.linalg.norm(self.G[:self.add_constrain_count,:], axis = 0)
|
|
87
|
+
M = np.shape(self.G)[1]
|
|
88
|
+
normB = np.linalg.norm(self.b)
|
|
89
|
+
|
|
90
|
+
if self.y is None:
|
|
91
|
+
self.y = np.arange(0,M,1) # Set of candidate points (those whose associated column has low norm are removed)
|
|
92
|
+
|
|
93
|
+
if self.Filter_tolerance > 0:
|
|
94
|
+
TOL_REMOVE = self.Filter_tolerance * normB
|
|
95
|
+
rmvpin = np.where(self.GnormNOONE[self.y] < TOL_REMOVE)
|
|
96
|
+
#self.y_complement = self.y[rmvpin]
|
|
97
|
+
self.y = np.delete(self.y,rmvpin)
|
|
98
|
+
else:
|
|
99
|
+
self.y_complement = np.arange(0, M, 1) # Initialize complement with all points
|
|
100
|
+
self.y_complement = np.delete(self.y_complement, self.y) # Remove candidates from complement
|
|
101
|
+
|
|
102
|
+
if self.Filter_tolerance > 0:
|
|
103
|
+
TOL_REMOVE = self.Filter_tolerance * normB # Compute removal tolerance
|
|
104
|
+
|
|
105
|
+
# Filter out low-norm columns from complement
|
|
106
|
+
rmvpin_complement = np.where(self.GnormNOONE[self.y_complement] < TOL_REMOVE)
|
|
107
|
+
self.y_complement = np.delete(self.y_complement, rmvpin_complement)
|
|
108
|
+
|
|
109
|
+
# Filter out low-norm columns from candidates
|
|
110
|
+
rmvpin = np.where(self.GnormNOONE[self.y] < TOL_REMOVE)
|
|
111
|
+
removed_count = np.size(rmvpin)
|
|
112
|
+
self.y = np.delete(self.y, rmvpin)
|
|
113
|
+
|
|
114
|
+
# Warning if some candidates were removed
|
|
115
|
+
if removed_count > 0:
|
|
116
|
+
Logger.PrintWarning("EmpiricalCubatureMethod", f"Some of the candidates were removed ({removed_count} removed). To include all candidates (with 0 weights in the HROM model part) for visualization and projection, consider using 'include_elements_model_parts_list' and 'include_conditions_model_parts_list' in the 'hrom_settings'.")
|
|
117
|
+
|
|
118
|
+
# Warning if all candidates were removed
|
|
119
|
+
if np.size(self.y) == 0:
|
|
120
|
+
Logger.PrintWarning("EmpiricalCubatureMethod", "All candidates were removed because they have no contribution to the residual. To include them all (with 0 weights in the HROM model part) for visualization and projection, use 'include_elements_model_parts_list' and 'include_conditions_model_parts_list' in the 'hrom_settings'.")
|
|
121
|
+
self.y = self.y_complement # Set candidates to complement
|
|
122
|
+
|
|
123
|
+
self.z = {} # Set of intergration points
|
|
124
|
+
self.mPOS = 0 # Number of nonzero weights
|
|
125
|
+
self.r = self.b.copy() # residual vector
|
|
126
|
+
self.m = len(self.b) # Default number of points
|
|
127
|
+
self.nerror = np.linalg.norm(self.r)/normB
|
|
128
|
+
self.nerrorACTUAL = self.nerror
|
|
129
|
+
|
|
130
|
+
def Run(self):
|
|
131
|
+
self.Initialize()
|
|
132
|
+
self.Calculate()
|
|
133
|
+
|
|
134
|
+
def expand_candidates_with_complement(self):
|
|
135
|
+
self.y = np.r_[self.y,self.y_complement]
|
|
136
|
+
print('expanding set to include the complement...')
|
|
137
|
+
ExpandedSetFlag = True
|
|
138
|
+
return ExpandedSetFlag
|
|
139
|
+
|
|
140
|
+
def Calculate(self):
|
|
141
|
+
"""
|
|
142
|
+
Method launching the element selection algorithm to find a set of elements: self.z, and wiegths: self.w
|
|
143
|
+
"""
|
|
144
|
+
MaximumLengthZ = 0
|
|
145
|
+
ExpandedSetFlag = False
|
|
146
|
+
k = 1 # number of iterations
|
|
147
|
+
self.success = True
|
|
148
|
+
while self.nerrorACTUAL > self.ECM_tolerance and self.mPOS < self.m and np.size(self.y) != 0:
|
|
149
|
+
|
|
150
|
+
if self.UnsuccesfulIterations > self.MaximumNumberUnsuccesfulIterations and not ExpandedSetFlag and hasattr(self, 'y_complement'):
|
|
151
|
+
ExpandedSetFlag = self.expand_candidates_with_complement()
|
|
152
|
+
|
|
153
|
+
#Step 1. Compute new point
|
|
154
|
+
if np.size(self.y)==1:
|
|
155
|
+
#candidate set consists of a single element
|
|
156
|
+
indSORT = 0
|
|
157
|
+
i = int(self.y)
|
|
158
|
+
else:
|
|
159
|
+
ObjFun = self.G[:,self.y].T @ self.r.T
|
|
160
|
+
ObjFun = ObjFun.T #/ self.GnormNOONE[self.y]
|
|
161
|
+
indSORT = np.argmax(ObjFun)
|
|
162
|
+
i = self.y[indSORT]
|
|
163
|
+
if k==1:
|
|
164
|
+
alpha = np.linalg.lstsq(self.G[:, [i]], self.b)[0]
|
|
165
|
+
H = 1/(self.G[:,i] @ self.G[:,i].T)
|
|
166
|
+
else:
|
|
167
|
+
H, alpha = self._UpdateWeightsInverse(self.G[:,self.z],H,self.G[:,i],alpha)
|
|
168
|
+
|
|
169
|
+
#Step 3. Move i from set y to set z
|
|
170
|
+
if k == 1:
|
|
171
|
+
self.z = i
|
|
172
|
+
else:
|
|
173
|
+
self.z = np.r_[self.z,i]
|
|
174
|
+
|
|
175
|
+
#self.y = np.delete(self.y,indSORT)
|
|
176
|
+
if np.size(self.y)==1:
|
|
177
|
+
if hasattr(self, 'y_complement'):
|
|
178
|
+
self.expand_candidates_with_complement()
|
|
179
|
+
self.y = np.delete(self.y,indSORT)
|
|
180
|
+
else:
|
|
181
|
+
self.success = False
|
|
182
|
+
break
|
|
183
|
+
else:
|
|
184
|
+
self.y = np.delete(self.y,indSORT)
|
|
185
|
+
|
|
186
|
+
# Step 4. Find possible negative weights
|
|
187
|
+
if any(alpha < 0):
|
|
188
|
+
print("WARNING: NEGATIVE weight found")
|
|
189
|
+
indexes_neg_weight = np.where(alpha <= 0.)[0]
|
|
190
|
+
self.y = np.append(self.y, (self.z[indexes_neg_weight]).T)
|
|
191
|
+
self.z = np.delete(self.z, indexes_neg_weight)
|
|
192
|
+
H = self._MultiUpdateInverseHermitian(H, indexes_neg_weight)
|
|
193
|
+
alpha = H @ (self.G[:, self.z].T @ self.b)
|
|
194
|
+
alpha = alpha.reshape(len(alpha),1)
|
|
195
|
+
|
|
196
|
+
if np.size(self.z) > MaximumLengthZ :
|
|
197
|
+
self.UnsuccesfulIterations = 0
|
|
198
|
+
else:
|
|
199
|
+
self.UnsuccesfulIterations += 1
|
|
200
|
+
|
|
201
|
+
#Step 6 Update the residual
|
|
202
|
+
if np.size(alpha)==1:
|
|
203
|
+
self.r = self.b.reshape(-1,1) - (self.G[:,self.z] * alpha).reshape(-1,1)
|
|
204
|
+
self.r = np.squeeze(self.r)
|
|
205
|
+
else:
|
|
206
|
+
Aux = self.G[:,self.z] @ alpha
|
|
207
|
+
self.r = np.squeeze(self.b - Aux.T)
|
|
208
|
+
self.nerror = np.linalg.norm(self.r) / np.linalg.norm(self.b) # Relative error (using r and b)
|
|
209
|
+
self.nerrorACTUAL = self.nerror
|
|
210
|
+
|
|
211
|
+
# STEP 7
|
|
212
|
+
self.mPOS = np.size(self.z)
|
|
213
|
+
print(f'k = {k}, m = {np.size(self.z)}, error n(res)/n(b) (%) = {self.nerror*100}, Actual error % = {self.nerrorACTUAL*100} ')
|
|
214
|
+
|
|
215
|
+
if k == 1:
|
|
216
|
+
ERROR_GLO = np.array([self.nerrorACTUAL])
|
|
217
|
+
NPOINTS = np.array([np.size(self.z)])
|
|
218
|
+
else:
|
|
219
|
+
ERROR_GLO = np.c_[ ERROR_GLO , self.nerrorACTUAL]
|
|
220
|
+
NPOINTS = np.c_[ NPOINTS , np.size(self.z)]
|
|
221
|
+
|
|
222
|
+
MaximumLengthZ = max(MaximumLengthZ, np.size(self.z))
|
|
223
|
+
k = k+1
|
|
224
|
+
|
|
225
|
+
if k-MaximumLengthZ>1000 and ExpandedSetFlag:
|
|
226
|
+
"""
|
|
227
|
+
this means using the initial candidate set, it was impossible to obtain a set of positive weights.
|
|
228
|
+
Try again without constraints!!!
|
|
229
|
+
TODO: incorporate this into greater workflow
|
|
230
|
+
"""
|
|
231
|
+
self.success = False
|
|
232
|
+
break
|
|
233
|
+
|
|
234
|
+
self.w = alpha.T * np.sqrt(self.W[self.z]) #TODO FIXME cope with weights vectors different from 1
|
|
235
|
+
|
|
236
|
+
print(f'Total number of iterations = {k}')
|
|
237
|
+
|
|
238
|
+
if missing_matplotlib == False and self.Plotting == True:
|
|
239
|
+
plt.plot(NPOINTS[0], ERROR_GLO[0])
|
|
240
|
+
plt.title('Element Selection Error Evolution')
|
|
241
|
+
plt.xlabel('Number of elements')
|
|
242
|
+
plt.ylabel('Error %')
|
|
243
|
+
plt.show()
|
|
244
|
+
|
|
245
|
+
def _UpdateWeightsInverse(self, A,Aast,a,xold):
|
|
246
|
+
"""
|
|
247
|
+
Method for the quick update of weights (self.w), whenever a negative weight is found
|
|
248
|
+
"""
|
|
249
|
+
c = np.dot(A.T, a)
|
|
250
|
+
d = np.dot(Aast, c).reshape(-1, 1)
|
|
251
|
+
s = np.dot(a.T, a) - np.dot(c.T, d)
|
|
252
|
+
aux1 = np.hstack([Aast + np.outer(d, d) / s, -d / s])
|
|
253
|
+
if np.shape(-d.T / s)[1]==1:
|
|
254
|
+
s = s.reshape(1,-1)
|
|
255
|
+
aux2 = np.squeeze(np.hstack([-d.T / s, 1 / s]))
|
|
256
|
+
else:
|
|
257
|
+
aux2 = np.hstack([np.squeeze(-d.T / s), 1 / s])
|
|
258
|
+
Bast = np.vstack([aux1, aux2])
|
|
259
|
+
v = np.dot(a.T, self.r) / s
|
|
260
|
+
x = np.vstack([(xold - d * v), v])
|
|
261
|
+
return Bast, x
|
|
262
|
+
|
|
263
|
+
def _MultiUpdateInverseHermitian(self, invH, neg_indexes):
|
|
264
|
+
"""
|
|
265
|
+
Method for the quick update of weights (self.w), whenever a negative weight is found
|
|
266
|
+
"""
|
|
267
|
+
neg_indexes = np.sort(neg_indexes)
|
|
268
|
+
for i in range(np.size(neg_indexes)):
|
|
269
|
+
neg_index = neg_indexes[i] - i
|
|
270
|
+
invH = self._UpdateInverseHermitian(invH, neg_index)
|
|
271
|
+
return invH
|
|
272
|
+
|
|
273
|
+
def _UpdateInverseHermitian(self, invH, neg_index):
|
|
274
|
+
"""
|
|
275
|
+
Method for the quick update of weights (self.w), whenever a negative weight is found
|
|
276
|
+
"""
|
|
277
|
+
if neg_index == np.shape(invH)[1]:
|
|
278
|
+
aux = (invH[0:-1, -1] * invH[-1, 0:-1]) / invH(-1, -1)
|
|
279
|
+
invH_new = invH[:-1, :-1] - aux
|
|
280
|
+
else:
|
|
281
|
+
aux1 = np.hstack([invH[:, 0:neg_index], invH[:, neg_index + 1:], invH[:, neg_index].reshape(-1, 1)])
|
|
282
|
+
aux2 = np.vstack([aux1[0:neg_index, :], aux1[neg_index + 1:, :], aux1[neg_index, :]])
|
|
283
|
+
invH_new = aux2[0:-1, 0:-1] - np.outer(aux2[0:-1, -1], aux2[-1, 0:-1]) / aux2[-1, -1]
|
|
284
|
+
return invH_new
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
|