1D adv-diff: Comparing Masked POD Galerkin against masked POD LSPG
Overview
This is a follow up to the previous demo here We compare here maskdd Galerkin and masked LSPG.
Main function
The main function of the demo is the following:
logger.setVerbosity([logger.loglevel.info]) # total number of grid points meshSize = 200 # create fom object fomObj = AdvDiff1d(nGrid=meshSize, adv_coef=1.0) # the final time to integrate to finalTime = .05 #--- 1. FOM ---# fomTimeStepSize = 1e-5 fomNumberOfSteps = int(finalTime/fomTimeStepSize) sampleEvery = 100 [fomFinalState, snapshots] = doFom(fomObj, fomTimeStepSize, fomNumberOfSteps, sampleEvery) #--- 2. POD ---# modes = computePodModes(snapshots) #--- 3. MASKED GALERKIN and LSPG ROM ---# # a masked problem is supposed to make it easier to emulate the # effect of hyper-reduction. To create a mask ROM problem, # we need to select and provide to pressio a set of indices # identifying a subset of the grid points in the full mesh. # This is a simple way to mimic hyper-reduction # without changing the FOM problem. In fact, the fom still # computes the full operators but we have an additional step # to "mask" the operators to compute the sample mesh version. # In this test, the meshSize = 200. Our sample mesh includes # the two end points since those contain the boundary conditions, # and 20 randomly selected grid points inside the domain. # So effectively we use 1/10 of the full mesh. random.seed(22123) sampleMeshSize = 20 sampleMeshIndices = random.sample(range(1, 199), sampleMeshSize) sampleMeshIndices = np.append(sampleMeshIndices, [0, 199]) # sort for convenience, not necessarily needed sampleMeshIndices = np.sort(sampleMeshIndices) romSize = 5 # number of modes to use romTimeStepSize = 1e-4 romNumberOfSteps = int(finalTime/romTimeStepSize) # run the masked galerkin problem [approximatedStateGal, romGal] = runMaskedGalerkin(fomObj, romTimeStepSize, romNumberOfSteps, modes[:,:romSize], sampleMeshIndices) # run the masked galerkin problem [approximatedStateLspg, romLspg] = runMaskedLspg(fomObj, romTimeStepSize, romNumberOfSteps, modes[:,:romSize], sampleMeshIndices) # compute l2-error between fom and approximate state fomNorm = linalg.norm(fomFinalState) err1 = linalg.norm(fomFinalState-approximatedStateGal) print("Galerkin: final state relative l2 error: {}".format(err1/fomNorm)) err2 = linalg.norm(fomFinalState-approximatedStateLspg) print("LSPG: final state relative l2 error: {}".format(err2/fomNorm)) logger.finalize() #----------------------------------------------#
1. Run FOM and collect snapshots
def doFom(fom, dt, nsteps, saveFreq): u = fom.u0.copy() U = [u] f = fom.createVelocity() for i in range(1,nsteps+1): # query rhs of discretized system fom.velocity(u, i*dt, f) # simple Euler forward u = u + dt*f if i % saveFreq == 0: U.append(u) Usolns = np.array(U) return [u, Usolns.T]
2. Compute POD modes
def computePodModes(snapshots): print("SVD on matrix: ", snapshots.shape) U,S,VT = np.linalg.svd(snapshots) return U
3. Create the sampling indices
# we need to select and provide to pressio a set of indices # identifying a subset of the grid points in the full mesh. # This is a simple way to mimic hyper-reduction # without changing the FOM problem. In fact, the fom still # computes the full operators but we have an additional step # to "mask" the operators to compute the sample mesh version. # In this test, the meshSize = 200. Our sample mesh includes # the two end points since those contain the boundary conditions, # and 20 randomly selected grid points inside the domain. # So effectively we use 1/10 of the full mesh. random.seed(22123) sampleMeshSize = 20 sampleMeshIndices = random.sample(range(1, 199), sampleMeshSize) sampleMeshIndices = np.append(sampleMeshIndices, [0, 199]) # sort for convenience, not necessarily needed sampleMeshIndices = np.sort(sampleMeshIndices) romSize = 5 # number of modes to use
4. The masker class
class MyMasker: def __init__(self, indices): self.rows_ = indices self.sampleMeshSize_ = len(indices) def createApplyMaskResult(self, operand): if (operand.ndim == 1): return np.zeros(self.sampleMeshSize_) else: return np.zeros((self.sampleMeshSize_, operand.shape[1])) def __call__(self, operand, time, result): if (operand.ndim == 1): result[:] = np.take(operand, self.rows_) else: result[:] = np.take(operand, self.rows_, axis=0)
5. Masked Galerkin ROM
def runMaskedGalerkin(fomObj, dt, nsteps, modes, sampleMeshIndices): # find out number of modes wanted romSize = modes.shape[1] # fom reference state: here it is zero fomReferenceState = np.zeros(fomObj.nGrid) # create ROM state by projecting the fom initial condition fomInitialState = fomObj.u0.copy() romState = np.dot(modes.T, fomInitialState) ''' creating a masked Galerkin problem involves these steps: (1) creating the decoder on the FULL mesh (2) create a "projector operator" by filtering the rows of the POD modes only on the sample mesh (aka mask) indices. The projector is responsible to project the FOM velocity. Note that one can use other matrices for the projector but that will be shown in other demos. (3) create a masker object responsible to mask the FOM operators. (4) create the masked Galerkin problem ''' # 1. create a linear decoder linearDecoder = rom.Decoder(modes) # 2. create the projector # here, simply use "collocation" with the POD modes filtered on the "sample mesh" modesOnSampleMesh = np.take(modes, sampleMeshIndices, axis=0) projector = MyProjector(modesOnSampleMesh) # 3. create the masker object masker = MyMasker(sampleMeshIndices) # 4. create the masked galerkin problem with Euler forward scheme = ode.stepscheme.BDF1 problem = rom.galerkin.MaskedImplicitProblem(scheme, fomObj, linearDecoder, \ romState, fomReferenceState, \ projector, masker) # linear and non linear solver lsO = MyLinSolver() nlsO = solvers.create_newton_raphson(problem, romState, lsO) nlsO.setMaxIterations(15) # solve the problem ode.advance_n_steps(problem, romState, 0., dt, nsteps, nlsO) # after we are done, use the reconstructor object to reconstruct the fom state # NOTE: even though the Galerkin problem was run on the "masked mesh points", # this reconstruction uses the POD modes on the full mesh stored in the decoder # so we can effectively obtain an approximation of the full solution fomRecon = problem.fomStateReconstructor() return [fomRecon(romState), romState]
6. Masked LSPG ROM
def runMaskedLspg(fomObj, dt, nsteps, modes, sampleMeshIndices): # find out number of modes wanted romSize = modes.shape[1] # fom reference state: here it is zero fomReferenceState = np.zeros(fomObj.nGrid) # create ROM state by projecting the fom initial condition fomInitialState = fomObj.u0.copy() romState = np.dot(modes.T, fomInitialState) ''' creating a masked LSPG problem involves these steps: (1) creating the decoder on the FULL mesh (2) create a masker object responsible to mask the FOM operators (3) create the masked LSPG problem ''' # 1. create a linear decoder linearDecoder = rom.Decoder(modes) # 2. create the masker object masker = MyMasker(sampleMeshIndices) # 3. create the masked galerkin problem with Euler forward scheme = ode.stepscheme.BDF1 problem = rom.lspg.unsteady.MaskedProblem(scheme, fomObj, linearDecoder,\ romState, fomReferenceState,\ masker) # linear and non linear solver lsO = MyLinSolver() nlsO = solvers.create_gauss_newton(problem, romState, lsO) nlsO.setMaxIterations(10) # solve the problem ode.advance_n_steps(problem, romState, 0., dt, nsteps, nlsO) # after we are done, use the reconstructor object to reconstruct the fom state # NOTE: even though the Galerkin problem was run on the "masked mesh points", # this reconstruction uses the POD modes on the full mesh stored in the decoder # so we can effectively obtain an approximation of the full solution fomRecon = problem.fomStateReconstructor() return [fomRecon(romState), romState]
Results
If everything works fine, the following plots shows the result. We first plot the result reconstructed only on the sample mesh. This can easily be done using the bases collocated on the sample mesh indices.
We then plot the fom solution reconstructed using the bases on the full mesh. Note that all we need to change is just using the full bases. We see that for this toy example, even with just 10% of the grid, LSPG with 5 modes accuractely reproduces the FOM solution. While for Galerkin the solution is less accurate.