Skip to content

Polygon.J() returns the polar moment of inertia about the origin, not the centroid (docstring says centroid; parallel-axis correction is missing) #182

Description

@benoitpaillard

Summary

Polygon.J() in aerosandbox/geometry/polygon.py is documented as returning the polar moment of inertia "taken about the centroid," but the implementation never applies the parallel-axis correction — it silently returns the origin-referenced value instead. Polygon.Ixx() and Polygon.Iyy(), defined right next to it in the same file, apply the correction correctly, so this looks like a copy/paste that dropped the correction step rather than an intentional design choice.

Minimal reproduction

A 4×3 rectangle, offset away from the origin so the bug is visible (an origin-centered shape would hide it, since the correction term would be zero):

from aerosandbox.geometry.polygon import Polygon
import numpy as np

# By the parallel-axis theorem, for any rectangle:
#   Ixx_centroid = b*h^3/12 = 4*3^3/12 = 9
#   Iyy_centroid = h*b^3/12 = 3*4^3/12 = 16
# This holds regardless of the rectangle's position, so the correct J = Ixx+Iyy = 25.
coords = np.array([
    [10, 20],
    [14, 20],
    [14, 23],
    [10, 23],
])
poly = Polygon(coordinates=coords)

print("area:    ", float(poly.area()))      # 12.0   -- correct
print("centroid:", poly.centroid())         # [12. 21.5]  -- correct
print("Ixx:     ", float(poly.Ixx()))       # 9.0    -- correct (matches parallel-axis theorem)
print("Iyy:     ", float(poly.Iyy()))       # 16.0   -- correct (matches parallel-axis theorem)
print("J:       ", float(poly.J()))         # 7300.0 -- WRONG. Expected 25.0 (= Ixx + Iyy)

Output on aerosandbox==4.2.9:

area:     12.0
centroid: [12.  21.5]
Ixx:      9.0
Iyy:      16.0
J:        7300.0

Root cause

Polygon.Ixx() (lines 167-191) correctly shifts to the centroid:

Ixx = 1 / 12 * np.sum(a * (y**2 + y * y_n + y_n**2))
Iuu = Ixx - A * centroid[1] ** 2   # <-- parallel-axis correction
return Iuu

Polygon.J() (lines 242-266) recomputes the same raw sums but returns them without the correction:

Ixx = 1 / 12 * np.sum(a * (y**2 + y * y_n + y_n**2))
Iyy = 1 / 12 * np.sum(a * (x**2 + x * x_n + x_n**2))
J = Ixx + Iyy
return J   # <-- these Ixx/Iyy were never shifted to the centroid

So J() actually returns Ixx_about_origin + Iyy_about_origin, which equals (Ixx_centroid + A*y_c^2) + (Iyy_centroid + A*x_c^2) — correct only for shapes centered exactly on the origin.

Suggested fix

Since Ixx()/Iyy() already do this correctly, the simplest fix is for J() to just reuse them:

def J(self):
    """
    Returns the nondimensionalized polar moment of inertia, taken about the centroid.
    """
    return self.Ixx() + self.Iyy()

(A more efficient version could compute a, A, and the centroid once and reuse them across Ixx/Iyy/Ixy/J, since right now each method recomputes these independently — happy to open a PR for either the minimal fix or that refactor, whichever is preferred.)

Impact

Any code using Polygon.J() — including anything built on Airfoil/KulfanAirfoil, which inherit from Polygon — silently gets an incorrect value. This isn't just a fixed offset: the missing term (A*(x_c^2+y_c^2)) depends on the shape's centroid location, which changes as a shape is perturbed/optimized. So beyond wrong absolute values, this also biases any comparison or optimization that uses .J() to rank or constrain different shapes.

Environment

  • aerosandbox 4.2.9 (also present on current main, confirmed same code at the same lines)
  • Python 3.10

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions