Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ Before contributing, make sure to read through the [Contributing Guidelines](CON
* Hang Haotian ([haotianh9](https://github.com/haotianh9))
* Monisha Sikka ([20086080](https://github.com/20086080))
* Cameron Hendrikse ([MonoChromatical](https://github.com/MonoChromatical))
* Abdullah Imran ([codexabdullah](https://github.com/codexabdullah))

### Supporters

Expand Down
48 changes: 36 additions & 12 deletions pterasoftware/geometry/_meshing.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ def mesh_wing(wing: wing_mod.Wing) -> None:
]
)

# Iterate through the Panels and populate their local position attributes.
Flpp_G_Cg = Fipp_G_Cg
Frpp_G_Cg = Fopp_G_Cg
Blpp_G_Cg = Bipp_G_Cg
Expand Down Expand Up @@ -266,25 +267,45 @@ def mesh_wing(wing: wing_mod.Wing) -> None:
]
)

# TODO: Understand how this block of code works. We're reflecting about a
# plane defined by a normal vector in geometry axes and a point in
# geometry axes relative to the CG. But that's okay I guess and we end
# up with reflected points in wing axes relative to the leading edge
# root point? Also then we perform an passive transformation to find
# them in geometry axes relative to the CG? Why do we even need to
# reflect them if we are staying in wing axes? If we take a reflected a
# non reflected Wing that are otherwise identical, they should have the
# same coordinates in their respective wing axes relative to their
# respective leading edge root points. So why is the active
# transformation necessary?
# DOCUMENT: Document the logic in this block of code.
# --------------------------------------------------------------------------
# Mirroring this wing section across the airplane's plane of symmetry.
#
# Why an ACTIVE transformation is required here, and not
# just a passive change of reference frame:
#
# Expressed in their own local wing axes, relative to their own leading-edge
# root points, a wing and its mirror image DO have identical coordinates --
# that is exactly what "mirror image" means, and it's why a symmetric wing
# only needs to be defined once with a `symmetric` flag.
#
# But the two wings do not exist in isolation; they must be placed together
# into one shared geometry frame relative to the airplane's CG so the solver

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Say "one shared geometry axis system" instead of "one shared geometry frame." This is a nit pick, but to avoid confusion, Ptera Software tries to be very specific about the language used when referring to axes, points, and frames. Quoting from docs/AXES_POINTS_AND_FRAMES.md:

An axis system, also called “axes,” contains information about three directions. In Ptera Software, all axes are Cartesian, meaning their three basis directions are linear, not angular.

Reference points, also called “points,” contain information about the location of a particular point in space.

Lastly, a reference frame, also called a “frame,” contains information about the location of an “observer,” and their motion relative to what is observed.

# can treat them as a single airframe. The mirrored wing's leading-edge root
# point and orientation (sweep, dihedral, incidence) are derived from the
# original wing's attributes.
#
# A reflection is an IMPROPER transformation (determinant -1) that flips
# handedness/chirality. A rotation or translation is a PROPER transformation
# (determinant +1) that preserves handedness. If we derived the mirrored wing's
# points using only a passive transformation, we would get a right-handed copy
# of the original wing just relocated -- meaning sweep, dihedral, twist, or
# camber would all carry the wrong geometric sign on the opposite side.
# Reflection is the only linear operation that genuinely inverts handedness
# to turn a right wing into a left wing.
# --------------------------------------------------------------------------
assert symmetryPoint_G_Cg is not None
assert symmetryNormal_G is not None

# Step 1: Generate the active transformation reflection matrix based on the
# airplane's symmetry plane (defined in global geometry axes relative to CG).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now reflect_T_act is built from symmetryNormal_G and symmetryPoint_G_Cg (geometry axes), but it's then applied to Fipp_Wn_Ler etc., which are in wing axes. Building a reflection in one axis system and then applying it to vectors in another isn't valid in general. It only lands on the correct mirror when the geometry-axis reflection happens to equal the wing-axis one, which is true for a square, CG-centered mount but not for, say, a wing whose axes are rolled or yawed relative to geometry axes (nonzero x or z in angles_Gs_to_Wn_ixyz), or a symmetry plane offset from the CG in y.

We can make it consistent by re-expressing the plane in wing axes first (with the point relative to the leading edge root point), then building the reflection there, so the transformation and the vectors are in the same axis system. This needs T_pas_G_Cg_to_Wn_Ler in scope (it can be added to the gather block at the top of mesh_wing() alongside T_pas_Wn_Ler_to_G_Cg):

            # Re-express the symmetry plane in wing axes (its point relative to the
            # leading edge root point) so the reflection is built in the same axes as
            # the vectors it is applied to.
            symmetryNormal_Wn = _transformations.apply_T_to_vectors(
                T_pas_G_Cg_to_Wn_Ler, symmetryNormal_G, is_position=False
            )
            symmetryPoint_Wn_Ler = _transformations.apply_T_to_vectors(
                T_pas_G_Cg_to_Wn_Ler, symmetryPoint_G_Cg, is_position=True
            )
            reflect_T_act = _transformations.generate_reflect_T(
                plane_point_A_a=symmetryPoint_Wn_Ler,
                plane_normal_A=symmetryNormal_Wn,
                passive=False,
            )

(The two assert * is not None lines just above can stay. They still narrow the types before the re-expression.)

Optional follow-on: since the plane doesn't change between wing sections, these are section-invariant, so they can be hoisted above the for wing_section_num in range(num_wing_sections): loop and computed once:

    # The type 4 mirrored half is reflected about the wing's symmetry plane. Re-express
    # that plane in wing axes (its point relative to the leading edge root point) once,
    # so each section's reflection is built in the same axes as the vectors it is
    # applied to.
    reflect_T_act = None
    if symmetry_type == 4:
        assert symmetryNormal_G is not None
        assert symmetryPoint_G_Cg is not None
        assert T_pas_G_Cg_to_Wn_Ler is not None
        symmetryNormal_Wn = _transformations.apply_T_to_vectors(
            T_pas_G_Cg_to_Wn_Ler, symmetryNormal_G, is_position=False
        )
        symmetryPoint_Wn_Ler = _transformations.apply_T_to_vectors(
            T_pas_G_Cg_to_Wn_Ler, symmetryPoint_G_Cg, is_position=True
        )
        reflect_T_act = _transformations.generate_reflect_T(
            plane_point_A_a=symmetryPoint_Wn_Ler,
            plane_normal_A=symmetryNormal_Wn,
            passive=False,
        )

With that hoisted, the per-section block drops the asserts and the reflect_T_act construction and goes straight to using it (an assert reflect_T_act is not None at the top of the if symmetry_type == 4: body keeps mypy happy).

reflect_T_act = _transformations.generate_reflect_T(
plane_point_A_a=symmetryPoint_G_Cg,
plane_normal_A=symmetryNormal_G,
passive=False,
)

# Step 2 (ACTIVE transformation): Mirror each local MCS point across the symmetry plane.
# This householder-like reflection explicitly flips the geometric handedness.
reflected_Fipp_Wn_Ler = _transformations.apply_T_to_vectors(
reflect_T_act, Fipp_Wn_Ler, is_position=True
)
Expand All @@ -297,6 +318,9 @@ def mesh_wing(wing: wing_mod.Wing) -> None:
reflected_Bopp_Wn_Ler = _transformations.apply_T_to_vectors(
reflect_T_act, Bopp_Wn_Ler, is_position=True
)

# Step 3 (PASSIVE transformation): Re-express the now-reflected points from local
# wing axes into the global geometry axes frame relative to the CG.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"global geometry axes frame" should just be "global geometry axes" or even just "geometry axes".

reflected_Fipp_G_Cg = _transformations.apply_T_to_vectors(
T_pas_Wn_Ler_to_G_Cg, reflected_Fipp_Wn_Ler, is_position=True
)
Expand Down