WarpTwin
Documentation for WarpTwin models and classes.
Loading...
Searching...
No Matches
script.py
###############################################################################
# Copyright (c) ATTX INC 2025. All Rights Reserved.
#
# This software and associated documentation (the "Software") are the 
# proprietary and confidential information of ATTX INC. The Software is 
# furnished under a license agreement between ATTX and the user organization 
# and may be used or copied only in accordance with the terms of the agreement.
# Refer to 'license/attx_license.adoc' for standard license terms.
#
# EXPORT CONTROL NOTICE: THIS SOFTWARE MAY INCLUDE CONTENT CONTROLLED UNDER THE
# INTERNATIONAL TRAFFIC IN ARMS REGULATIONS (ITAR) OR THE EXPORT ADMINISTRATION 
# REGULATIONS (EAR99). No part of the Software may be used, reproduced, or 
# transmitted in any form or by any means, for any purpose, without the express 
# written permission of ATTX, INC.
###############################################################################
"""
Electric Propulsion Transfer GTO-->GEO
--------------------------------------
This script demonstrates a simple analysis with spacecraft transfer from GTO to GEO.
"""
import sys
from warptwin.WarpTwinPy import (SimulationExecutive, CsvLogger, DEGREES_TO_RADIANS,
                                     Time, Node, END_STEP, connectSignals, CartesianVector3,
                                     Frame)
from warptwin.SpicePlanet import SpicePlanet
from warptwin.Spacecraft import Spacecraft
from warptwinutils.vizkit.VizKitPlanetRelative import VizKitPlanetRelative
from warptwin.OrbitalElementsSensorModel import OrbitalElementsSensorModel

# GN&C-specific stuff
from warptwin.DirectionalAdaptiveGuidance import DirectionalAdaptiveGuidance

# Create an instance of the thruster model. This is just a python dictionary to store
# basic data. Nothing exciting here
thruster = {
    'thrust_N' : 300.0/1000.0,      # mN convert to N
    'isp' : 1200.0,                 # Specific impulse, s
    'mdot_kg_s' : 3.5*1e-6*2.35,    # mdot of the thruster
    'sc_dry_mass_kg' : 500,         # Dry mass of the spacecraft
    'sc_prop_mass_kg' : 150,        # Propellant mass in kg
}

# Boilerplate simulation executive initialization
exc = SimulationExecutive()     # Create our executive -- by convention named exc
exc.args().addDefaultArgument("end",     60.0*86400.0)
exc.parseArgs(sys.argv)         # this interperets command-line inputs
exc.setTime("2026 FEB 28 12:00:00.000")     # Arbitrarily set date to guess for launch date
step_size = 10.0
exc.setRateSec(step_size)

# Create an Earth SPICE planet for our propagation
earth = SpicePlanet(exc, "earth")

# Generate spacecraft and configure mass
sc = Spacecraft(exc, "sc")
sc.params.planet_ptr(earth.outputs.self_id())
sc.params.mass(thruster['sc_prop_mass_kg'] + thruster['sc_dry_mass_kg'])

# Set up our directional adaptive guidance, which controls the electric thrust
# module. Note we set our target orbital elements and weight their value
dg = DirectionalAdaptiveGuidance(exc, END_STEP, "dg")
dg.params.a_weight(1.0)
dg.inputs.a_target(42000000.0)
dg.inputs.e_target(1e-6)
dg.inputs.i_target(DEGREES_TO_RADIANS*18.0)

# Connect spacecraft position to EP control input position
connectSignals(sc.outputs.pos_sc_pci, dg.inputs.pos_pci)
connectSignals(sc.outputs.vel_sc_pci, dg.inputs.vel_pci)

# Generate a node, which applies our electric thrust force on the vehicle.
# Alternatively, we could use the thruster model, but for this simple example
# using a node directly is sufficient
dg_node = Node("dg_node", sc.outputs.body())
dg_node.force_frame(earth.outputs.inertial_frame())

# Generate a model to output our spacecraft orbital elements for control and plotting
sc_oe = OrbitalElementsSensorModel(exc, "sc_oe")
connectSignals(sc.outputs.pos_sc_pci, sc_oe.inputs.pos__inertial)
connectSignals(sc.outputs.vel_sc_pci, sc_oe.inputs.vel__inertial)

# Create an instance of vizkit to see our spacecraft propagation
vk = VizKitPlanetRelative(exc)
vk.target(sc.outputs.body())
vk.planet(earth.outputs.inertial_frame())
exc.logManager().addLog(vk, Time(500))

# Set up logging
states = CsvLogger(exc, "states.csv")
states.addParameter('.exc.schedule.time.base_time', "time")
states.addParameter(sc.outputs.pos_sc_pci, "pos")
states.addParameter(sc.outputs.latitude_detic, "latitude")
states.addParameter(sc.outputs.longitude, "longitude")
states.addParameter(sc.outputs.altitude_detic, "altitude_m")
states.addParameter(sc_oe.outputs.a, "a")
states.addParameter(sc_oe.outputs.e, "e")
states.addParameter(sc_oe.outputs.i, "i")
states.addParameter(sc.params.mass, "mass")
exc.logManager().addLog(states, Time(100))

# Allow startup to initialize frames
exc.startup()

# Set initial spacecraft states. Set from orbital elements to be GTO 
sc.initializeFromOrbitalElements(0.5*(6378137.0+180000.0 + 6378137.0+35786000.0),
                                 0.73080,
                                 28.5*DEGREES_TO_RADIANS,
                                 0.0*DEGREES_TO_RADIANS,
                                 0.0*DEGREES_TO_RADIANS,
                                 0.0*DEGREES_TO_RADIANS)

exc.step(Time(0))

# And finally run simulation
updated = False
while not exc.isTerminated():
    # Map our DG output to our node input by multiplying by accel
    dg_node.force(CartesianVector3([
        thruster['thrust_N']*dg.outputs.accel_pnt_pci().get(0),
        thruster['thrust_N']*dg.outputs.accel_pnt_pci().get(1),
        thruster['thrust_N']*dg.outputs.accel_pnt_pci().get(2)
    ]))
    sc.params.mass(sc.params.mass() - thruster['mdot_kg_s']*step_size)
    err = exc.step()
    if(err):
        exit(1)
    
    if not updated and sc_oe.outputs.a() > 40000000:
        print("SMA Target Reached. Exiting Spiral Phase and entering Alignment")
        print("At time: " + str(exc.time().base_time().asFloatingPoint()))
        print("Setting targets to: ")
        print("\t SMA: " + str(dg.inputs.a_target()))
        print("\t e: " + str(dg.inputs.e_target()))
        print("\t i: " + str(dg.inputs.i_target()))
        updated = True
        dg.params.a_weight(10.0)
        dg.params.e_weight(1.0)
        dg.params.i_weight(1.0)