NVIDIA Modulus Sym v1.1.0
Sym v1.1.0

Conjugate Heat Transfer

This tutorial uses Modulus Sym to study the conjugate heat transfer between the heat sink and the surrounding fluid. The temperature variations inside solid and fluid would be solved in a coupled manner with appropriate interface boundary conditions. In this tutorial, you will learn:

  1. How to generate a 3D geometry using the geometry module in Modulus Sym.

  2. How to set up a conjugate heat transfer problem using the interface boundary conditions in Modulus Sym.

  3. How to use the Multi-Phase training approach in Modulus Sym for one way coupled problems.

Note

This tutorial assumes that you have completed tutorial Introductory Example on and have familiarized yourself with the basics of the Modulus Sym APIs. Also, you should review the Scalar Transport: 2D Advection Diffusion tutorial for additional details on writing some of the thermal boundary conditions.

Note

The scripts used in this problem can be found at examples/three_fin_3d/. The scripts are made configurable such that the same three fin problem can be solved for higher reynolds numbers, parameterized geometry, etc. very easily by only configuring the Hydra configs. This example focuses on the laminar variant of this as the purpose of this tutorial is to cover the ideas related to multi-phase training and conjugate heat transfer. The tutorial Parameterized 3D Heat Sink covers the parameterization aspect of this problem.

Therefore in this problem, the custom configs for turbulent and parameterized are both set to false.

The geometry for a 3-fin heat sink placed inside a channel is shown in Fig. 142. The inlet to the channel is at 1 \(m/s\). The pressure at the outlet is specified as 0 \(Pa\). All the other surfaces of the geometry are treated as no-slip walls.

threeFin_geom.png

Fig. 142 Three fin heat sink geometry (All dimensions in \(m\))

The inlet is at 273.15 \(K\). The channel walls are adiabatic. The heat sink has a heat source of \(0.2 \times 0.4\) \(m\) at the bottom of the heat sink situated centrally on the bottom surface. The heat source generates heat such that the temperature gradient on the source surface is 360 \(K/m\) in the normal direction. Conjugate heat transfer takes place between the fluid-solid contact surface.

The properties fluid and thermal properties of the fluid and the solid are as follows:

Table 5 Fluid and Solid Properties
Property Fluid Solid
Kinematic Viscosity \((m^2/s)\) 0.02 NA
Thermal Diffusivity \((m^2/s)\) 0.02 0.0625
Thermal Conductivity \((W/m.K)\) 1.0 5.0

In this tutorial, since you are dealing with only incompressible flow, there is a one way coupling between the heat and flow equations. This means that it is possible to train the temperature field after the flow field is trained and converged. Such an approach is useful while training the multiphysics problems which are one way coupled as it is possible to achieve significant speed-up, as well as simulate cases with same flow boundary conditions but different thermal boundary conditions. One can easily use the same flow field as in input to train for different thermal boundary conditions.

Therefore, for this problem you have three separate files for the geometry, flow solver, and heat solver. The three_fin_geometry.py will contain all the definitions of geometry. three_fin_flow.py and three_fin_thermal.py would then use this geometry to setup the relevant flow and heat constraints and solve them individually. The basic idea would be to train the flow model to convergence and then start the heat training after by initializing from the trained flow model to solve for the temperature distributions in fluid and solid simultaneously.

In this problem you will nondimensionalize the temperature according to the following equation:

(207)\[\theta= T/273.15 - 1.0\]

Creating Geometry

The three_fin_geometry.py script contains all the details relevant to the geometry generation. We will use the Box primitive to create the 3-fin geometry and Channel primitive to generate the channel. Similar to 2D, Channel and Box are defined by using the two corner points. Like 2D, the Channel geometry has no bounding planes in the x-direction. You will also make use of the repeat method to create the fins. This speeds up the generation of repetitive structures in comparison to constructing the same geometry separately and doing boolean operations to assemble them.

Use the Plane geometry to create the planes at the inlet and outlet. The code for generating the required geometries is shown below. Please note the normal directions for the inlet and outlet planes.

Additionally, the parameters required for solving the heat part as also defined upfront, ex. dimensions and locations of source etc.

