Skip to content

Commit 092d8a3

Browse files
authored
Time accurate adjoint (#152)
* Changed the objFuncHist filename to objFuncTimeSeries to avoid confusion. * Changed the hybridAdjoint flag such that it will not save states when inactive. * Added an option to save intermediate variables for time-accurate adjoint. * Added the dt residual terms and also fixed the missing ddtCorr terms for pisoFoam. * Checked if both hybrid and time-accurate adjoints are active. * Updated pisoFoam tests * Added mesh.moving(false) right after mesh.movePoints() calls. * Updated tests. * Assigned fields and their oldTime(). Adjoint residual matched flow. * Removed the time and timeIndex lists. * Reverted back the time list. * The time accurate residual worked for JacobianFree. * Fixed the issue that the initial residual (ddt part) in the AD code is not correctly computed. * Assigned all oldTimes even a variable does not need them. * Added the calculation of dRdWOldTPsi using AD. * Implemented the off-diagonal block terms. Also fixed a problem for first two step residuals. The last step residual is still not quite right. * Updated the residual for pisoFoam with ts. * Moved the unsteady adjoint functions from PisoFoam to DASolver. * Updated tests. * Added DAPimpleFoam primal. * Added residual computation for pimpleFoam. * Added the initOldTimes function to fix the issue that the first setTimeInstance call could not set the correct oldTime field. * Updated the unsteadyAdjoint option. * Reverted the adjoint calling sequence for hybridAdjoint. * Updated test. * Fixed an issue in assigning dRdWOldTPsi. Added zero timeAccurateVecs function. * Fixed an issue for computing mean field. * Fixed an issue for oldTime derivative by not registering oldTime fields if we do not need them in residual computation. Not doing this will get completely off adjoint derivative, especially for the calcdRdWTOldPsi function. * Added the DALaplacianFoam solver. * Removed all ResPartDeriv variables because they are not used. * Fixed the missing part in solid MakeFiles. * Added DAScalarTransportFoam. * Fixed bugs for Laplacian and ScalarTransportFoam. Derivs not accurate. * Fixed a bug in the actuatorDisk and actuatorLine models that impacted their circ force computation. * Fixed a bug in the fCirc calculation for actuators to make the profile more smooth. * Fixed a minor issue. * Updated the tests. * Relaxed the test tolerance. * Changed the version to 226
1 parent dc1f08f commit 092d8a3

File tree

109 files changed

+4052
-909
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

109 files changed

+4052
-909
lines changed

dafoam/optFuncs.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,10 @@ def calcObjFuncValuesMP(xDV):
136136
return funcsMP, fail
137137

138138

139-
def calcObjFuncValuesHybridAdjoint(xDV):
139+
def calcObjFuncValuesUnsteady(xDV):
140140
"""
141141
Update the design surface and run the primal solver to get objective function values.
142-
This is the hybrid adjoint version of calcObjFuncValues
142+
This is the unsteady adjoint version of calcObjFuncValues
143143
"""
144144

145145
Info("\n")
@@ -164,9 +164,12 @@ def calcObjFuncValuesHybridAdjoint(xDV):
164164
# Solve the CFD problem
165165
DASolver()
166166

167-
# Set values for the hybrid adjoint objectives. This function needs to be
167+
# Set values for the unsteady adjoint objectives. This function needs to be
168168
# implemented in run scripts
169-
setHybridAdjointObjFuncs(DASolver, funcs, evalFuncs)
169+
setObjFuncsUnsteady(DASolver, funcs, evalFuncs)
170+
171+
# assign state lists to mats
172+
DASolver.setTimeInstanceVar(mode="list2Mat")
170173

171174
b = time.time()
172175

@@ -217,7 +220,7 @@ def calcObjFuncSens(xDV, funcs):
217220
Info("Objective Function Sensitivity: ")
218221
Info(funcsSens)
219222
Info("Adjoint Runtime: %g s" % (b - a))
220-
223+
221224
# write the sensitivity values to file
222225
DASolver.writeTotalDeriv("totalDerivHist.txt", funcsSens, evalFuncs)
223226

@@ -299,10 +302,10 @@ def calcObjFuncSensMP(xDV, funcs):
299302
return funcsSensMP, fail
300303

301304

302-
def calcObjFuncSensHybridAdjoint(xDV, funcs):
305+
def calcObjFuncSensUnsteady(xDV, funcs):
303306
"""
304307
Run the adjoint solver and get objective function sensitivities.
305-
This is the hybrid adjoint version of calcObjFuncSens
308+
This is the unsteady adjoint version of calcObjFuncSens
306309
"""
307310

308311
Info("\n")
@@ -320,14 +323,32 @@ def calcObjFuncSensHybridAdjoint(xDV, funcs):
320323
# write the deform FFDs
321324
DASolver.writeDeformedFFDs()
322325

326+
# assign the state mats to lists
327+
DASolver.setTimeInstanceVar(mode="mat2List")
328+
323329
# Setup an empty dictionary for the evaluated derivative values
324330
funcsSensCombined = {}
325331

326332
funcsSensAllInstances = []
327333

328-
nTimeInstances = DASolver.getOption("hybridAdjoint")["nTimeInstances"]
334+
mode = DASolver.getOption("unsteadyAdjoint")["mode"]
335+
nTimeInstances = DASolver.getOption("unsteadyAdjoint")["nTimeInstances"]
336+
if mode == "hybridAdjoint":
337+
iEnd = -1
338+
elif mode == "timeAccurateAdjoint":
339+
iEnd = 0
340+
341+
# NOTE: calling calcRes here is critical because it will setup the correct
342+
# old time levels for setTimeInstanceField. Otherwise, the residual for the
343+
# first adjoint time instance will be incorrect because the residuals have
344+
# not been computed and the old time levels will be zeros for all variables,
345+
# this will create issues for the setTimeInstanceField call (nOldTimes)
346+
DASolver.calcPrimalResidualStatistics("calc")
347+
348+
# set these vectors zeros
349+
DASolver.zeroTimeAccurateAdjointVectors()
329350

330-
for i in range(nTimeInstances):
351+
for i in range(nTimeInstances - 1, iEnd, -1):
331352

332353
Info("--Solving Adjoint for Time Instance %d--" % i)
333354

@@ -355,13 +376,13 @@ def calcObjFuncSensHybridAdjoint(xDV, funcs):
355376

356377
funcsSensAllInstances.append(funcsSens)
357378

358-
setHybridAdjointObjFuncsSens(DASolver, funcs, funcsSensAllInstances, funcsSensCombined)
379+
setObjFuncsSensUnsteady(DASolver, funcs, funcsSensAllInstances, funcsSensCombined)
359380

360381
funcsSensCombined["fail"] = fail
361382

362383
# Print the current solution to the screen
363384
with np.printoptions(precision=16, threshold=5, suppress=True):
364-
Info("Objective Function Sensitivity Hybrid Adjoint: ")
385+
Info("Objective Function Sensitivity Unsteady Adjoint: ")
365386
Info(funcsSensCombined)
366387

367388
b = time.time()

dafoam/pyDAFoam.py

Lines changed: 176 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
1212
"""
1313

14-
__version__ = "2.2.5"
14+
__version__ = "2.2.6"
1515

1616
import subprocess
1717
import os
@@ -57,6 +57,7 @@ class DAOPTION(object):
5757
## - DASimpleFoam: Incompressible steady-state flow solver for Navier-Stokes equations
5858
## - DASimpleTFoam: Incompressible steady-state flow solver for Navier-Stokes equations with temperature
5959
## - DAPisoFoam: Incompressible transient flow solver for Navier-Stokes equations
60+
## - DAPimpleFoam: Incompressible transient flow solver for Navier-Stokes equations
6061
## - DARhoSimpleFoam: Compressible steady-state flow solver for Navier-Stokes equations (subsonic)
6162
## - DARhoSimpleCFoam: Compressible steady-state flow solver for Navier-Stokes equations (transonic)
6263
## - DATurboFoam: Compressible steady-state flow solver for Navier-Stokes equations (turbomachinery)
@@ -228,6 +229,20 @@ class DAOPTION(object):
228229
## "addToAdjoint": True,
229230
## }
230231
## },
232+
## "THRUST": {
233+
## "part1": {
234+
## "type": "variableVolSum",
235+
## "source": "boxToCell",
236+
## "min": [-50.0, -50.0, -50.0],
237+
## "max": [50.0, 50.0, 50.0],
238+
## "varName": "fvSource",
239+
## "varType": "vector",
240+
## "component": 0,
241+
## "isSquare": 0,
242+
## "scale": 1.0,
243+
## "addToAdjoint": True,
244+
## },
245+
## },
231246
## "FI": {
232247
## "part1": {
233248
## "type": "stateErrorNorm",
@@ -397,9 +412,10 @@ class DAOPTION(object):
397412
## This is used only for transonic solvers such as DARhoSimpleCFoam
398413
transonicPCOption = -1
399414

400-
## Options for hybrid adjoint. Here nTimeInstances is the number of time instances
401-
## periodicity is the periodicity of flow oscillation
402-
hybridAdjoint = {"active": False, "nTimeInstances": -1, "periodicity": -1.0}
415+
## Options for unsteady adjoint. mode can be hybridAdjoint or timeAccurateAdjoint
416+
## Here nTimeInstances is the number of time instances and periodicity is the
417+
## periodicity of flow oscillation (hybrid adjoint only)
418+
unsteadyAdjoint = {"mode": "None", "nTimeInstances": -1, "periodicity": -1.0}
403419

404420
## At which iteration should we start the averaging of objective functions.
405421
## This is only used for unsteady solvers
@@ -703,6 +719,12 @@ def __init__(self, comm=None, options=None):
703719
# initialize the adjoint vector dict
704720
self.adjVectors = self._initializeAdjVectors()
705721

722+
# initialize the dRdWOldTPsi vectors
723+
self._initializeTimeAccurateAdjointVectors()
724+
725+
# check if the combination of options is valid.
726+
self._checkOptions()
727+
706728
Info("pyDAFoam initialization done!")
707729

708730
return
@@ -714,9 +736,9 @@ def _solverRegistry(self):
714736
"""
715737

716738
self.solverRegistry = {
717-
"Incompressible": ["DASimpleFoam", "DASimpleTFoam", "DAPisoFoam"],
739+
"Incompressible": ["DASimpleFoam", "DASimpleTFoam", "DAPisoFoam", "DAPimpleFoam"],
718740
"Compressible": ["DARhoSimpleFoam", "DARhoSimpleCFoam", "DATurboFoam"],
719-
"Solid": ["DASolidDisplacementFoam"],
741+
"Solid": ["DASolidDisplacementFoam", "DALaplacianFoam", "DAScalarTransportFoam"],
720742
}
721743

722744
def __call__(self):
@@ -831,6 +853,62 @@ def _initializeAdjTotalDeriv(self):
831853

832854
return adjTotalDeriv
833855

856+
def _initializeTimeAccurateAdjointVectors(self):
857+
"""
858+
Initialize the dRdWTPsi vectors for time accurate adjoint.
859+
Here we need to initialize current time step and two previous
860+
time steps 0 and 00 for both state and residuals. This is
861+
because the backward ddt scheme depends on U, U0, and U00
862+
"""
863+
if self.getOption("unsteadyAdjoint")["mode"] == "timeAccurateAdjoint":
864+
objFuncDict = self.getOption("objFunc")
865+
wSize = self.solver.getNLocalAdjointStates()
866+
self.dRdW0TPsi = {}
867+
self.dRdW00TPsi = {}
868+
self.dR0dW0TPsi = {}
869+
self.dR0dW00TPsi = {}
870+
self.dR00dW0TPsi = {}
871+
self.dR00dW00TPsi = {}
872+
for objFuncName in objFuncDict:
873+
if objFuncName in self.objFuncNames4Adj:
874+
vecA = PETSc.Vec().create(PETSc.COMM_WORLD)
875+
vecA.setSizes((wSize, PETSc.DECIDE), bsize=1)
876+
vecA.setFromOptions()
877+
vecA.zeroEntries()
878+
self.dRdW0TPsi[objFuncName] = vecA
879+
880+
vecB = vecA.duplicate()
881+
vecB.zeroEntries()
882+
self.dRdW00TPsi[objFuncName] = vecB
883+
884+
vecC = vecA.duplicate()
885+
vecC.zeroEntries()
886+
self.dR0dW0TPsi[objFuncName] = vecC
887+
888+
vecD = vecA.duplicate()
889+
vecD.zeroEntries()
890+
self.dR0dW00TPsi[objFuncName] = vecD
891+
892+
vecE = vecA.duplicate()
893+
vecE.zeroEntries()
894+
self.dR00dW0TPsi[objFuncName] = vecE
895+
896+
vecF = vecA.duplicate()
897+
vecF.zeroEntries()
898+
self.dR00dW00TPsi[objFuncName] = vecF
899+
900+
def zeroTimeAccurateAdjointVectors(self):
901+
if self.getOption("unsteadyAdjoint")["mode"] == "timeAccurateAdjoint":
902+
objFuncDict = self.getOption("objFunc")
903+
for objFuncName in objFuncDict:
904+
if objFuncName in self.objFuncNames4Adj:
905+
self.dRdW0TPsi[objFuncName].zeroEntries()
906+
self.dRdW00TPsi[objFuncName].zeroEntries()
907+
self.dR0dW0TPsi[objFuncName].zeroEntries()
908+
self.dR0dW00TPsi[objFuncName].zeroEntries()
909+
self.dR00dW0TPsi[objFuncName].zeroEntries()
910+
self.dR00dW00TPsi[objFuncName].zeroEntries()
911+
834912
def _calcObjFuncNames4Adj(self):
835913
"""
836914
Compute the objective function names for which we solve the adjoint equation
@@ -857,6 +935,20 @@ def _calcObjFuncNames4Adj(self):
857935
raise Error("addToAdjoint can be either True or False")
858936
return objFuncList
859937

938+
def _checkOptions(self):
939+
"""
940+
Check if the combination of options are valid.
941+
For example, if timeAccurateAdjoint is active, we have to set
942+
adjJacobianOption = JacobianFree
943+
NOTE: we should add all possible checks here!
944+
"""
945+
# check time accurate adjoint
946+
if self.getOption("unsteadyAdjoint")["mode"] == "timeAccurateAdjoint":
947+
if not self.getOption("adjJacobianOption") == "JacobianFree":
948+
raise Error("timeAccurateAdjoint only supports adjJacobianOption=JacobianFree!")
949+
950+
# check other combinations...
951+
860952
def saveMultiPointField(self, indexMP):
861953
"""
862954
Save the state variable vector to self.wVecMPList
@@ -885,18 +977,76 @@ def setMultiPointField(self, indexMP):
885977

886978
return
887979

980+
def calcPrimalResidualStatistics(self, mode):
981+
if self.getOption("adjJacobianOption") == "JacobianFD":
982+
self.solver.calcPrimalResidualStatistics(mode.encode())
983+
elif self.getOption("adjJacobianOption") == "JacobianFree":
984+
self.solverAD.calcPrimalResidualStatistics(mode.encode())
985+
888986
def setTimeInstanceField(self, instanceI):
889987
"""
890988
Set the OpenFOAM state variables based on instance index
891989
"""
892990

893-
self.solver.setTimeInstanceField(instanceI)
991+
if self.getOption("adjJacobianOption") == "JacobianFD":
992+
solver = self.solver
993+
elif self.getOption("adjJacobianOption") == "JacobianFree":
994+
solver = self.solverAD
995+
996+
solver.setTimeInstanceField(instanceI)
894997
# NOTE: we need to set the OF field to wVec vector here!
895998
# this is because we will assign self.wVec to the solveAdjoint function later
896-
self.solver.ofField2StateVec(self.wVec)
999+
solver.ofField2StateVec(self.wVec)
8971000

8981001
return
8991002

1003+
def initTimeInstanceMats(self):
1004+
1005+
nLocalAdjointStates = self.solver.getNLocalAdjointStates()
1006+
nLocalAdjointBoundaryStates = self.solver.getNLocalAdjointBoundaryStates()
1007+
nTimeInstances = -99999
1008+
adjMode = self.getOption("unsteadyAdjoint")["mode"]
1009+
if adjMode == "hybridAdjoint" or adjMode == "timeAccurateAdjoint":
1010+
nTimeInstances = self.getOption("unsteadyAdjoint")["nTimeInstances"]
1011+
1012+
self.stateMat = PETSc.Mat().create(PETSc.COMM_WORLD)
1013+
self.stateMat.setSizes(((nLocalAdjointStates, None), (None, nTimeInstances)))
1014+
self.stateMat.setFromOptions()
1015+
self.stateMat.setPreallocationNNZ((nTimeInstances, nTimeInstances))
1016+
self.stateMat.setUp()
1017+
1018+
self.stateBCMat = PETSc.Mat().create(PETSc.COMM_WORLD)
1019+
self.stateBCMat.setSizes(((nLocalAdjointBoundaryStates, None), (None, nTimeInstances)))
1020+
self.stateBCMat.setFromOptions()
1021+
self.stateBCMat.setPreallocationNNZ((nTimeInstances, nTimeInstances))
1022+
self.stateBCMat.setUp()
1023+
1024+
self.timeVec = PETSc.Vec().createSeq(nTimeInstances, bsize=1, comm=PETSc.COMM_SELF)
1025+
self.timeIdxVec = PETSc.Vec().createSeq(nTimeInstances, bsize=1, comm=PETSc.COMM_SELF)
1026+
1027+
def initOldTimes(self):
1028+
# No need to initialize oldTimes for FD
1029+
if self.getOption("adjJacobianOption") == "JacobianFree":
1030+
self.solverAD.initOldTimes()
1031+
1032+
def setTimeInstanceVar(self, mode):
1033+
1034+
if mode == "list2Mat":
1035+
self.solver.setTimeInstanceVar(mode.encode(), self.stateMat, self.stateBCMat, self.timeVec, self.timeIdxVec)
1036+
elif mode == "mat2List":
1037+
if self.getOption("adjJacobianOption") == "JacobianFD":
1038+
self.solver.setTimeInstanceVar(
1039+
mode.encode(), self.stateMat, self.stateBCMat, self.timeVec, self.timeIdxVec
1040+
)
1041+
elif self.getOption("adjJacobianOption") == "JacobianFree":
1042+
self.solverAD.setTimeInstanceVar(
1043+
mode.encode(), self.stateMat, self.stateBCMat, self.timeVec, self.timeIdxVec
1044+
)
1045+
else:
1046+
raise Error("adjJacobianOption can only be either JacobianFD or JacobianFree!")
1047+
else:
1048+
raise Error("mode can only be either mat2List or list2Mat!")
1049+
9001050
def writeDesignVariable(self, fileName, xDV):
9011051
"""
9021052
Write the design variable history to files in the json format
@@ -1703,12 +1853,26 @@ def solveAdjoint(self):
17031853
elif self.getOption("adjJacobianOption") == "JacobianFree":
17041854
self.solverAD.calcdFdWAD(self.xvVec, self.wVec, objFuncName.encode(), dFdW)
17051855

1856+
# if it is time accurate adjoint, add extra terms for dFdW
1857+
if self.getOption("unsteadyAdjoint")["mode"] == "timeAccurateAdjoint":
1858+
# first copy the vectors from previous residual time step level
1859+
self.dR0dW0TPsi[objFuncName].copy(self.dR00dW0TPsi[objFuncName])
1860+
self.dR0dW00TPsi[objFuncName].copy(self.dR00dW00TPsi[objFuncName])
1861+
self.dRdW0TPsi[objFuncName].copy(self.dR0dW0TPsi[objFuncName])
1862+
self.dRdW00TPsi[objFuncName].copy(self.dR0dW00TPsi[objFuncName])
1863+
dFdW.axpy(-1.0, self.dR0dW0TPsi[objFuncName])
1864+
dFdW.axpy(-1.0, self.dR00dW00TPsi[objFuncName])
1865+
17061866
# Initialize the adjoint vector psi and solve for it
17071867
if self.getOption("adjJacobianOption") == "JacobianFD":
17081868
self.adjointFail = self.solver.solveLinearEqn(ksp, dFdW, self.adjVectors[objFuncName])
17091869
elif self.getOption("adjJacobianOption") == "JacobianFree":
17101870
self.adjointFail = self.solverAD.solveLinearEqn(ksp, dFdW, self.adjVectors[objFuncName])
17111871

1872+
if self.getOption("unsteadyAdjoint")["mode"] == "timeAccurateAdjoint":
1873+
self.solverAD.calcdRdWOldTPsiAD(1, self.adjVectors[objFuncName], self.dRdW0TPsi[objFuncName])
1874+
self.solverAD.calcdRdWOldTPsiAD(2, self.adjVectors[objFuncName], self.dRdW00TPsi[objFuncName])
1875+
17121876
dFdW.destroy()
17131877

17141878
ksp.destroy()
@@ -2206,6 +2370,10 @@ def _initSolver(self):
22062370
if self.getOption("printDAOptions"):
22072371
self.solver.printAllOptions()
22082372

2373+
adjMode = self.getOption("unsteadyAdjoint")["mode"]
2374+
if adjMode == "hybridAdjoint" or adjMode == "timeAccurateAdjoint":
2375+
self.initTimeInstanceMats()
2376+
22092377
self.solverInitialized = 1
22102378

22112379
return

0 commit comments

Comments
 (0)