Skip to content

In formationBarycenter classical orbital-element averaging is singular near circular orbits #1656

Description

@carlo98

Describe the bug
With useOrbitalElements = True, FormationBarycenter::computeBaricenter() averages a, e and i linearly, and the argument of periapsis omega and the true anomaly f separately with the weighted circular mean atan2(sum m_i sin(alpha_i), sum m_i cos(alpha_i)).

In a near-circular orbit, omega and f of each spacecraft are not well defined individually. Only their sum, the argument of latitude u = omega + f, is. In a formation, the small eccentricity vectors of the members point in different directions, so the members' omega (and with them f) are spread over the whole circle while their u are almost equal. The circular means of omega and of f are then sums of vectors that nearly cancel, and their directions are arbitrary. The sum of the two means is not the mean argument of latitude. The scalar mean of e also ignores the directions of the eccentricity vectors.

The module documentation only lists the singularity of the ascending node for near-equatorial orbits as a limitation.

To reproduce
Three or four spacecraft of equal mass in a projected circular orbit formation of radius 100 m about a circular 7000 km orbit (i = 0.9 rad), without a spacecraft at the center. The members have e = 7.1e-6 and omega of, for example, 237.3, 117.3 and 357.3 deg:

import numpy as np
from Basilisk.architecture import messaging
from Basilisk.fswAlgorithms import formationBarycenter
from Basilisk.utilities import SimulationBaseClass, macros, orbitalMotion as om

mu = om.MU_EARTH * 1e9  # [m^3/s^2]
oe = om.ClassicElements()
oe.a, oe.e, oe.i, oe.Omega, oe.omega, oe.f = 7000e3, 0.0, 0.9, 0.3, 0.0, 1.0  # [m, -, rad, rad, rad, rad]
rc, vc = (np.array(x) for x in om.elem2rv(mu, oe))
n = np.sqrt(mu / oe.a**3)  # [rad/s]

def barycenter(phasesDeg, useOrbitalElements, rho=100.0):  # rho [m]
    sim = SimulationBaseClass.SimBaseClass()
    sim.CreateNewProcess("p").addTask(sim.CreateNewTask("t", macros.sec2nano(1.0)))
    module = formationBarycenter.FormationBarycenter()
    module.useOrbitalElements = useOrbitalElements
    module.mu = mu
    keep = []
    for ph in np.radians(phasesDeg):
        rho_H = [rho / 2 * np.cos(ph), -rho * np.sin(ph), rho * np.cos(ph)]  # [m]
        rhoP_H = [-rho / 2 * n * np.sin(ph), -rho * n * np.cos(ph), -rho * n * np.sin(ph)]  # [m/s]
        rd, vd = om.hill2rv(rc, vc, rho_H, rhoP_H)
        keep.append(messaging.NavTransMsg().write(messaging.NavTransMsgPayload(r_BN_N=list(rd), v_BN_N=list(vd))))
        keep.append(messaging.VehicleConfigMsg().write(messaging.VehicleConfigMsgPayload(massSC=100.0)))
        module.addSpacecraftToModel(keep[-2], keep[-1])
    rec = module.transOutMsg.recorder()
    sim.AddModelToTask("t", module)
    sim.AddModelToTask("t", rec)
    sim.InitializeSimulation()
    sim.ConfigureStopTime(0)
    sim.ExecuteSimulation()
    return np.array(rec.r_BN_N[-1])

for phases in ([0, 120, 240], [0, 120.1, 240], [0, 121, 240], [0, 125, 240], [0, 140, 240], [0, 90, 180, 270]):
    rCart = barycenter(phases, False)
    rOe = barycenter(phases, True)
    print(f"phases {phases}: |r_cartesian - r_ref| = {np.linalg.norm(rCart - rc):.2f} m, "
          f"|r_OE - r_cartesian| = {np.linalg.norm(rOe - rCart):.1f} m")

It prints

phases [0, 120, 240]: |r_cartesian - r_ref| = 0.00 m, |r_OE - r_cartesian| = 1098382.5 m
phases [0, 120.1, 240]: |r_cartesian - r_ref| = 0.06 m, |r_OE - r_cartesian| = 17703.2 m
phases [0, 121, 240]: |r_cartesian - r_ref| = 0.63 m, |r_OE - r_cartesian| = 1777.4 m
phases [0, 125, 240]: |r_cartesian - r_ref| = 3.16 m, |r_OE - r_cartesian| = 367.6 m
phases [0, 140, 240]: |r_cartesian - r_ref| = 12.40 m, |r_OE - r_cartesian| = 106.1 m
phases [0, 90, 180, 270]: |r_cartesian - r_ref| = 0.00 m, |r_OE - r_cartesian| = 895148.1 m

When a spacecraft is at the center of the formation (chief plus three deputies), its exactly circular orbit dominates the means and the result is close to the Cartesian barycenter. The same holds for two spacecraft of equal mass, where the two circular means happen to add up to the mean u.

Expected behavior
For a formation whose members are close to each other, the orbital-element barycenter is close to the members, for any eccentricity, including near-circular orbits. If a configuration is singular for the chosen elements, the module documentation says so.

Screenshots
Na

Desktop (please complete the following information):

  • OS: Linux Ubuntu
  • Version: Basilisk 2.13.0b0
  • Python version: 3.11.10

Additional context
Possible fix: average elements that are not singular for circular (and equatorial) orbits, for example the equinoctial elements of orbitalMotion (clElem2eqElem / eqElem2clElem): a, P1 = e sin(Omega + omega), P2 = e cos(Omega + omega), Q1 = tan(i / 2) sin(Omega) and Q2 = tan(i / 2) cos(Omega) linearly, and the mean longitude l (or the true longitude L) with the circular mean. This also removes the documented equatorial singularity. Averaging the mean anomaly (or mean longitude) instead of the true anomaly would keep the barycenter on the mean motion of the formation for eccentric orbits.

Activity

  1. carlo98 commented on Nov 1, 2026

    @carlo98
    ContributorAuthor

    Hi @zakariyaghazzol, I am not. Thank you 💪

  2. zakariyaghazzol commented on Nov 1, 2026

    @zakariyaghazzol
    Contributor

    Hey @carlo98, are you already working on a fix for this? If not, I can investigate the equinoctial approach you suggested, starting with reproducing your example. Could we also clarify how the averaged orbit should behave for noncircular orbits, so the fix covers those cases as well?

  3. zakariyaghazzol commented on Nov 1, 2026

    @zakariyaghazzol
    Contributor

    @carlo98, in that case. I’m leaning toward the mean-longitude option you mentioned. For spacecraft with the same semimajor axis, it keeps their shared mean motion in the two-body case. Is that the behavior you had in mind for eccentric orbits?

  4. carlo98 commented on Nov 3, 2026

    @carlo98
    ContributorAuthor

    Yes, that’s the behavior I had in mind.

    Two additional notes to consider:

    1. Consider also the use of the retrograde-factor variant of the equinoctial elements (to deal with i=pi).

    2. In the documentation add a note on how e>=1 is dealt with.

  5. schaubh commented on Nov 3, 2026

    @schaubh
    Contributor

    We don't want to change the existing behavior of this module. If we just switch to doing mass averaged equinoctial elements, then we get a different OE-barycenter than if we do this using classical elements. Averaging classical elements just doesn't make sense near their singularities.

    We could introduce a flag to do the orbit element averaging using a range of orbit element types. This keeps the existing classical element averaging, but introduces alternate equinoctial averaging if operating near equatorial or circular orbits.

  6. schaubh commented on Nov 4, 2026

    @schaubh
    Contributor

    @zakariyaghazzol, let me know if you are already working on this. Otherwise I can knock this out readily.

  7. self-assigned this
    on Nov 4, 2026
  8. added theissue type on Nov 4, 2026
  9. zakariyaghazzol commented on Nov 4, 2026

    @zakariyaghazzol
    Contributor

    Yes @schaubh, I’ve got it working locally with the optional setting you suggested. Just finishing up the review before opening a PR.

  10. added 8 commits that reference this issue on Nov 5, 2026
  11. added 3 commits that reference this issue on Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

  • Status
    ✅ Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions