Skip to content

Commit 69b1842

Browse files
committed
Add adjoint for thermal radiation
1 parent 1aeae4c commit 69b1842

File tree

4 files changed

+429
-23
lines changed

4 files changed

+429
-23
lines changed

funtofem/driver/funtofem_nlbgs_driver.py

+6
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,12 @@ def _solve_steady_adjoint(self, scenario):
289289
print("Flow solver returned fail flag")
290290
return fail
291291

292+
# Add contribution from thermal radiation
293+
if scenario.thermal_rad:
294+
self.solvers.thermal_rad.iterate_adjoint(
295+
scenario, self.model.bodies, step
296+
)
297+
292298
# Get the structural adjoint rhs
293299
for body in self.model.bodies:
294300
body.transfer_disps_adjoint(scenario)

funtofem/interface/radiation_interface.py

+176-23
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,20 @@
2020
limitations under the License.
2121
"""
2222

23-
from __future__ import print_function
23+
from __future__ import print_function, annotations
2424

2525
__all__ = ["RadiationInterface"]
2626

2727
import numpy as np
2828
import os
29+
from typing import TYPE_CHECKING
2930
from funtofem import TransferScheme
3031
from ._solver_interface import SolverInterface
3132

33+
if TYPE_CHECKING:
34+
from ..model.body import Body
35+
from ..model.scenario import Scenario
36+
3237

3338
class RadiationInterface(SolverInterface):
3439
"""
@@ -38,7 +43,7 @@ class RadiationInterface(SolverInterface):
3843
3944
"""
4045

41-
def __init__(self, comm, model, conv_hist=False):
46+
def __init__(self, comm, model, conv_hist=False, complex_mode=False):
4247
"""
4348
The instantiation of the thermal radiation interface class will populate the model
4449
with the aerodynamic surface mesh, body.aero_X and body.aero_nnodes.
@@ -54,6 +59,7 @@ def __init__(self, comm, model, conv_hist=False):
5459
"""
5560
self.comm = comm
5661
self.conv_hist = conv_hist
62+
self.complex_mode = complex_mode
5763

5864
# setup forward and adjoint tolerances
5965
super().__init__()
@@ -68,6 +74,74 @@ def __init__(self, comm, model, conv_hist=False):
6874
# Get the initial aerodynamic surface meshes
6975
self.initialize(model.scenarios[0], model.bodies)
7076

77+
def set_variables(self, scenario, bodies):
78+
"""
79+
Set the design variables into the solver.
80+
The scenario and bodies objects have dictionaries of :class:`~variable.Variable` objects.
81+
The interface class should pick out which type of variables it needs and pass them into the solver
82+
83+
Parameters
84+
----------
85+
scenario: :class:`~scenario.Scenario`
86+
The current scenario
87+
bodies: list of :class:`~body.Body` objects
88+
The bodies in the model
89+
"""
90+
pass
91+
92+
def set_functions(self, scenario, bodies):
93+
"""
94+
Set the function definitions into the solver.
95+
The scenario has a list of function objects.
96+
97+
Parameters
98+
----------
99+
scenario: :class:`~scenario.Scenario`
100+
The current scenario
101+
bodies: list of :class:`~body.Body` objects
102+
The bodies in the model
103+
"""
104+
pass
105+
106+
def get_functions(self, scenario, bodies):
107+
"""
108+
Put the function values from the solver in the value attribute of the scneario's functions.
109+
The scenario has the list of function objects where the functions owned by this solver will be set.
110+
You can evaluate the functions based on the name or based on the functions set during
111+
:func:`~solver_interface.SolverInterface.set_functions`.
112+
The solver is only responsible for returning the values of functions it owns.
113+
114+
Parameters
115+
----------
116+
scenario: :class:`~scenario.Scenario`
117+
The current scenario
118+
bodies: list of :class:`~body.Body` objects
119+
The bodies in the model
120+
"""
121+
pass
122+
123+
def get_function_gradients(self, scenario, bodies):
124+
"""
125+
Get the derivatives of all the functions with respect to design variables associated with this solver.
126+
127+
Each solver sets the function gradients for its own variables into the function objects using either
128+
``function.set_gradient(var, value)`` or ``function.add_gradient(var, vaule)``. Note that before
129+
this function is called, all gradient components are zeroed.
130+
131+
The derivatives are stored in a dictionary in each function class. As a result, the gradients are
132+
stored in an unordered format. The gradients returned by ``model.get_function_gradients()`` are
133+
flattened into a list of lists whose order is determined by the variable list stored in the model
134+
class.
135+
136+
Parameters
137+
----------
138+
scenario: :class:`~scenario.Scenario`
139+
The current scenario
140+
bodies: list of :class:`~body.Body` objects
141+
The bodies in the model
142+
"""
143+
pass
144+
71145
def initialize(self, scenario, bodies):
72146
"""
73147
Initialize the thermal radiation interface and solver.
@@ -109,26 +183,7 @@ def initialize(self, scenario, bodies):
109183

110184
return 0
111185

