Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/autonoma.ca.git
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
 *
 * SPDX-License-Identifier: MIT
 */

/**
 * Responsible for simulating a rocket flying through a planet's atmosphere
 * into a stable orbit.
 */
class Rocket {
  /**
   * Initializes the Rocket object with wet mass and payload mass.
   * Calculates dry mass and sets initial azimuth and altitude.
   *
   * @param {number} wetMass - Total mass including fuel (kilograms).
   * @param {number} payloadMass - Mass of the payload (kilograms).
   */
  constructor(wetMass, payloadMass) {
    this.mass = wetMass;
    this.payloadMass = payloadMass;
    this.dryMass = wetMass * 0.1 + this.payloadMass;

    this.azimuth = 0.0;
    this.altitude = 0.0;
  }

  /**
   * Associates the rocket with a specific planet.
   *
   * @param {object} planet - An object representing the planet.
   */
  on(planet) {
    this.planet = planet;
  }

  /**
   * Sets the fuselage properties including diameter, drag coefficient,
   * and specific impulse.
   *
   * @param {number} diameter - Diameter of the rocket (meters).
   * @param {number} dragCoefficient - Drag coefficient (dimensionless).
   * @param {number} specificImpulse - Specific impulse (seconds).
   */
  fuselage(diameter, dragCoefficient, specificImpulse) {
    const crossSectionalArea = Math.PI * (diameter / 2) ** 2;
    this.ballisticCoefficient = dragCoefficient * crossSectionalArea;
    this.specificImpulse = specificImpulse;
  }

  /**
   * Sets the rocket's latitude and altitude for the launch.
   *
   * @param {number} latitude - Latitude of the launch site (degrees).
   * @param {number} altitude - Altitude of the launch site (meters).
   */
  translocate(latitude, altitude) {
    console.assert(altitude >= 0);

    this.latitude = latitude;
    this.altitude = altitude;
  }

  /**
   * Sets the target apogee (maximum altitude) for the rocket's flight.
   *
   * @param {number} targetAltitude - Target altitude (meters).
   */
  apogee(targetAltitude) {
    this.targetAltitude = targetAltitude;

    this.hVelocityTarget = this.planet.orbitalSpeed(
      this.latitude, this.targetAltitude
    );
  }

  /**
   * Calculates the rotational speed of the planet at the current latitude
   * and altitude.
   *
   * @returns {number} Rotational speed (meters per second).
   */
  rotational() {
    return this.planet.rotationalSpeed(this.latitude, this.altitude);
  }

  /**
   * Launches the rocket with an initial velocity.
   *
   * @param {number} velocity - Initial velocity (Mach).
   */
  launch(velocity) {
    const g = this.planet.gravity(this.latitude, this.targetAltitude);
    this.exhaustVelocity = this.specificImpulse * g;

    this.hVelocityInitial = this.rotational();
    this.hVelocity = this.hVelocityInitial;
    this.vVelocity = velocity * this.planet.soundSpeed(this.altitude);
    this.vVelocityInitial = this.vVelocity;
    this.maxAcceleration = 5.0 * g;

    const drag = this.drag();
    this.massFlowRate = Math.abs(this.drag()[1]) / this.exhaustVelocity * 1.001;
    this.tThrust = this.exhaustVelocity * this.massFlowRate;

    const vThrust = -drag[1];
    const hThrust = Math.sqrt(this.tThrust ** 2 - vThrust ** 2);

    this.vAcceleration = drag[1] / this.mass + g + vThrust / this.mass;
    this.hAcceleration = drag[0] / this.mass + hThrust / this.mass;
  }

  /**
   * Attaches a flight recorder to the rocket.
   *
   * @param {object} blackBox - An object representing the flight recorder.
   */
  recorder(blackBox) {
    this.flightRecorder = blackBox;
  }

  /**
   * Calculates the drag force experienced by the rocket.
   *
   * @returns {Array<number>} Drag force components [horizontal, vertical]
   * (newtons).
   */
  drag() {
    const rho = this.planet.airDensity(this.altitude);
    const vHorNet = this.hVelocity - this.hVelocityInitial;
    let speedTotal = this.magnitude(vHorNet, this.vVelocity);

    if (speedTotal === 0) {
      speedTotal = 1e-10;
    }

    const dragForce = 0.5 * rho * this.ballisticCoefficient *
      speedTotal ** 2;
    const ratio = [-vHorNet / speedTotal, -this.vVelocity / speedTotal];

    return [dragForce * ratio[0], dragForce * ratio[1]];
  }