Note

The script contains a few extra definitions that are only relevant for parameterized geometry. These are not relevant for this tutorial.

Copy
Copied!
            

# define sympy varaibles to parametize domain curves x, y, z = Symbol("x"), Symbol("y"), Symbol("z") x_pos = Symbol("x_pos") # parametric variation fin_height_m, fin_height_s = Symbol("fin_height_m"), Symbol("fin_height_s") fin_length_m, fin_length_s = Symbol("fin_length_m"), Symbol("fin_length_s") fin_thickness_m, fin_thickness_s = Symbol("fin_thickness_m"), Symbol("fin_thickness_s") height_m_range = (0.0, 0.6) height_s_range = (0.0, 0.6) length_m_range = (0.5, 1.0) length_s_range = (0.5, 1.0) thickness_m_range = (0.05, 0.15) thickness_s_range = (0.05, 0.15) param_ranges = { fin_height_m: height_m_range, fin_height_s: height_s_range, fin_length_m: length_m_range, fin_length_s: length_s_range, fin_thickness_m: thickness_m_range, fin_thickness_s: thickness_s_range, } fixed_param_ranges = { fin_height_m: 0.4, fin_height_s: 0.4, fin_length_m: 1.0, fin_length_s: 1.0, fin_thickness_m: 0.1, fin_thickness_s: 0.1, } # geometry params for domain channel_origin = (-2.5, -0.5, -0.5) channel_dim = (5.0, 1.0, 1.0) heat_sink_base_origin = (-1.0, -0.5, -0.3) heat_sink_base_dim = (1.0, 0.2, 0.6) fin_origin = (heat_sink_base_origin[0] + 0.5 - fin_length_s / 2, -0.3, -0.3) fin_dim = (fin_length_s, fin_height_s, fin_thickness_s) # two side fins total_fins = 2 # two side fins flow_box_origin = (-1.1, -0.5, -0.5) flow_box_dim = (1.6, 1.0, 1.0) source_origin = (-0.7, -0.5, -0.1) source_dim = (0.4, 0.0, 0.2) source_area = 0.08 # define geometry class ThreeFin(object): def __init__(self, parameterized: bool = False): # set param ranges if parameterized: pr = Parameterization(param_ranges) self.pr = param_ranges else: pr = Parameterization(fixed_param_ranges) self.pr = fixed_param_ranges # channel self.channel = Channel( channel_origin, ( channel_origin[0] + channel_dim[0], channel_origin[1] + channel_dim[1], channel_origin[2] + channel_dim[2], ), parameterization=pr, ) # three fin heat sink heat_sink_base = Box( heat_sink_base_origin, ( heat_sink_base_origin[0] + heat_sink_base_dim[0], # base of heat sink heat_sink_base_origin[1] + heat_sink_base_dim[1], heat_sink_base_origin[2] + heat_sink_base_dim[2], ), parameterization=pr, ) fin_center = ( fin_origin[0] + fin_dim[0] / 2, fin_origin[1] + fin_dim[1] / 2, fin_origin[2] + fin_dim[2] / 2, ) fin = Box( fin_origin, ( fin_origin[0] + fin_dim[0], fin_origin[1] + fin_dim[1], fin_origin[2] + fin_dim[2], ), parameterization=pr, ) gap = (heat_sink_base_dim[2] - fin_dim[2]) / ( total_fins - 1 ) # gap between fins fin_2 = fin.translate([0, 0, gap]) fin = fin + fin_2 three_fin = heat_sink_base + fin # parameterized center fin center_fin_origin = ( heat_sink_base_origin[0] + 0.5 - fin_length_m / 2, fin_origin[1], -fin_thickness_m / 2, ) center_fin_dim = (fin_length_m, fin_height_m, fin_thickness_m) center_fin = Box( center_fin_origin, ( center_fin_origin[0] + center_fin_dim[0], center_fin_origin[1] + center_fin_dim[1], center_fin_origin[2] + center_fin_dim[2], ), parameterization=pr, ) self.three_fin = three_fin + center_fin # entire geometry self.geo = self.channel - self.three_fin # low and high resultion geo away and near the heat sink flow_box = Box( flow_box_origin, ( flow_box_origin[0] + flow_box_dim[0], # base of heat sink flow_box_origin[1] + flow_box_dim[1], flow_box_origin[2] + flow_box_dim[2], ), ) self.lr_geo = self.geo - flow_box self.hr_geo = self.geo & flow_box lr_bounds_x = (channel_origin[0], channel_origin[0] + channel_dim[0]) lr_bounds_y = (channel_origin[1], channel_origin[1] + channel_dim[1]) lr_bounds_z = (channel_origin[2], channel_origin[2] + channel_dim[2]) self.lr_bounds = {x: lr_bounds_x, y: lr_bounds_y, z: lr_bounds_z} hr_bounds_x = (flow_box_origin[0], flow_box_origin[0] + flow_box_dim[0]) hr_bounds_y = (flow_box_origin[1], flow_box_origin[1] + flow_box_dim[1]) hr_bounds_z = (flow_box_origin[2], flow_box_origin[2] + flow_box_dim[2]) self.hr_bounds = {x: hr_bounds_x, y: hr_bounds_y, z: hr_bounds_z} # inlet and outlet self.inlet = Plane( channel_origin, ( channel_origin[0], channel_origin[1] + channel_dim[1], channel_origin[2] + channel_dim[2], ), -1, parameterization=pr, ) self.outlet = Plane( (channel_origin[0] + channel_dim[0], channel_origin[1], channel_origin[2]), ( channel_origin[0] + channel_dim[0], channel_origin[1] + channel_dim[1], channel_origin[2] + channel_dim[2], ), 1, parameterization=pr, ) # planes for integral continuity self.integral_plane = Plane( (x_pos, channel_origin[1], channel_origin[2]), ( x_pos, channel_origin[1] + channel_dim[1], channel_origin[2] + channel_dim[2], ), 1, )

Neural network, Nodes and Multi-Phase training

Let’s have a look at the networks and nodes required to solve the flow and heat for this problem. The architectures and nodes for flow problem are very similar to previous tutorials. You will add the nodes for NavierStokes and NormalDotVec and create a single flow network that has the coordinates as inputs and the velocity components and the pressure as output. The code for the flow nodes can be found here:

Copy
Copied!
            

# make navier stokes equations if cfg.custom.turbulent: ze = ZeroEquation(nu=0.002, dim=3, time=False, max_distance=0.5) ns = NavierStokes(nu=ze.equations["nu"], rho=1.0, dim=3, time=False) navier_stokes_nodes = ns.make_nodes() + ze.make_nodes() else: ns = NavierStokes(nu=0.01, rho=1.0, dim=3, time=False) navier_stokes_nodes = ns.make_nodes() normal_dot_vel = NormalDotVec() # make network arch if cfg.custom.parameterized: input_keys = [ Key("x"), Key("y"), Key("z"), Key("fin_height_m"), Key("fin_height_s"), Key("fin_length_m"), Key("fin_length_s"), Key("fin_thickness_m"), Key("fin_thickness_s"), ] else: input_keys = [Key("x"), Key("y"), Key("z")] flow_net = FullyConnectedArch( input_keys=input_keys, output_keys=[Key("u"), Key("v"), Key("w"), Key("p")] ) # make list of nodes to unroll graph on flow_nodes = ( navier_stokes_nodes + normal_dot_vel.make_nodes() + [flow_net.make_node(name="flow_network")] )

For the thermal nodes, start by adding nodes for relevant equations like AdvectionDiffusion, Diffusion, DiffusionInterface and GradNormal that will be used to define the various thermal boundary conditions relevant to this problem. Also, create 3 separate neural networks flow_net, thermal_f_net and thermal_s_net. The first one is the same flow network defined in the flow scripts. This network architecture definition in heat script must exactly match to that of the flow script for successful initialization of the flow model during heat training. Set the optimize argument as False while making the nodes of flow network to avoid optimizing the flow network during the heat training. Finally, separate networks to predict the temperatures in fluid and solid are created.

Copy
Copied!
            

# make thermal equations ad = AdvectionDiffusion(T="theta_f", rho=1.0, D=0.02, dim=3, time=False) dif = Diffusion(T="theta_s", D=0.0625, dim=3, time=False) dif_inteface = DiffusionInterface("theta_f", "theta_s", 1.0, 5.0, dim=3, time=False) f_grad = GradNormal("theta_f", dim=3, time=False) s_grad = GradNormal("theta_s", dim=3, time=False) # make network arch if cfg.custom.parameterized: input_keys = [ Key("x"), Key("y"), Key("z"), Key("fin_height_m"), Key("fin_height_s"), Key("fin_length_m"), Key("fin_length_s"), Key("fin_thickness_m"), Key("fin_thickness_s"), ] else: input_keys = [Key("x"), Key("y"), Key("z")] flow_net = FullyConnectedArch( input_keys=input_keys, output_keys=[Key("u"), Key("v"), Key("w"), Key("p")], ) thermal_f_net = FullyConnectedArch( input_keys=input_keys, output_keys=[Key("theta_f")] ) thermal_s_net = FullyConnectedArch( input_keys=input_keys, output_keys=[Key("theta_s")] ) # make list of nodes to unroll graph on thermal_nodes = ( ad.make_nodes() + dif.make_nodes() + dif_inteface.make_nodes() + f_grad.make_nodes() + s_grad.make_nodes() + [flow_net.make_node(name="flow_network", optimize=False)] + [thermal_f_net.make_node(name="thermal_f_network")] + [thermal_s_net.make_node(name="thermal_s_network")] )

Setting up Flow Domain and Constraints

The contents of three_fin_flow.py script are described below.

Inlet, Outlet and Channel and Heat Sink walls

For inlet boundary conditions, specify the velocity to be a constant velocity of 1.0 \(m/s\) in x-direction. Like in tutorial Introductory Example, weight the velocity by the SDF of the channel to avoid sharp discontinuity at the boundaries. For outlet, specify the pressure to be 0. All the channel walls and heat sink walls are treated as no slip boundaries.

Interior

The flow equations can be specified in the low resolution and high resolution domains of the problem by using PointwiseInteriorConstraint. This allows independent point densities in these two areas to be controlled easily.

Integral Continuity

The inlet volumetric flow is 1 \(m^3/s\) so, specify 1.0 as the value for integral_continuity key.

The code for flow domain is shown below.

Copy
Copied!
            

geo = ThreeFin(parameterized=cfg.custom.parameterized) # params for simulation # fluid params inlet_vel = 1.0 volumetric_flow = 1.0 # make flow domain flow_domain = Domain() # inlet u_profile = inlet_vel * tanh((0.5 - Abs(y)) / 0.02) * tanh((0.5 - Abs(z)) / 0.02) constraint_inlet = PointwiseBoundaryConstraint( nodes=flow_nodes, geometry=geo.inlet, outvar={"u": u_profile, "v": 0, "w": 0}, batch_size=cfg.batch_size.Inlet, criteria=Eq(x, channel_origin[0]), lambda_weighting={ "u": 1.0, "v": 1.0, "w": 1.0, }, # weight zero on edges parameterization=geo.pr, batch_per_epoch=5000, ) flow_domain.add_constraint(constraint_inlet, "inlet") # outlet constraint_outlet = PointwiseBoundaryConstraint( nodes=flow_nodes, geometry=geo.outlet, outvar={"p": 0}, batch_size=cfg.batch_size.Outlet, criteria=Eq(x, channel_origin[0] + channel_dim[0]), lambda_weighting={"p": 1.0}, parameterization=geo.pr, batch_per_epoch=5000, ) flow_domain.add_constraint(constraint_outlet, "outlet") # no slip for channel walls no_slip = PointwiseBoundaryConstraint( nodes=flow_nodes, geometry=geo.geo, outvar={"u": 0, "v": 0, "w": 0}, batch_size=cfg.batch_size.NoSlip, lambda_weighting={ "u": 1.0, "v": 1.0, "w": 1.0, }, # weight zero on edges parameterization=geo.pr, batch_per_epoch=5000, ) flow_domain.add_constraint(no_slip, "no_slip") # flow interior low res away from three fin lr_interior = PointwiseInteriorConstraint( nodes=flow_nodes, geometry=geo.geo, outvar={"continuity": 0, "momentum_x": 0, "momentum_y": 0, "momentum_z": 0}, batch_size=cfg.batch_size.InteriorLR, lambda_weighting={ "continuity": Symbol("sdf"), "momentum_x": Symbol("sdf"), "momentum_y": Symbol("sdf"), "momentum_z": Symbol("sdf"), }, compute_sdf_derivatives=True, parameterization=geo.pr, batch_per_epoch=5000, criteria=Or(x < -1.1, x > 0.5), ) flow_domain.add_constraint(lr_interior, "lr_interior") # flow interiror high res near three fin hr_interior = PointwiseInteriorConstraint( nodes=flow_nodes, geometry=geo.geo, outvar={"continuity": 0, "momentum_x": 0, "momentum_z": 0, "momentum_y": 0}, batch_size=cfg.batch_size.InteriorLR, lambda_weighting={ "continuity": Symbol("sdf"), "momentum_x": Symbol("sdf"), "momentum_y": Symbol("sdf"), "momentum_z": Symbol("sdf"), }, compute_sdf_derivatives=True, parameterization=geo.pr, batch_per_epoch=5000, criteria=And(x > -1.1, x < 0.5), ) flow_domain.add_constraint(hr_interior, "hr_interior") # integral continuity def integral_criteria(invar, params): sdf = geo.geo.sdf(invar, params) return np.greater(sdf["sdf"], 0) integral_continuity = IntegralBoundaryConstraint( nodes=flow_nodes, geometry=geo.integral_plane, outvar={"normal_dot_vel": volumetric_flow}, batch_size=5, integral_batch_size=cfg.batch_size.IntegralContinuity, criteria=integral_criteria, lambda_weighting={"normal_dot_vel": 1.0}, parameterization={**geo.pr, **{x_pos: (-1.1, 0.1)}}, fixed_dataset=False, num_workers=4, ) flow_domain.add_constraint(integral_continuity, "integral_continuity")

Note

The addition of integral continuity planes and separate flow box for dense sampling are examples of adding more training data and user knowledge/bias of the problem to the training. This addition helps to improve the accuracy and convergence to a great extent and it is recommended wherever possible.

Setting up Thermal Multi-Phase Domain and Constraints

The contents of three_fin_thermal.py are described below.

Inlet, Outlet and Channel walls:

For the heat part, specify temperature at the inlet. All the outlet and the channel walls will have a zero gradient boundary condition which will be enforced by setting 'normal_gradient_theta_f' equal to 0. We will use 'theta_f' for defining the temperatures in fluid and 'theta_s' for defining the temperatures in solid.

Fluid and Solid Interior:

Just like the Scalar Transport: 2D Advection Diffusion tutorial, set 'advection_diffusion' equal to 0 in both, low and high resolution fluid domains. For solid interior, set 'diffusion' equal to 0.

Fluid-Solid Interface:

For the interface between fluid and solid, enforce both Neumann and Dirichlet boundary condition by setting 'diffusion_interface_dirichlet_theta_f_theta_s' and 'diffusion_interface_neumann_theta_f_theta_s' both equal to 0.

Note

The order in which you define 'theta_f' and 'theta_s' in the interface boundary condition must match the one in PDE definition DiffusionInterface to avoid any graph unroll errors. The corresponding conductivities must be also specified in the same order in the PDE definition.

Heat Source:

Apply a \(tanh\) smoothing while defining the heat source on the bottom wall of the heat sink. Smoothing out the sharp boundaries helps in training the Neural Network converge faster. The 'normal_gradient_theta_s' is set equal to grad_t in the area of source and 0 everywhere else on the bottom surface of heat sink.

The code for heat domain is shown below.

Copy
Copied!
            

geo = ThreeFin(parameterized=cfg.custom.parameterized) # params for simulation # heat params inlet_t = 293.15 / 273.15 - 1.0 grad_t = 360 / 273.15 # make flow domain thermal_domain = Domain() # inlet constraint_inlet = PointwiseBoundaryConstraint( nodes=thermal_nodes, geometry=geo.inlet, outvar={"theta_f": inlet_t}, batch_size=cfg.batch_size.Inlet, criteria=Eq(x, channel_origin[0]), lambda_weighting={"theta_f": 1.0}, # weight zero on edges parameterization=geo.pr, ) thermal_domain.add_constraint(constraint_inlet, "inlet") # outlet constraint_outlet = PointwiseBoundaryConstraint( nodes=thermal_nodes, geometry=geo.outlet, outvar={"normal_gradient_theta_f": 0}, batch_size=cfg.batch_size.Outlet, criteria=Eq(x, channel_origin[0] + channel_dim[0]), lambda_weighting={"normal_gradient_theta_f": 1.0}, # weight zero on edges parameterization=geo.pr, ) thermal_domain.add_constraint(constraint_outlet, "outlet") # channel walls insulating def wall_criteria(invar, params): sdf = geo.three_fin.sdf(invar, params) return np.less(sdf["sdf"], -1e-5) channel_walls = PointwiseBoundaryConstraint( nodes=thermal_nodes, geometry=geo.channel, outvar={"normal_gradient_theta_f": 0}, batch_size=cfg.batch_size.ChannelWalls, criteria=wall_criteria, lambda_weighting={"normal_gradient_theta_f": 1.0}, parameterization=geo.pr, ) thermal_domain.add_constraint(channel_walls, "channel_walls") # fluid solid interface def interface_criteria(invar, params): sdf = geo.channel.sdf(invar, params) return np.greater(sdf["sdf"], 0) fluid_solid_interface = PointwiseBoundaryConstraint( nodes=thermal_nodes, geometry=geo.three_fin, outvar={ "diffusion_interface_dirichlet_theta_f_theta_s": 0, "diffusion_interface_neumann_theta_f_theta_s": 0, }, batch_size=cfg.batch_size.SolidInterface, criteria=interface_criteria, parameterization=geo.pr, ) thermal_domain.add_constraint(fluid_solid_interface, "fluid_solid_interface") # heat source sharpen_tanh = 60.0 source_func_xl = (tanh(sharpen_tanh * (x - source_origin[0])) + 1.0) / 2.0 source_func_xh = ( tanh(sharpen_tanh * ((source_origin[0] + source_dim[0]) - x)) + 1.0 ) / 2.0 source_func_zl = (tanh(sharpen_tanh * (z - source_origin[2])) + 1.0) / 2.0 source_func_zh = ( tanh(sharpen_tanh * ((source_origin[2] + source_dim[2]) - z)) + 1.0 ) / 2.0 gradient_normal = ( grad_t * source_func_xl * source_func_xh * source_func_zl * source_func_zh ) heat_source = PointwiseBoundaryConstraint( nodes=thermal_nodes, geometry=geo.three_fin, outvar={"normal_gradient_theta_s": gradient_normal}, batch_size=cfg.batch_size.HeatSource, criteria=Eq(y, source_origin[1]), ) thermal_domain.add_constraint(heat_source, "heat_source") # flow interior low res away from three fin lr_flow_interior = PointwiseInteriorConstraint( nodes=thermal_nodes, geometry=geo.geo, outvar={"advection_diffusion_theta_f": 0}, batch_size=cfg.batch_size.InteriorLR, criteria=Or(x < -1.1, x > 0.5), ) thermal_domain.add_constraint(lr_flow_interior, "lr_flow_interior") # flow interiror high res near three fin hr_flow_interior = PointwiseInteriorConstraint( nodes=thermal_nodes, geometry=geo.geo, outvar={"advection_diffusion_theta_f": 0}, batch_size=cfg.batch_size.InteriorHR, criteria=And(x > -1.1, x < 0.5), ) thermal_domain.add_constraint(hr_flow_interior, "hr_flow_interior") # solid interior solid_interior = PointwiseInteriorConstraint( nodes=thermal_nodes, geometry=geo.three_fin, outvar={"diffusion_theta_s": 0}, batch_size=cfg.batch_size.SolidInterior, lambda_weighting={"diffusion_theta_s": 100.0}, ) thermal_domain.add_constraint(solid_interior, "solid_interior")

Adding Validators and Monitors

In this tutorial you will monitors for pressure drops during the flow field simulation and monitors for peak temperature reached in the source chip during the heat simulation.

Similarly, respective validation data are added to the flow and heat scripts. Only flow monitors and validators are shown for brevity.

Monitors and validators in flow script:

Copy
Copied!
            

# flow data file_path = "../openfoam/" if os.path.exists(to_absolute_path(file_path)): mapping = { "Points:0": "x", "Points:1": "y", "Points:2": "z", "U:0": "u", "U:1": "v", "U:2": "w", "p_rgh": "p", } if cfg.custom.turbulent: openfoam_var = csv_to_dict( to_absolute_path("openfoam/threeFin_extend_zeroEq_re500_fluid.csv"), mapping, ) else: openfoam_var = csv_to_dict( to_absolute_path("openfoam/threeFin_extend_fluid0.csv"), mapping ) openfoam_var = {key: value[0::4] for key, value in openfoam_var.items()} openfoam_var["x"] = openfoam_var["x"] + channel_origin[0] openfoam_var["y"] = openfoam_var["y"] + channel_origin[1] openfoam_var["z"] = openfoam_var["z"] + channel_origin[2] openfoam_var.update({"fin_height_m": np.full_like(openfoam_var["x"], 0.4)}) openfoam_var.update({"fin_height_s": np.full_like(openfoam_var["x"], 0.4)}) openfoam_var.update({"fin_thickness_m": np.full_like(openfoam_var["x"], 0.1)}) openfoam_var.update({"fin_thickness_s": np.full_like(openfoam_var["x"], 0.1)}) openfoam_var.update({"fin_length_m": np.full_like(openfoam_var["x"], 1.0)}) openfoam_var.update({"fin_length_s": np.full_like(openfoam_var["x"], 1.0)}) openfoam_invar_numpy = { key: value for key, value in openfoam_var.items() if key in [ "x", "y", "z", "fin_height_m", "fin_height_s", "fin_thickness_m", "fin_thickness_s", "fin_length_m", "fin_length_s", ] } openfoam_outvar_numpy = { key: value for key, value in openfoam_var.items() if key in ["u", "v", "w", "p"] } openfoam_validator = PointwiseValidator( nodes=flow_nodes, invar=openfoam_invar_numpy, true_outvar=openfoam_outvar_numpy, ) flow_domain.add_validator(openfoam_validator) else: warnings.warn( f"Directory{file_path}does not exist. Will skip adding validators. Please download the additional files from NGC https://catalog.ngc.nvidia.com/orgs/nvidia/teams/modulus/resources/modulus_sym_examples_supplemental_materials" ) # add pressure monitor invar_inlet_pressure = geo.integral_plane.sample_boundary( 1024, parameterization={**fixed_param_ranges, **{x_pos: -2}} ) pressure_monitor = PointwiseMonitor( invar_inlet_pressure, output_names=["p"], metrics={"inlet_pressure": lambda var: torch.mean(var["p"])}, nodes=flow_nodes, ) flow_domain.add_monitor(pressure_monitor) # add pressure drop for design optimization # run only for parameterized cases and in eval mode if cfg.custom.parameterized and cfg.run_mode == "eval": # define candidate designs num_samples = cfg.custom.num_samples inference_param_tuple = itertools.product( np.linspace(*height_m_range, num_samples), np.linspace(*height_s_range, num_samples), np.linspace(*length_m_range, num_samples), np.linspace(*length_s_range, num_samples), np.linspace(*thickness_m_range, num_samples), np.linspace(*thickness_s_range, num_samples), ) for ( HS_height_m_, HS_height_s_, HS_length_m_, HS_length_s_, HS_thickness_m_, HS_thickness_s_, ) in inference_param_tuple: HS_height_m = float(HS_height_m_) HS_height_s = float(HS_height_s_) HS_length_m = float(HS_length_m_) HS_length_s = float(HS_length_s_) HS_thickness_m = float(HS_thickness_m_) HS_thickness_s = float(HS_thickness_s_) specific_param_ranges = { fin_height_m: HS_height_m, fin_height_s: HS_height_s, fin_length_m: HS_length_m, fin_length_s: HS_length_s, fin_thickness_m: HS_thickness_m, fin_thickness_s: HS_thickness_s, } # add metrics for front pressure plane_param_ranges = { **specific_param_ranges, **{x_pos: heat_sink_base_origin[0] - heat_sink_base_dim[0]}, } metric = ( "front_pressure" + str(HS_height_m) + "_" + str(HS_height_s) + "_" + str(HS_length_m) + "_" + str(HS_length_s) + "_" + str(HS_thickness_m) + "_" + str(HS_thickness_s) ) invar_pressure = geo.integral_plane.sample_boundary( 1024, parameterization=plane_param_ranges, ) front_pressure_monitor = PointwiseMonitor( invar_pressure, output_names=["p"], metrics={metric: lambda var: torch.mean(var["p"])}, nodes=flow_nodes, ) flow_domain.add_monitor(front_pressure_monitor) # add metrics for back pressure plane_param_ranges = { **specific_param_ranges, **{x_pos: heat_sink_base_origin[0] + 2 * heat_sink_base_dim[0]}, } metric = ( "back_pressure" + str(HS_height_m) + "_" + str(HS_height_s) + "_" + str(HS_length_m) + "_" + str(HS_length_s) + "_" + str(HS_thickness_m) + "_" + str(HS_thickness_s) ) invar_pressure = geo.integral_plane.sample_boundary( 1024, parameterization=plane_param_ranges, ) back_pressure_monitor = PointwiseMonitor( invar_pressure, output_names=["p"], metrics={metric: lambda var: torch.mean(var["p"])}, nodes=flow_nodes, ) flow_domain.add_monitor(back_pressure_monitor)