112-
def get_functions(self, scenario, bodies):
113-
"""
114-
Populate the scenario with the aerodynamic function values.
115-
116-
Parameters
117-
----------
118-
scenario: :class:`~scenario.Scenario`
119-
The scenario
120-
bodies: :class:`~body.Body`
121-
list of FUNtoFEM bodies
122-
"""
123-
pass
124-
# for function in scenario.functions:
125-
# if function.analysis_type=='aerodynamic':
126-
# # the [6] index returns the value
127-
# if self.comm.Get_rank() == 0:
128-
# function.value = interface.design_pull_composite_func(function.id)[6]
129-
# function.value = self.comm.bcast(function.value,root=0)
130-
131-
def iterate(self, scenario, bodies, step):
186+
def iterate(self, scenario: Scenario, bodies: list[Body], step):
132187
"""
133188
Forward iteration of thermal radiation.
134189
@@ -156,19 +211,93 @@ def iterate(self, scenario, bodies, step):
156211
aero_nnodes = body.get_num_aero_nodes()
157212

158213
aero_temps = body.get_aero_temps(scenario, time_index=step)
214+
print(f"aero_temps: {aero_temps}")
159215
heat_rad = self.calc_heat_flux(aero_temps, scenario)
160216

161217
heat_flux = body.get_aero_heat_flux(scenario, time_index=step)
162218
heat_flux += heat_rad
163219

164220
return 0
165221

222+
def post(self, scenario, bodies):
223+
"""
224+
Perform any tasks the solver needs to do after the forward steps are complete, e.g., evaluate functions,
225+
post-process, deallocate unneeded memory.
226+
227+
Parameters
228+
----------
229+
scenario: :class:`~scenario.Scenario`
230+
The current scenario
231+
bodies: list of :class:`~body.Body` objects
232+
The bodies in the model
233+
"""
234+
pass
235+
236+
def initialize_adjoint(self, scenario, bodies):
237+
"""
238+
Perform any tasks the solver needs to do before taking adjoint steps
239+
240+
Parameters
241+
----------
242+
scenario: :class:`~scenario.Scenario`
243+
The current scenario
244+
bodies: list of :class:`~body.Body` objects
245+
The bodies in the model
246+
"""
247+
return 0
248+
249+
def iterate_adjoint(self, scenario: Scenario, bodies: list[Body], step):
250+
"""
251+
Adjoint iteration of thermal radiation.
252+
253+
Add contribution to aerodynamic temperature adjoint.
254+
"""
255+
256+
nfuncs = scenario.count_adjoint_functions()
257+
for ibody, body in enumerate(bodies, 1):
258+
# Get the adjoint-Jacobian product for the heat flux
259+
aero_flux_ajp = body.get_aero_heat_flux_ajp(scenario)
260+
aero_nnodes = body.get_num_aero_nodes()
261+
aero_flux = body.get_aero_heat_flux(scenario, time_index=step)
262+
aero_temps = body.get_aero_temps(scenario, time_index=step)
263+
264+
aero_temps_ajp = body.get_aero_temps_ajp(scenario)
265+
266+
if aero_flux_ajp is not None and aero_nnodes > 0:
267+
# Solve the aero heat flux computation adjoint
268+
# dR/dhA^{T} * psi_R = - dQ/dhA^{T} * psi_Q = - aero_flux_ajp
269+
psi_R = aero_flux_ajp
270+
271+
rad_heat_deriv = self.calc_heat_flux_deriv(aero_temps, scenario)
272+
273+
dtype = TransferScheme.dtype
274+
lam = np.zeros((aero_nnodes, nfuncs), dtype=dtype)
275+
276+
for func in range(nfuncs):
277+
lam[:, func] = psi_R[:, func] * rad_heat_deriv[:]
278+
279+
if not self.complex_mode:
280+
lam = lam.astype(np.double)
281+
282+
for func in range(nfuncs):
283+
print(f"aero_flux_ajp: {aero_flux_ajp[:, func]}")
284+
print(f"lam: {lam[:, func]}")
285+
aero_flux_ajp[:, func] += lam[:, func]
286+
287+
return
288+
289+
def post_adjoint(self, scenario, bodies):
290+
"""
291+
Any actions that need to be performed after completing the adjoint solve, e.g., evaluating gradients, deallocating memory, etc.
292+
"""
293+
pass
294+
166295
def calc_heat_flux(self, temps, scenario=None):
167296
"""
168297
Implementation of thermal radiation from a surface to a low-temperature environment.
169298
Calculate the heat flux per area as a function of temperature.
170299
171-
Paramters
300+
Parameters
172301
---------
173302
temps:
174303
Array of temperatures.
@@ -191,3 +320,27 @@ def calc_heat_flux(self, temps, scenario=None):
191320
rad_heat[indx] = -sigma_sb * emis * F_v * (temp_i**4 - T_v**4)
192321

193322
return rad_heat
323+
324+
def calc_heat_flux_deriv(self, temps, scenario=None):
325+
"""
326+
Calculate the derivative of radiative heat flux residual dR/dtA
327+
328+
This forms a diagonal matrix, since the heat flux depends only on the temperature
329+
at its corresponding node.
330+
"""
331+
if scenario is None:
332+
emis = 0.8
333+
F_v = 1.0
334+
T_v = 0.0
335+
else:
336+
emis = scenario.emis
337+
F_v = scenario.F_v
338+
T_v = scenario.T_v
339+
340+
sigma_sb = 5.670374419e-8
341+
rad_heat_deriv = np.zeros_like(temps, dtype=TransferScheme.dtype)
342+
343+
for indx, temp_i in enumerate(temps):
344+
rad_heat_deriv[indx] = -4 * sigma_sb * emis * F_v * temp_i**3
345+
346+
return rad_heat_deriv

0 commit comments

Comments
 (0)