Generating Random Simplex Solar Hierarchies and Architecture Discussion

This post builds on the concepts contained in “Simplex Solar Hierarchies: Problem Specification.”

In this post we will create a system which produces random, abstract simplex solar hierarchies for an N-ary solar system. We emphasize abstract here because these orbital hierarchies will contain no information about the actual Keplerian elements of the various solar bodies they describe: only their hierarchical arrangement.

Architectural Preface

Before we dive into the development of the solar hierarchy, we should pause to discuss a little bit about where exactly we are in the PISES baseline.

At its topmost level, a galaxy in PISES is comprised of a singular class: GalacticSystem. GalacticSystem contains a variety of information about the Galaxy: its type, age, number of solar systems, and other information specific to its galactic type: eccentricity, bulge prominence, bar length, spiral constant, etc.

However, GalacitcSystem also contains, at program start, a large collection of SolarSystemFrame objects. A SolarSystemFrame contains only the barest, smallest amount of information needed about a Solar System in order to render the Galaxy. The Frame object contains only the number of stars, their types, and the system’s positional data relative to the galactic center.

When the user enters a solar system, they cause the generation of a SolarSystem object, which extends SolarSystemFrame. Needless to say, SolarSystem contains all of the detailed information about that solar system – but only what is required to fully render that solar system, nothing more.

In the GalacticSystem, the SolarSystemFrame in question is now replaced with a SolarSystem object. Over time, as the user visits more and more solar systems, these skeletal frames will be fleshed out into fully defined solar systems.

A fully defined SolarSystem contains, like the galactic system, a variety of top-level information about the solar system. It also will contain a number of StarSystem objects – and, after this blog post, it will also contain an OrbitalHierarchy which relates all of these StarSystems!

Each of these StarSystems contain a number of WorldSystemFrames. Upon visitation, a WorldSystemFrame will transform into a fully defined WorldSystem, in much the same way that the solar system frame became a solar system.

With this architectural preface, I believe there is enough context to begin developing our abstract solar hierarchy.

A notional depiction of a GlalacticSystem’s data structure.

The OrbitalNode Data Structure

An orbital hierarchy is comprised of some number of orbital nodes. For this reason, we will create the class OrbitalNode.

Each OrbitalNode must contain the following information:

  1. An Identifier.
  2. A pointer to a StarSystem which the OrbitalNode represents (or null, if this is a Barycenter).
  3. An OrbitalSystem, a class describing the actual keplerian elements of this node. This can begin as uninitialized, since right now we’re just developing the abstract hierarchy, not the explicit orbital pathways of the solar bodies.
  4. A Parent Node (null if no parent).
  5. Two Child Nodes (null if no children).
  6. A Sibling Node (null if no sibling).
  7. A depth counter (how deep in the hierarchy the node is)
  8. A mass. This mass will be the mass of the node itself and of all its descendants. This mass will eventually be used in orbit determination.
  9. A child designation. We need to know which child this node is: “child 1,” the left child, or “child 2,” the right child. This information will be needed when we begin splitting nodes.

Given these requirements, here is a simple OrbitalNode class:

public class OrbitalNode 
{
    // The Star System at this Orbital Node.  Null if this is a Barycenter.
    public StarSystem starSystem;

    // The Orbital System of this node.  If the node is a StarSystem, this will be the StarSystem's OrbitalSystem.  
    // If the node is a Barycenter, this orbitalSystem will not be correlated with a StarSystem.
    public OrbitalSystem orbitalSystem;

    // The Parent Orbital Node.  Null if this is a depth 0 orbital node.
    public OrbitalNode parent;

    // The Child Orbital Nodes.  Null if this is a leaf orbital node.
    public OrbitalNode child1;
    public OrbitalNode child2;

    // The Sibling Orbital Node.  Null if this node has no sibling.
    public OrbitalNode sibling;

    // How deep into the orbital hierarchy this orbital node is.  The first node is depth 0.
    public int depth;

    // The mass of this node and all the nodes of its descendants.
    public float nodeMass;

    // The Child Designation of this node; null if an only child
    public Child childDesignation;

    // Constructor
    public OrbitalNode(StarSystem ss, OrbitalNode p, OrbitalNode c1, OrbitalNode c2, OrbitalNode s, int d, float nm, Child c)
    {
        starSystem = ss;
        parent = p;
        child1 = c1;
        child2 = c2;
        sibling = s;
        depth = d;
        nodeMass = nm;
        childDesignation = c;
    }

    // Whether this node is Child 1, Child 2, or an only child
    public enum Child
    {
        CHILD_1,
        CHILD_2,
        ONLY_CHILD
    }
}

Generating the Orbital Hierarchy

When it is time to generate the OrbitalHierarchy, we will sort the stars on mass, and then begin with the first (heaviest) star. This star will become the “Root Node.” It will have no parents and no children, and be of the designation, “ONLY_CHILD.”

We will also need to maintain track of nodes which we will call “leaf nodes.” Leaf nodes are nodes which can be split, and have new stars added to them.

At initialization, there will be only one leaf node: the root node. Once we split that root node, we will have three nodes: a barycenter at the root, and two star system nodes as its children. Each of these star system nodes will now be leaf nodes, able to be split into further nodes.

When a node becomes split, it turns into a barycenter, and its corresponding star system gets moved into a lower hierarchy.

In the following example, you can see a solar hierarchy of depth 3. The white X depicts the D0 root node, with two children. Child 1 is the blue X, a barycenter of D1, and child 2 is the red star.

The blue barycenter also has two D2 children: Child 1 is the yellow X, a barycenter, and child 2 is the yellow star.

Finally, the yellow barycenter has two D3 children: two blue stars.

The red, yellow and two blue stars are all leaf nodes. That means, if we want to add another star to this system, those are the nodes that we would split.