Once both the flow and heat scripts are defined, run the three_fin_flow.py first to solve for the flow field. Once a desired level of convergence is achieved you can run three_fin_thermal.py to solve for heat.

The table and figures below show the results of Pressure drop and Peak temperatures obtained from the Modulus Sym and compare it with the results from OpenFOAM solver.

Table 6 Comparisons of Results with OpenFOAM

Modulus Sym OpenFOAM
Pressure Drop \((Pa)\) 7.51 7.49
Peak Temperature \((^{\circ} C)\) 78.35 78.05
CHTXSlice.png

Fig. 143 Left: Modulus Sym. Center: OpenFOAM. Right: Difference. Top: Temperature distribution in Fluid. Bottom: Temperature distribution in Solid (Temperature scales in C)

CHTZSlice.png

Fig. 144 Left: Modulus Sym. Center: OpenFOAM. Right: Difference. (Temperature scales in C)

Plotting gradient quantities: Wall Velocity Gradients

In a variety of applications, it is desirable to plot the gradients of some quantities inside the domain. One such example relevant to fluid flows is the wall velocity gradients and wall shear stresses. These quantities are often plotted to compute frictional forces, etc. You can visualize such quantities in Modulus Sym by outputting the \(x\), \(y\) and \(z\) derivatives of the desired variables using an PointwiseInferencer.

Copy
Copied!
            

... # add inferencer inferencer = PointwiseInferencer( geo.sample_boundary(4000, parameterization=pr), ["u__x", "u__y", "u__z", "v__x", "v__y", "v__z", "w__x", "w__y", "w__z"], nodes=flow_nodes, ) flow_domain.add_inferencer(inferencer, "inf_data")

You can then post-process these quantities based on your choice to visualize the desired variables. Paraview’s Calculator Filter was used for the plot shown below. The wall velocity gradients comparison between OpenFOAM and Modulus Sym is shown in Fig. 145.

wall_shear_stress_3Fin.png

Fig. 145 Comparison of magnitude of wall velocity gradients. Left: Modulus Sym. Right: OpenFOAM

© Copyright 2023, NVIDIA Modulus Team. Last updated on Oct 17, 2023.