|
| 1 | +--- |
| 2 | +jupytext: |
| 3 | + text_representation: |
| 4 | + extension: .mystnb |
| 5 | + format_name: myst |
| 6 | + format_version: 0.13 |
| 7 | + jupytext_version: 1.14.1 |
| 8 | +kernelspec: |
| 9 | + display_name: Python 3 (ipykernel) |
| 10 | + language: python |
| 11 | + name: python3 |
| 12 | +--- |
| 13 | +# Applied: Develop an external aerodynamic simulation model for Fluent analysis |
| 14 | + |
| 15 | +The Ahmed body is a simplified car model used to study airflow around vehicles. The wake (turbulent flow behind the body) |
| 16 | + depends on the slant angle: |
| 17 | + |
| 18 | + - Less than 12 degrees: The airflow stays attached to the slant, creating low drag and a mostly two-dimensional flow. |
| 19 | + - 12 to 30 degrees: The flow becomes three-dimensional with strong c-pillar vortices, peaking at 30 degrees. This increases drag due to low-pressure areas on the rear surfaces. |
| 20 | + - More than 30 degrees: The flow fully separates from the slant, reducing drag and weakening the c-pillar vortices. |
| 21 | + |
| 22 | +This example creates an Ahmed body with a slant angle of 20 degrees. It consists of these steps: |
| 23 | + 1. Launch PyAnsys Geometry and define the default units. |
| 24 | + 2. Create sketches for the Ahmed body, enclosure, and BOI (Body of Influence). |
| 25 | + 3. Generate solid bodies from the sketches. |
| 26 | + 4. Perform Boolean operations for region extraction. |
| 27 | + 5. Group faces and define a named selection. |
| 28 | + 6. Export model as a CAD file. |
| 29 | + 7. Close session. |
| 30 | + |
| 31 | +### Define function for sorting planar face pairs along any axis |
| 32 | + |
| 33 | +This function is used to sort the planar faces along any of the coordinate axis. This is used primarily for sorting the faces to define |
| 34 | +the named selections. |
| 35 | + |
| 36 | +```{code-cell} ipython3 |
| 37 | +def face_identifier(faces, axis): |
| 38 | + """ |
| 39 | + Sort a pair of planar faces based on their positions along the specified coordinate axis. |
| 40 | + |
| 41 | + Args: |
| 42 | + faces : List[IFace, IFace] |
| 43 | + List of planar face pairs. |
| 44 | + |
| 45 | + axis : (string) |
| 46 | + Axis to sort the face pair on. Options are "x", "y", or "z". |
| 47 | + |
| 48 | + Returns: |
| 49 | + IFace, IFace |
| 50 | + - IFace: Face with the centroid positioned behind the other face along the specified axis. |
| 51 | + - IFace: Face with the centroid positioned ahead of the other face along the specified axis. |
| 52 | + |
| 53 | + """ |
| 54 | + min_face = "" |
| 55 | + max_face = "" |
| 56 | + if axis == "x": |
| 57 | + position = 0 |
| 58 | + elif axis == "y": |
| 59 | + position = 1 |
| 60 | + else: |
| 61 | + position = 2 |
| 62 | + min_face_cor_val = faces[0].point(0.5, 0.5)[position] |
| 63 | + min_face = faces[0] |
| 64 | + max_face_cor_val = faces[0].point(0.5, 0.5)[position] |
| 65 | + max_face = faces[0] |
| 66 | + for face in faces[1:]: |
| 67 | + if face.point(0.5, 0.5)[position] < min_face_cor_val: |
| 68 | + min_face_cor_val = face.point(0.5, 0.5)[position] |
| 69 | + min_face = face |
| 70 | + continue |
| 71 | + elif face.point(0.5, 0.5)[position] > max_face_cor_val: |
| 72 | + max_face_cor_val = face.point(0.5, 0.5)[position] |
| 73 | + max_face = face |
| 74 | + return min_face, max_face |
| 75 | + |
| 76 | +``` |
| 77 | + |
| 78 | +### Define function for calculating the vertical and horizontal distances based on the slant angle |
| 79 | + |
| 80 | + |
| 81 | +```{code-cell} ipython3 |
| 82 | +def distance_calculator(hypo, slant_angle): |
| 83 | + """ |
| 84 | + Calculate the horizontal and vertical distances based on the hypotenuse and slant angle. |
| 85 | + |
| 86 | + Args: |
| 87 | + hypo : int |
| 88 | + Length of the hypotenuse in millimeters. |
| 89 | + |
| 90 | + slant_angle : int |
| 91 | + Slant angle in degrees. |
| 92 | + |
| 93 | + Returns: |
| 94 | + slant_x (float): Horizontal distance calculated using the sine of the slant angle. |
| 95 | + slant_y (float): Vertical distance calculated using the cosine of the slant angle. |
| 96 | + |
| 97 | + """ |
| 98 | + slant_x = hypo * math.cos(math.radians(slant_angle)) |
| 99 | + slant_y = hypo * math.sin(math.radians(slant_angle)) |
| 100 | + return slant_y, slant_x |
| 101 | + |
| 102 | +``` |
| 103 | + |
| 104 | +### Launch PyAnsys geometry and define the default units |
| 105 | +Before you start creating the Ahmed body, you must import the necessary modules to create the model using PyAnsys Geometry. |
| 106 | +It's also a good practice to define the units before initiating the development of the sketch. |
| 107 | + |
| 108 | +```{code-cell} ipython3 |
| 109 | +from ansys.geometry.core import launch_modeler |
| 110 | +from ansys.geometry.core.sketch import Sketch |
| 111 | +from ansys.geometry.core.math import ( |
| 112 | + Point2D, |
| 113 | + Plane, |
| 114 | + Point3D, |
| 115 | + UNITVECTOR3D_X, |
| 116 | + UNITVECTOR3D_Y, |
| 117 | + UNITVECTOR3D_Z, |
| 118 | +) |
| 119 | +from ansys.geometry.core.misc import UNITS, DEFAULT_UNITS |
| 120 | + |
| 121 | +from ansys.geometry.core.plotting import GeometryPlotter |
| 122 | +import math |
| 123 | +import os |
| 124 | + |
| 125 | +modeler = launch_modeler() |
| 126 | +DEFAULT_UNITS.LENGTH = UNITS.mm |
| 127 | +DEFAULT_UNITS.angle = UNITS.degrees |
| 128 | + |
| 129 | +``` |
| 130 | + |
| 131 | +### Create sketches for the Ahmed body, enclosure, and BOI |
| 132 | + |
| 133 | +Define the appropriate sketch planes parallel to the y-z and x-z planes, passing through the origin (namely `sketch_plane` and `sketch_plane_2` respectively). |
| 134 | + |
| 135 | +#### Define the sketch planes |
| 136 | + |
| 137 | +{width=500px, align=center} |
| 138 | + |
| 139 | +```{code-cell} ipython3 |
| 140 | + |
| 141 | +# Define sketch plane on the y-z plane passing through the origin |
| 142 | +sketch_plane = Plane( |
| 143 | + origin=Point3D([0, 0, 0]), |
| 144 | + direction_x=UNITVECTOR3D_Y, |
| 145 | + direction_y=UNITVECTOR3D_Z, |
| 146 | + ) |
| 147 | + |
| 148 | +# Define sketch plane on the x-z plane passing through the origin |
| 149 | +sketch_plane_2 = Plane( |
| 150 | + origin=Point3D([0, 0, 0]), |
| 151 | + direction_x=UNITVECTOR3D_X, |
| 152 | + direction_y=UNITVECTOR3D_Z, |
| 153 | +) |
| 154 | +``` |
| 155 | +#### Define the Ahmed body |
| 156 | + |
| 157 | +{width=500px, align=center} |
| 158 | + |
| 159 | +```{code-cell} ipython3 |
| 160 | +# Calculate the horizontal and vertical distance based on slant angle of 20 degrees |
| 161 | +slant_y, slant_x = distance_calculator(hypo=222, slant_angle=20) |
| 162 | + |
| 163 | +# Define sketch for the Ahmed body |
| 164 | +ahmed_body_sketch = Sketch(sketch_plane) |
| 165 | +ahmed_body_sketch.segment( |
| 166 | + start=Point2D([50, 0]), end=Point2D([338 - slant_y, 0]) |
| 167 | +).segment_to_point(Point2D([338, slant_x])).segment_to_point( |
| 168 | + Point2D([338, 944]) |
| 169 | +).arc_to_point( |
| 170 | + end=Point2D([238, 1044]), center=Point2D([238, 944]), clockwise=False |
| 171 | +).segment_to_point( |
| 172 | + Point2D([150, 1044]) |
| 173 | +).arc_to_point( |
| 174 | + end=Point2D([50, 944]), center=Point2D([150, 944]), clockwise=False |
| 175 | +).segment_to_point( |
| 176 | + end=Point2D([50, 0]) |
| 177 | +) |
| 178 | +ahmed_body_sketch.plot() |
| 179 | +``` |
| 180 | + |
| 181 | +#### Define the enclosure |
| 182 | + |
| 183 | +{width=500px, align=center} |
| 184 | + |
| 185 | + |
| 186 | +```{code-cell} ipython3 |
| 187 | +# Define sketch for enclosure |
| 188 | +enclosure_sketch = Sketch(plane=sketch_plane) |
| 189 | +enclosure_sketch.box(center=Point2D([1014 / 2, 0]), height=4176, width=1014) |
| 190 | +enclosure_sketch.plot() |
| 191 | + |
| 192 | +# Define sketch for mounting 1 |
| 193 | +mount_sketch_1 = Sketch(sketch_plane_2) |
| 194 | +mount_sketch_1.circle(center=Point2D([163.5, 792]), radius=15) |
| 195 | + |
| 196 | +# Define sketch for mounting 2 |
| 197 | +mount_sketch_2 = Sketch(sketch_plane_2) |
| 198 | +mount_sketch_2.circle(center=Point2D([163.5, 322]), radius=15) |
| 199 | + |
| 200 | +# Define sketch for the fillet |
| 201 | +ahmed_body_fillet_sketch = Sketch(sketch_plane_2) |
| 202 | +ahmed_body_fillet_sketch.segment( |
| 203 | + start=Point2D([194.5, 944]), end=Point2D([194.5, 1044]) |
| 204 | +).segment_to_point(Point2D([94.5, 1044])).arc_to_point( |
| 205 | + Point2D([194.5, 944]), center=Point2D([94.5, 944]), clockwise=True |
| 206 | +) |
| 207 | +``` |
| 208 | + |
| 209 | +#### Define the BOI |
| 210 | + |
| 211 | +{width=500px, align=center} |
| 212 | + |
| 213 | +```{code-cell} ipython3 |
| 214 | +# Define sketch for BOI |
| 215 | +boi_sketch = Sketch(sketch_plane_2) |
| 216 | +boi_sketch.box(center=Point2D([0, -325]), width=1000, height=1450) |
| 217 | +boi_sketch.plot() |
| 218 | +``` |
| 219 | + |
| 220 | +### Generate solid bodies from the sketches |
| 221 | +From the 2D sketches, generate 3D models by extruding the sketch. First create the design body (namely `ahmed_model`, which is the root part. A component named `Component1` is |
| 222 | +created under the root part. All the bodies generated as a part of sketch extrusion would be placed within `Component1`. |
| 223 | + |
| 224 | +```{code-cell} ipython3 |
| 225 | + |
| 226 | +# Create design object |
| 227 | +design = modeler.create_design("ahmed_model") |
| 228 | + |
| 229 | +# Create component |
| 230 | +component_1 = design.add_component("Component1") |
| 231 | + |
| 232 | +# Create body `ahmed_body` by extrusion |
| 233 | +ahmed_body = component_1.extrude_sketch( |
| 234 | + "ahmed_body", sketch=ahmed_body_sketch, distance=194.5 |
| 235 | +) |
| 236 | + |
| 237 | +# Create body `ahmed_body_fillet` by cut extrusion |
| 238 | +ahmed_body_fillet = component_1.extrude_sketch( |
| 239 | + "ahmed_body_fillet", sketch=ahmed_body_fillet_sketch, distance=-500, cut=True |
| 240 | +) |
| 241 | + |
| 242 | +# Create body `enclosure` by extrusion |
| 243 | +enclosure = component_1.extrude_sketch( |
| 244 | + "Solid1", sketch=enclosure_sketch, distance=1167 |
| 245 | +) |
| 246 | + |
| 247 | +# Create body `mounting_1` by extrusion |
| 248 | +mount_1 = component_1.extrude_sketch( |
| 249 | + "mount_1", sketch=mount_sketch_1, distance=-100 |
| 250 | +) |
| 251 | + |
| 252 | +# Create body `mounting_2` by extrusion |
| 253 | +mount_2 = component_1.extrude_sketch( |
| 254 | + "mount_2", sketch=mount_sketch_2, distance=-100 |
| 255 | +) |
| 256 | + |
| 257 | +# Create body `boi` by extrusion |
| 258 | +# The direction is negative since the sketch is created in X-Z plane, resulting in the direction of normal to be parallel to the -Y axis. |
| 259 | +boi_body = design.extrude_sketch( |
| 260 | + "boi", sketch=boi_sketch, distance=500, direction="-" |
| 261 | +) |
| 262 | +``` |
| 263 | + |
| 264 | +### Perform Boolean operations for region extraction |
| 265 | + |
| 266 | +```{code-cell} ipython3 |
| 267 | +enclosure.subtract([ahmed_body, mount_1, mount_2]) |
| 268 | +enclosure.plot() |
| 269 | +``` |
| 270 | + |
| 271 | +### Group faces and define named selection |
| 272 | + |
| 273 | + |
| 274 | +```{code-cell} ipython3 |
| 275 | +plane_surface = [] |
| 276 | +cylindrical_surface = [] |
| 277 | + |
| 278 | +# Group faces of enclosure based on topology |
| 279 | +for face in enclosure.faces: |
| 280 | + if face.surface_type.name == "SURFACETYPE_PLANE": |
| 281 | + plane_surface.append(face) |
| 282 | + elif face.surface_type.name == "SURFACETYPE_CYLINDER": |
| 283 | + cylindrical_surface.append(face) |
| 284 | + |
| 285 | +wall_mount = [] |
| 286 | + |
| 287 | +# Identify faces associated with mounting |
| 288 | +for cyl_face in cylindrical_surface: |
| 289 | + if cyl_face.point(0, 0.5)[1] < 0.050: |
| 290 | + wall_mount.append(cyl_face) |
| 291 | + |
| 292 | +# Identify faces associated with enclosure extremes |
| 293 | +outlet_face, inlet_face = face_identifier(faces=plane_surface, axis="z") |
| 294 | +symmetry_face, symmetry_x_pos = face_identifier(faces=plane_surface, axis="x") |
| 295 | +ground, top = face_identifier(faces=plane_surface, axis="y") |
| 296 | + |
| 297 | + |
| 298 | +# Create named selection |
| 299 | +design.create_named_selection("wall_mount", faces=wall_mount) |
| 300 | +design.create_named_selection("inlet", faces=[inlet_face]) |
| 301 | +design.create_named_selection("outlet", faces=[outlet_face]) |
| 302 | +design.create_named_selection("wall_ground", faces=[ground]) |
| 303 | +design.create_named_selection("symmetry_top", faces=[top]) |
| 304 | +design.create_named_selection("symmetry_center_plane", faces=[symmetry_face]) |
| 305 | +design.create_named_selection("symmetry_x_pos", faces=[symmetry_x_pos]) |
| 306 | +design.create_named_selection( |
| 307 | + "ahmed_body_20_0degree_boi_half-boi", faces=boi_body.faces |
| 308 | +) |
| 309 | + |
| 310 | +body_planar_surface = list( |
| 311 | + set(plane_surface) |
| 312 | + - set([outlet_face, inlet_face, symmetry_face, symmetry_x_pos, ground, top]) |
| 313 | +) |
| 314 | +body_circular_surface = list(set(cylindrical_surface) - set(wall_mount)) |
| 315 | + |
| 316 | +rear_surface, front_surface = face_identifier(faces=body_planar_surface, axis="z") |
| 317 | +bottom_surface, top_surface = face_identifier(faces=body_planar_surface, axis="y") |
| 318 | +side_suface, symmetry_surface = face_identifier(faces=body_planar_surface, axis="x") |
| 319 | + |
| 320 | +body_circular_surface.append(front_surface) |
| 321 | + |
| 322 | +design.create_named_selection("wall_ahmed_body_front", faces=body_circular_surface) |
| 323 | +design.create_named_selection( |
| 324 | + "wall_ahmed_body_main", |
| 325 | + faces=[symmetry_surface, bottom_surface, top_surface], |
| 326 | +) |
| 327 | + |
| 328 | +# Identify the face that forms a 20-degree angle with the y-axis. |
| 329 | +for face in body_planar_surface: |
| 330 | + if round(math.degrees(math.acos(abs(face.normal().y)))) == 20: |
| 331 | + hypo_face = face |
| 332 | +design.create_named_selection( |
| 333 | + "wall_ahmed_body_rear", faces=[hypo_face, rear_surface] |
| 334 | +) |
| 335 | +``` |
| 336 | + |
| 337 | +### Export model as a PMDB file |
| 338 | + |
| 339 | +Export the geometry into a Fluent-compatible format. The following code exports the geometry into a PMDB file, which retains the named selections. |
| 340 | + |
| 341 | +```{code-cell} ipython3 |
| 342 | + |
| 343 | +# Save design |
| 344 | +file = design.export_to_pmdb() |
| 345 | +print(f"Design saved to {file}") |
| 346 | +``` |
| 347 | +You can import the exported PMDB file into Fluent to set up the mesh and perform the simulation. |
| 348 | +For an example of how to set up the mesh and boundary conditions in Fluent, see the [Ahmed Body External Aerodynamics Simulation](https://examples.fluent.docs.pyansys.com/version/dev/examples/00-released_examples/00-ahmed_body_workflow.html) example in the Fluent documentation. |
| 349 | + |
| 350 | +### Close session |
| 351 | + |
| 352 | +```{code-cell} ipython3 |
| 353 | +modeler.close() |
| 354 | +``` |
| 355 | + |
| 356 | +### References |
| 357 | +[1] S.R. Ahmed, G. Ramm, Some Salient Features of the Time-Averaged Ground Vehicle Wake,SAE-Paper 840300,1984 |
0 commit comments