To get started, we will have our SolarSystem constructor call a private method, GenerateOrbitalHierarchy.

public class SolarSystem : SolarSystemFrame
{
    // Various other elements unrelated to this post 
    // ...
    // ...

    // Orbital Hierarchy
    List<OrbitalNode> allNodes = new List<OrbitalNode>();
    public OrbitalNode rootNode = null;

    // Random seed
    System.Random r = new System.Random();

    // Construct a star system from a list of types.
    public SolarSystem(System.Guid si, System.Numerics.Vector3 p, List<StarType> st, bool es)
    {
        // Various initializations unrelated to this post
        // ...
        // ...

        // Generate Orbital Hierarchy
        GenerateOrbitalHierarchy();
        rootNode = allNodes.FirstOrDefault(n => n.depth == 0);
    }

    // Generate an orbital hierarchy.
    private void GenerateOrbitalHierarchy()
    {
        // If this is a unary system, there will be one node.
        if (starCount == 1)
        {
            allNodes.Add(new OrbitalNode(0, stars[0], null, null, null, null, 0, stars[0].solarMass, OrbitalNode.Child.ONLY_CHILD));
            return;
        }

        // Sort the systems on mass.
        List<StarSystem> massSortedSystems = stars.OrderBy(s => s.solarMass).ToList();

        // Create an empty collection for leaf nodes.
        List<OrbitalNode> leafNodes = new List<OrbitalNode>();

        // Add an orbital node for every star system.
        foreach(StarSystem ss in massSortedSystems)
        {
            AddOrbitalNode(ss, leafNodes);
        }
    }
}

GenerateOrbitalHierarchy will add an orbital node for each StarSystem in the SolarSystem. This is how we add an OrbitalNode:

// Add an orbital node to the orbital hierarchy.
private void AddOrbitalNode(StarSystem starSystem, List<OrbitalNode> leafNodes)
{
    // If this is the first orbital node,
    if (rootNode == null)
    {
        // Generate a root node; also specify it as a leaf.
        rootNode = new OrbitalNode(starSystem, null, null, null, null, 0, starSystem.solarMass, OrbitalNode.Child.ONLY_CHILD);
        allNodes.Add(rootNode);
        leafNodes.Add(rootNode);
    }
    // Otherwise, choose a leaf node and split it.
    else 
    {
        OrbitalNode randomLeaf = leafNodes[r.Next(leafNodes.Count)];
        SplitLeafNode(starSystem, randomLeaf, leafNodes);
    }
}

AddOrbitalNode employs the method, SplitLeafNode on a random leaf node. To split a leaf node, we do the following:

// Split a leaf node and add starSystem
private void SplitLeafNode(StarSystem starSystem, OrbitalNode nodeToSplit, List<OrbitalNode> leafNodes)
{
    // Create a new barycenter to replace the node.
    OrbitalNode newBarycenter = new OrbitalNode(null, nodeToSplit.parent, null, null, nodeToSplit.sibling, nodeToSplit.depth, nodeToSplit.nodeMass, nodeToSplit.childDesignation);
    allNodes.Add(newBarycenter);

    // Update the leaf parent's child designation.
    // If it is child 1, reassign the parent's child 1 pointer.
    if (nodeToSplit.childDesignation == OrbitalNode.Child.CHILD_1)
    {
        nodeToSplit.parent.child1 = newBarycenter;
    }
    // If it is child 2, reassign the parent's child 2 pointer.
    else if (nodeToSplit.childDesignation == OrbitalNode.Child.CHILD_2)
    {
        nodeToSplit.parent.child2 = newBarycenter;
    }
    // Otherwise, this is the root node.  Do nothing.

    // Generate two children.  
    // Child 1 will be the leaf node.
    OrbitalNode child1 = new OrbitalNode(nodeToSplit.starSystem, newBarycenter, null, null, null, nodeToSplit.depth + 1, nodeToSplit.starSystem.solarMass, OrbitalNode.Child.CHILD_1);
    allNodes.Add(child1);

    // Child 2 will be the new system.
    OrbitalNode child2 = new OrbitalNode(starSystem, newBarycenter, null, null, child1, nodeToSplit.depth + 1, starSystem.solarMass, OrbitalNode.Child.CHILD_2);
    allNodes.Add(child2);

    // At this point, we have fully replaced the node to split.
    allNodes.Remove(nodeToSplit);

    // update child 1 siblings.
    child1.sibling = child2;

    // Update newbarycenter children.
    newBarycenter.child1 = child1;
    newBarycenter.child2 = child2;

    // Update leafnodes.
    leafNodes.Remove(nodeToSplit);
    leafNodes.Add(child1);
    leafNodes.Add(child2);

    // Update the mass of all nodes above the new child.
    updateParentalMasses(child2, child2.nodeMass);
}

Lastly, you can see that we employ a recursive helper method, UpdateParentalMasses.

// Update the mass of all parents of the designated node.
private void UpdateParentalMasses(OrbitalNode targetNode, float addedMass)
{
    // We have reached the top of the hierarchy.
    if (targetNode.parent == null)
    {
        return;
    }

    // Otherwise, update the parent and ascend the hierarchy.
    targetNode.parent.nodeMass += addedMass;
    UpdateParentalMasses(targetNode.parent, addedMass);
}

Next Steps

At this point, we have our abstract orbital hierarchy! Starting with the Root Node, we can traverse this binary tree. Each StarSystem of the SolarSystem has a place in the ‘mobile’ hierarchy.

Eventually we want to render this solar system. We’ll want to render the stars and planets in their elliptical paths which means we’ll have to turn this abstract orbital hierarchy into a set of Keplerian Orbital Elements; this will be the subject of the next post.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s