dragonfly_sim.core.meshwarping ============================== .. py:module:: dragonfly_sim.core.meshwarping Attributes ---------- .. autoapisummary:: dragonfly_sim.core.meshwarping._NO_PITCH Classes ------- .. autoapisummary:: dragonfly_sim.core.meshwarping.IDWarp_jax Module Contents --------------- .. py:class:: IDWarp_jax(mesh_obj, wall_meshtag_val: int = 2, anchor_meshtag_vals=(3, 4), extrusion_width: float = None, LdefFact: float = 1.0, err_tol: float = 1e-06, **idwarp_kwargs) Bases: :py:obj:`csdl_alpha.CustomExplicitOperation` IDW mesh deformation via the ``idwarp-jax`` package's kd-tree warper. Maps the replicated ``(M_global, dim)`` wall-node motions (``evaluate(bdry_motions, cp_motion_inputs)``, usually ``shape_parameterization.WallFFD.wall_displacement``) to rank-local volume node motions, in geometry-node order. Because the map is closed form rather than a PDE solve, this is a ``CustomExplicitOperation`` -- CSDL calls ``compute`` and ``compute_jacvec_product``. Everything that is a property of the mesh rather than of the warp lives on the ``Mesh`` (mesh_manager): the wall and anchor node sets and their global numbering (``Mesh.boundary_node_set``), the tagged facets and their orientation, applying/resetting node motions, mesh quality, and the output files. The wall numbering here is the mesh's wall node set's. The warp itself is ``idwarp_jax.build_volume_pts_func``, a kd-tree IDWarp that batches its forward and reverse passes internally, so its memory stays bounded without any chunking here. ``err_tol`` is its pruning tolerance, relative to the IDW denominator; ``err_tol=0`` disables pruning and gives the exact dense IDW sum. This class only turns the mesh's wall and anchor node sets into the package's surface and moves arrays between CSDL and it. :Parameters: **mesh_obj** : mesh_manager.Mesh, with its boundary facets tagged. Its ``output_suffix`` (Mesh.configure_output) names this class's memory logs. **wall_meshtag_val** : tag of the moving (design) surface. .. **anchor_meshtag_vals** : tags of the boundaries held at zero displacement. Pass ``()`` to disable anchoring -- but read the note above first. **extrusion_width** : 2D only; width of the extruded boundary ribbon. ``None`` derives it from the smallest boundary edge. See ``_resolve_extrusion_width`` for why the default is what it is -- the choice trades IDW distance fidelity against the conditioning of Newell's method, and neither limit is safe. **LdefFact** : IDWarp's dimensionless deformation length: ``Ldef = LdefFact * Ldef0``, with ``Ldef0`` the largest distance from the wall nodes' centroid to a wall node. The package takes ``Ldef0`` over every prescribed node, anchors included -- the farfield radius -- so the factor is rescaled before it is passed on. **err_tol** : kd-tree pruning tolerance, see above. Measured against ``err_tol=0`` on the 2D NACA0012 mesh: 1e-6 gives 1.2e-3 relative motion error at half the cost; 1e-10 prunes almost nothing (cost of the exact sum). On the 3D wing mesh 1e-6 keeps ~25% of the dense interactions, with a ~47 GiB host route cache summed over all ranks. **\*\*idwarp_kwargs** : forwarded to ``build_volume_pts_func``, over these defaults: - ``normal_eps = warp_eps = 1e-30``. The package defaults (1e-6) are dimensional: ``normal_eps`` is added to |2A|^2 of every face, which is ~1e-14 for a 2D ribbon quad and ~1e-10 for a small 3D wall face, and ``warp_eps`` floors every IDW distance at 1e-3. Either would wreck the warp. - ``symmetry_tolerance``: 0 in 2D, where no ribbon node may be pinned to y = 0; 1e-6 in 3D, the tolerance the model tags symmetry facets with. - ``target_batch_interactions = 1_000_000`` (package: 6e6). Each rank holds one batch of volume-point x source interactions in flight, and the reverse pass's working arrays scale with it: on the 3D wing mesh at err_tol=1e-6 they add ~0.9 GiB/rank at 1e6 vs ~2.4 GiB/rank at 6e6, with identical results and no measurable slowdown. - ``progress = print_timings = False``; this class reports its own timings. Everything else (``zeroCornerRotations``, ``cornerAngle``, ``useRotations``, ``rotation_eps``, ``bucket_size``, ``route_cache``, ...) is the package's default. .. !! processed by numpydoc !! .. py:method:: _assemble_motions(wall_motions, interior_3d) (N_local, dim) mesh-node motions in geometry-node order. Owned wall nodes take their rows of the replicated wall array, interior nodes the warp output; anchor nodes stay zero. .. !! processed by numpydoc !! .. py:method:: _assert_planar(motion_3d) In 2D the out-of-plane (y) motion must vanish identically. It does so exactly, per surface node: both nodal normals are in-plane, so the rotation is about y, so the y component of M (x_v - Xs0_i) + Xs_i - x_v is (+-w/2) + (-+w/2) = 0. A nonzero value here means the ribbon construction is wrong, not that a tolerance is tight. .. !! processed by numpydoc !! .. py:method:: _build_global_surface_topology() Assemble the replicated prescribed surface as the package's mesh dict. ``self._mesh_dict`` holds ``points`` (the surface rows, ``self._Xs0``) and ``faces`` (every face with type 1, so wall and anchor faces are all prescribed). Every rank ends up with byte-identical arrays, because every rank has to evaluate the IDW sum against the whole surface. .. !! processed by numpydoc !! .. py:method:: _build_warper() Build this rank's idwarp_jax warper over its own interior nodes. The volume points are the interior nodes only: wall motions are prescribed and anchors are held at zero, so neither needs warping. A rank that owns no interior node gets one dummy point, because the package cannot preprocess an empty volume. .. !! processed by numpydoc !! .. py:method:: _check_surface_topology(n_flipped) Fail loudly if the ranks assembled different surfaces. Identical on every rank, byte for byte: otherwise ranks evaluate different surfaces and the gradients they reduce are incoherent. A content hash rather than a sum, so a permutation cannot slip through. .. !! processed by numpydoc !! .. py:method:: _expand(wall_vals) (M_global, dim) wall values -> (n_surface_rows, 3), in every layer. The anchor rows stay zero. .. !! processed by numpydoc !! .. py:method:: _fold(rows) Adjoint of ``_expand``: (n_surface_rows, 3) -> (M_global, dim). The layers are summed because the forward writes the same wall motion into every layer. The anchor rows are dropped: they are sensitivities with respect to a constant. .. !! processed by numpydoc !! .. py:method:: _log_memory(note) .. py:method:: _project(wall_vals) Zero the y component of wall nodes on the symmetry plane. Takes and returns a full (M_global, dim) array. The package pins those nodes to y = 0 before warping, so this is the wall motion the warp actually assumed. A diagonal projection, hence its own adjoint. Identity in 2D, where no node is pinned. .. !! processed by numpydoc !! .. py:method:: _resolve_extrusion_width(node_coords, local_faces) Pick the 2D ribbon width: a fraction of the shortest boundary edge. Two effects pull in opposite directions, and the factor below is where they balance. Smaller w is better for the IDW geometry: w enters the distance as sqrt(d_xy^2 + w^2/4), so a width comparable to the near-wall spacing would blunt exactly the weight concentration that makes the warp follow the surface. At w = 0.1 * h_min the nearest volume node sees its distance inflated by 0.125%, which is nothing. Larger w is better for conditioning. Newell's method in idwarp_jax's normal.py sums cross products of *absolute* coordinates, whose magnitudes are set by the farfield, and those terms have to cancel down to twice the face area -- which for a ribbon quad is only edge_length * w. The relative error in the resulting normal therefore goes as 1/w. Measured on the 2D mesh, as the error in reproducing a rigid surface translation: w = 1e-5 -> 6.5e-08 w = 1e-2 -> 6.7e-11 w = 1e-4 -> 6.7e-09 w = 1e-1 -> 6.7e-12 w = 1e-3 -> 6.7e-10 w = 1e+0 -> 6.6e-13 0.1 * h_min puts that error around 1e-9 for an O(1) displacement, four orders below the smallest cell, at a distance distortion of 0.1%. (3D is unaffected: real surface faces have areas many orders larger than an extruded ribbon's, so nothing cancels catastrophically.) The minimum is taken over every prescribed boundary edge, so in practice it is set by the wall, where the edges are finest. .. !! processed by numpydoc !! .. py:method:: compute(input_vals, output_vals) Forward warp. No collectives: every rank already has the whole surface. .. !! processed by numpydoc !! .. py:method:: compute_jacvec_product(input_vals, output_vals, d_inputs, d_outputs, mode) Matrix-free VJP. Only reverse mode is supported. Each rank seeds only the rows it owns -- its own wall nodes for the identity block, its own interior nodes for the IDW block -- so the per-rank gradients are disjoint contributions to one (M_global, dim) array and summing them is exact. .. !! processed by numpydoc !! .. py:method:: evaluate(bdry_motions: csdl_alpha.Variable, cp_motion_inputs: csdl_alpha.Variable) Register the operation. ``cp_motion_inputs`` carries no derivative -- it is declared only to keep every rank's reverse chain attached to the design variables, including ranks that own no wall nodes. .. !! processed by numpydoc !! .. py:attribute:: LdefFact .. py:attribute:: M_global .. py:attribute:: N_local .. py:attribute:: _cols :value: [0, 2] .. py:attribute:: _extrusion_width_arg :value: None .. py:attribute:: _idwarp_kwargs .. py:attribute:: _interior_local_idx .. py:attribute:: _n_interior .. py:attribute:: _n_surface_nodes .. py:attribute:: anchor_meshtag_vals :value: (3, 4) .. py:attribute:: anchors .. py:attribute:: dimensions .. py:attribute:: err_tol .. py:attribute:: mesh .. py:attribute:: ordered_callbacks :value: True .. py:attribute:: wall .. py:attribute:: wall_meshtag_val :value: 2 .. py:data:: _NO_PITCH