  /**
   * Determines the rocket's current altitude.
   */
  altitudeReached() {
    const mu = this.planet.standardGravity();
    const r0 = this.planet.radius(this.latitude);
    const r1 = r0 + this.altitude;
    const r2 = mu / (-0.5 * this.vVelocity ** 2 + mu / r1);

    return r2 - r0;
  }

  /**
   * Checks if the rocket is still flying, i.e., if it has fuel, has not
   * reached the target altitude, and hasn't crashed.
   *
   * @returns {boolean} True if the rocket is flying.
   */
  flying() {
    return (
      this.mass > this.dryMass &&
      this.altitude < this.targetAltitude &&
      this.altitude > 0
    );
  }

  /**
   * Simulates the flight of the rocket for a given time step. Based on
   * Professor Aaron Ridley's implementation:
   *
   * https://github.com/aaronjridley/Kepler/blob/main/OrbitalInsertion/launch.py
   *
   * @param {number} step - Time step for the simulation (partial seconds).
   */
  fly(step) {
    const drag = this.drag();

    if (this.hVelocity < this.hVelocityTarget) {
      const accMag = this.magnitude(this.hAcceleration, this.vAcceleration);
      const absDrag = Math.abs(drag[1]);

      if (accMag > this.maxAcceleration &&
        this.exhaustVelocity * this.massFlowRate * 0.98 > absDrag) {
        this.massFlowRate *= 0.99;
      }

      if (this.exhaustVelocity * this.massFlowRate < absDrag) {
        this.massFlowRate = absDrag / this.exhaustVelocity * 1.1;
      }
    } else {
      this.massFlowRate = 0.0;
    }

    console.assert(this.massFlowRate >= 0);

    // Compute the total thrust.
    this.tThrust = this.exhaustVelocity * this.massFlowRate;

    let vThrust = 0.0;
    let hThrust = 0.0;

    if (this.tThrust > 0) {
      vThrust = -drag[1];

      const reached = this.altitudeReached();

      // Increase vertical thrust until orbital altitude is reached.
      if (reached < this.targetAltitude) {
        vThrust = 0.99 * this.tThrust;
      }

      if (vThrust > this.tThrust) {
        vThrust = this.tThrust;
      }

      hThrust = Math.sqrt(this.tThrust ** 2 - vThrust ** 2)
    }

    const g = this.planet.gravity(this.latitude, this.altitude);
    this.vAcceleration = drag[1] / this.mass - g + vThrust / this.mass;
    this.hAcceleration = drag[0] / this.mass + hThrust / this.mass;

    this.vVelocity += this.vAcceleration * step;
    this.hVelocity += this.hAcceleration * step;

    this.altitude += this.vVelocity * step + 0.5 * this.vAcceleration * step ** 2;
    this.azimuth += this.hVelocity * step + 0.5 * this.hAcceleration * step ** 2;

    this.mass -= this.massFlowRate * step;
  }

  /**
   * Records the rocket's flight data at a given time.
   *
   * @param {number} time - The current simulation time (seconds).
   */
  record(time) {
    if (this.flightRecorder) {
      this.flightRecorder.azimuth(time, this.azimuth);
      this.flightRecorder.horizontalVelocity(time, this.hVelocity);
      this.flightRecorder.horizontalAcceleration(time, this.hAcceleration);
      this.flightRecorder.altitude(time, this.altitude);
      this.flightRecorder.verticalVelocity(time, this.vVelocity);
      this.flightRecorder.verticalAcceleration(time, this.vAcceleration);
      this.flightRecorder.thrustAcceleration(time, this.tThrust / this.mass);
      this.flightRecorder.mass(time, this.mass);
    }
  }

  /**
   * Calculates the magnitude of two values.
   *
   * @param {number} x - First value.
   * @param {number} y - Second value.
   * @returns {number} Magnitude.
   */
  magnitude(x, y) {
    return Math.sqrt(x ** 2 + y ** 2);
  }

  emptyMass() {
    return this.dryMass;
  }

  initialOrbitalVelocity() {
    return this.hVelocityInitial;
  }

  initialVerticalVelocity() {
    return this.vVelocityInitial;
  }

  successfulFlight() {
    return (
      this.mass >= this.dryMass &&
      this.hVelocity >= this.hVelocityTarget &&
      this.altitude >= this.targetAltitude
    );
  }
}

export default Rocket;