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.
Files changed (30) hide show
  1. KratosMultiphysics/.libs/KratosRomApplication.so +0 -0
  2. KratosMultiphysics/.libs/libKratosRomCore.dylib +0 -0
  3. KratosMultiphysics/RomApplication/__init__.py +9 -0
  4. KratosMultiphysics/RomApplication/auxiliary_functions_workflow.py +20 -0
  5. KratosMultiphysics/RomApplication/calculate_rom_basis_output_process.py +224 -0
  6. KratosMultiphysics/RomApplication/empirical_cubature_method.py +290 -0
  7. KratosMultiphysics/RomApplication/hrom_training_utility.py +486 -0
  8. KratosMultiphysics/RomApplication/numpy_output_process.py +114 -0
  9. KratosMultiphysics/RomApplication/parallel_svd.py +88 -0
  10. KratosMultiphysics/RomApplication/petrov_galerkin_training_utility.py +192 -0
  11. KratosMultiphysics/RomApplication/projected_residuals_output_process.py +126 -0
  12. KratosMultiphysics/RomApplication/python_solvers_wrapper_rom.py +78 -0
  13. KratosMultiphysics/RomApplication/randomized_singular_value_decomposition.py +222 -0
  14. KratosMultiphysics/RomApplication/rom_analysis.py +436 -0
  15. KratosMultiphysics/RomApplication/rom_database.py +526 -0
  16. KratosMultiphysics/RomApplication/rom_manager.py +1145 -0
  17. KratosMultiphysics/RomApplication/rom_nn_interface.py +58 -0
  18. KratosMultiphysics/RomApplication/rom_nn_trainer.py +330 -0
  19. KratosMultiphysics/RomApplication/rom_solver.py +107 -0
  20. KratosMultiphysics/RomApplication/rom_testing_utilities.py +69 -0
  21. KratosMultiphysics/RomApplication/save_rom_coefficients_process.py +120 -0
  22. KratosMultiphysics/RomApplication/tsqr.py +251 -0
  23. kratosromapplication-10.4.4.dist-info/METADATA +14 -0
  24. kratosromapplication-10.4.4.dist-info/RECORD +30 -0
  25. kratosromapplication-10.4.4.dist-info/WHEEL +6 -0
  26. kratosromapplication.dylibs/libKratosCore.dylib +4 -0
  27. kratosromapplication.dylibs/libKratosLinearSolversCore.dylib +0 -0
  28. kratosromapplication.dylibs/libgcc_s.1.1.dylib +0 -0
  29. kratosromapplication.dylibs/libgomp.1.dylib +0 -0
  30. kratosromapplication.dylibs/libstdc++.6.dylib +0 -0
@@ -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
+