From 5506ff9a0f32ec2df3f00c585e6e2f2fb08edddf Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Wed, 9 Jul 2025 20:58:43 -0400 Subject: [PATCH 01/18] Clean up: proper path joining --- src/python/NN_Utils.py | 17 +++++++++-------- src/scripts/train_alpha_network.py | 7 ++++--- src/scripts/train_neural_network.py | 17 +++++++++-------- src/scripts/train_u_network.py | 18 ++++++++++-------- 4 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/python/NN_Utils.py b/src/python/NN_Utils.py index e63f11c..ad22ed2 100644 --- a/src/python/NN_Utils.py +++ b/src/python/NN_Utils.py @@ -1,6 +1,7 @@ import torch import matplotlib.pyplot as plt import numpy as np +import os from StateVectorUtilities import non_dimensionalize from torch.utils.data import TensorDataset, random_split @@ -40,7 +41,7 @@ def evaluate_neural_network( ax.set_ylabel(r"Control deltas u") ax.legend() fig.tight_layout() - fig.savefig(dir_plots + "nn_val_compare_u.jpg") # Vector format + fig.savefig(os.path.join(dir_plots,"nn_val_compare_u.jpg")) fig, ax = plt.subplots(figsize=(6, 6)) ax.scatter( @@ -51,7 +52,7 @@ def evaluate_neural_network( ax.set_ylabel(r"Control deltas $\alpha_y$") ax.legend() fig.tight_layout() - fig.savefig(dir_plots + "nn_val_compare_alpha_x.jpg") # Vector format + fig.savefig(os.path.join(dir_plots,"nn_val_compare_alpha_x.jpg")) fig, ax = plt.subplots(figsize=(6, 6)) ax.scatter( @@ -62,7 +63,7 @@ def evaluate_neural_network( ax.set_ylabel(r"Control deltas $\alpha_y$") ax.legend() fig.tight_layout() - fig.savefig(dir_plots + "nn_val_compare_alpha_y.jpg") # Vector format + fig.savefig(os.path.join(dir_plots,"nn_val_compare_alpha_y.jpg")) elif params["control_data_set"] == "u": @@ -79,7 +80,7 @@ def evaluate_neural_network( ax.set_ylabel(r"Control deltas u") ax.legend() fig.tight_layout() - fig.savefig(dir_plots + "nn_val_compare_u.jpg") # Vector format + fig.savefig(os.path.join(dir_plots,"nn_val_compare_u.jpg")) elif params["control_data_set"] == "alpha": @@ -92,7 +93,7 @@ def evaluate_neural_network( ax.set_ylabel(r"Control deltas $\alpha_x$") ax.legend() fig.tight_layout() - fig.savefig(dir_plots + "nn_val_compare_alpha_x.jpg") # Vector format + fig.savefig(os.path.join(dir_plots,"nn_val_compare_alpha_x.jpg")) # Vector format fig, ax = plt.subplots(figsize=(6, 6)) ax.scatter( @@ -103,7 +104,7 @@ def evaluate_neural_network( ax.set_ylabel(r"Control deltas $\alpha_y$") ax.legend() fig.tight_layout() - fig.savefig(dir_plots + "nn_val_compare_alpha_y.jpg") # Vector format + fig.savefig(os.path.join(dir_plots,"nn_val_compare_alpha_y.jpg")) else: raise Exception( @@ -158,7 +159,7 @@ def compare_NN_with_ephem(NN_TBT, sample_ephem_compare, dir_plots, params): ax.set_ylabel("Ephemeris Throttle u") ax.legend() fig.tight_layout() - fig.savefig(dir_plots + "nn_ephem_compare_u.jpg") + fig.savefig(os.path.join(dir_plots,"nn_ephem_compare_u.jpg")) fig, ax = plt.subplots(figsize=(6, 6)) ax.plot( @@ -191,7 +192,7 @@ def compare_NN_with_ephem(NN_TBT, sample_ephem_compare, dir_plots, params): ax.set_ylabel("Ephemeris Thrust Direction") ax.legend() fig.tight_layout() - fig.savefig(dir_plots + "nn_ephem_compare_alpha.jpg") + fig.savefig(os.path.join(dir_plots,"nn_ephem_compare_alpha.jpg")) def query_NN_at_ephem_state(NN_TBT, vector, params): diff --git a/src/scripts/train_alpha_network.py b/src/scripts/train_alpha_network.py index dd78452..f327384 100644 --- a/src/scripts/train_alpha_network.py +++ b/src/scripts/train_alpha_network.py @@ -65,14 +65,15 @@ def train_u_network(): # paths print("Current wd: " + os.getcwd()) dir_training_dir = ( - "..\\data\\training_ephems\\test_set_bang_bang\\" # path to training data + "..\\data\\training_ephems\\test_set_bang_bang_subset\\" # path to training data ) dir_plots = "..\\data\\plots\\" # path for storing plot data dir_nn = "..\\data\\neural_networks\\" # path for saving trained nn path_training_dir = os.path.normpath(os.path.join(os.getcwd(), dir_training_dir)) path_plots = os.path.normpath(os.path.join(os.getcwd(), dir_plots)) path_nn = os.path.normpath(os.path.join(os.getcwd(), dir_nn)) - path_plot_nn_training = path_plots + "nn_training.jpg" + path_nn = os.path.normpath(os.path.join(path_nn, "nn_controller_weights.pth")) + path_plot_nn_training = os.path.join(path_plots,"nn_training.jpg") # plotting structure init arr_epochs = [] @@ -186,7 +187,7 @@ def train_u_network(): ) # save NN to file - torch.save(NN_TBT.state_dict(), path_nn + "nn_controller_weights.pth") + torch.save(NN_TBT.state_dict(), path_nn ) train_u_network() diff --git a/src/scripts/train_neural_network.py b/src/scripts/train_neural_network.py index 7e0d5a9..8998b44 100644 --- a/src/scripts/train_neural_network.py +++ b/src/scripts/train_neural_network.py @@ -64,14 +64,15 @@ def train_neural_network(): # paths dir_training_dir = ( - "\\data\\training_ephems\\test_set_bang_bang_subset\\" # path to training data + "..\\data\\training_ephems\\test_set_bang_bang_subset\\" # path to training data ) - dir_plots = "\\data\\plots\\" # path for storing plot data - dir_nn = "\\data\\neural_networks\\" # path for saving trained nn - path_training_dir = os.getcwd() + dir_training_dir - path_plots = os.getcwd() + dir_plots - path_nn = os.getcwd() + dir_nn - path_plot_nn_training = path_plots + "nn_training.jpg" + dir_plots = "..\\data\\plots\\" # path for storing plot data + dir_nn = "..\\data\\neural_networks\\" # path for saving trained nn + path_training_dir = os.path.normpath(os.path.join(os.getcwd(), dir_training_dir)) + path_plots = os.path.normpath(os.path.join(os.getcwd(), dir_plots)) + path_nn = os.path.normpath(os.path.join(os.getcwd(), dir_nn)) + path_nn = os.path.normpath(os.path.join(path_nn, "nn_controller_weights.pth")) + path_plot_nn_training = os.path.join(path_plots,"nn_training.jpg") # plotting structure init arr_epochs = [] @@ -177,7 +178,7 @@ def train_neural_network(): plot_training_loss(arr_epochs, arr_loss_train, arr_loss, path_plot_nn_training) # save NN to file - torch.save(NN_TBT.state_dict(), path_nn + "nn_controller_weights.pth") + torch.save(NN_TBT.state_dict(), path_nn ) train_neural_network() diff --git a/src/scripts/train_u_network.py b/src/scripts/train_u_network.py index 3ce14d1..2ecbf6c 100644 --- a/src/scripts/train_u_network.py +++ b/src/scripts/train_u_network.py @@ -63,15 +63,17 @@ def train_u_network(): } # paths + print("Current wd: " + os.getcwd()) dir_training_dir = ( - "\\data\\training_ephems\\test_set_bang_bang\\" # path to training data + "..\\data\\training_ephems\\test_set_bang_bang_subset\\" # path to training data ) - dir_plots = "\\data\\plots\\" # path for storing plot data - dir_nn = "\\data\\neural_networks\\" # path for saving trained nn - path_training_dir = os.getcwd() + dir_training_dir - path_plots = os.getcwd() + dir_plots - path_nn = os.getcwd() + dir_nn - path_plot_nn_training = path_plots + "nn_training.jpg" + dir_plots = "..\\data\\plots\\" # path for storing plot data + dir_nn = "..\\data\\neural_networks\\" # path for saving trained nn + path_training_dir = os.path.normpath(os.path.join(os.getcwd(), dir_training_dir)) + path_plots = os.path.normpath(os.path.join(os.getcwd(), dir_plots)) + path_nn = os.path.normpath(os.path.join(os.getcwd(), dir_nn)) + path_nn = os.path.normpath(os.path.join(path_nn, "nn_controller_weights.pth")) + path_plot_nn_training = os.path.join(path_plots,"nn_training.jpg") # plotting structure init arr_epochs = [] @@ -186,7 +188,7 @@ def train_u_network(): ) # save NN to file - torch.save(NN_TBT.state_dict(), path_nn + "nn_controller_weights.pth") + torch.save(NN_TBT.state_dict(), path_nn ) train_u_network() From 196703da266bd112e1b006bc7b51510f4fd77cd5 Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Wed, 9 Jul 2025 21:00:26 -0400 Subject: [PATCH 02/18] Get working setup post utility updates This is a clean up commit that reformats some variables into dictionary keys and so on after the refactoring of the other nn training scripts. I neglected to do this in the previous branch. --- src/scripts/train_alpha_network.py | 4 +-- src/scripts/train_neural_network.py | 54 ++++++++++++++--------------- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/src/scripts/train_alpha_network.py b/src/scripts/train_alpha_network.py index f327384..2e33312 100644 --- a/src/scripts/train_alpha_network.py +++ b/src/scripts/train_alpha_network.py @@ -40,11 +40,11 @@ def train_u_network(): # parameters params = { "training_data_pts": 1000, # training data batch size - "training_epochs": 1000, # number of training epochs to run + "training_epochs": 100, # number of training epochs to run "patience": 200000, # Epochs without training loss improvement to stop training "learning_rate_i": 0.1, # Initial Parameter learning rate "learning_rate_f": 0.1, # Final Parameter learning rate - "plot_update": 1000, # Number of epochs before plot is updated + "plot_update": 100, # Number of epochs before plot is updated "report_update": 1, # Number of epochs between reporting training status "train_fraction": 0.8, # Fraction of data to use for training "eval_fraction": 0.2, # Fraction of data to use for eval diff --git a/src/scripts/train_neural_network.py b/src/scripts/train_neural_network.py index 8998b44..7a01cad 100644 --- a/src/scripts/train_neural_network.py +++ b/src/scripts/train_neural_network.py @@ -14,7 +14,7 @@ from gymnasium.envs.registration import register from torch.utils.data import DataLoader from torch.optim.lr_scheduler import CosineAnnealingLR -from Neural_Net_Controller import NN_TBT_Controller +from Neural_Net_Controllers import NN_TBT_Controller from Training_Data_Generation import read_ephems_from_dir from Constants import Constants from Plotting_Utils import format_plots, plot_training_loss @@ -38,19 +38,19 @@ def train_neural_network(): # parameters - training_data_pts = 1000 # training data batch size - training_epochs = 100000 # number of training epochs to run - min_mse = 999999 # min mse init value - patience = 200000 # If the number of iterations since min is greater than this number - training ends - learning_rate_i = 0.1 # Initial Parameter learning rate - learning_rate_f = 0.1 # Final Parameter learning rate - plot_update = training_epochs # Number of epochs before plot is updated - report_update = 1000 # Number of epochs between reporting training status - train_fraction = 0.8 # Fraction of data to use for training - eval_fraction = 0.2 # Fraction of data to use for eval - annealing_tmax = 1000 - params = { + "training_data_pts": 1000, # training data batch size + "training_epochs": 1000, # number of training epochs to run + "patience": 200000, # Epochs without training loss improvement to stop training + "learning_rate_i": 0.4, # Initial Parameter learning rate + "learning_rate_f": 0.4, # Final Parameter learning rate + "plot_update": 1000, # Number of epochs before plot is updated + "report_update": 10, # Number of epochs between reporting training status + "train_fraction": 0.8, # Fraction of data to use for training + "eval_fraction": 0.2, # Fraction of data to use for eval + "annealing_tmax": 1000, # Cosine annealing max iters + "loss": "MSE", # MSE, BCEWithLogitsLoss + "control_data_set": "all", # Control data sets to train (all, u, alpha) "mu": Constants.MU_SUN * 10 ** (9), # sun mu [m^3/s^2] "max_T": 1.33, # max spacecraft thrust [N] "ISP": 3872.0, # spacecraft specific impulse [s] @@ -87,11 +87,11 @@ def train_neural_network(): # establish optimizer # optimizer = torch.optim.Adam(NN_TBT.parameters(), lr=learning_rate_i) - optimizer = torch.optim.SGD(NN_TBT.parameters(), lr=learning_rate_i) + optimizer = torch.optim.SGD(NN_TBT.parameters(), lr=params["learning_rate_i"]) # define a LR scheduler scheduler = CosineAnnealingLR( - optimizer, T_max=annealing_tmax, eta_min=learning_rate_f + optimizer, T_max=params["annealing_tmax"], eta_min=params["learning_rate_f"] ) # read ephemeris files @@ -103,9 +103,7 @@ def train_neural_network(): print(str(num_ephems * set_ephems[0].num_vectors) + " training data points") print("Number of Neural Network Parameters: " + str(num_p)) - train_dataset, val_dataset = pre_process_training_data( - set_ephems, train_fraction, eval_fraction, params - ) + train_dataset, val_dataset = pre_process_training_data( set_ephems, params ) # Training # -------------------------------------------------------------------------------------------------------- @@ -123,10 +121,10 @@ def train_neural_network(): NN_TBT.train() # using torch loader object to load training and eval data - train_loader = DataLoader(train_dataset, batch_size=training_data_pts, shuffle=True) - val_loader = DataLoader(val_dataset, batch_size=training_data_pts, shuffle=False) + train_loader = DataLoader(train_dataset, batch_size=params["training_data_pts"], shuffle=True) + val_loader = DataLoader(val_dataset, batch_size=params["training_data_pts"], shuffle=False) - while epoch <= training_epochs: + while epoch <= params["training_epochs"]: # perform training epoch NN_TBT, avg_train_loss = training_epoch( @@ -139,7 +137,7 @@ def train_neural_network(): i_at_min = epoch # exit condition - if epoch > patience + i_at_min: + if epoch > params["patience"] + i_at_min: print("Patience Criterion reached, exiting training") flag_exit = True break @@ -148,7 +146,7 @@ def train_neural_network(): break # eval NN - if epoch % plot_update == 0: + if epoch % params["plot_update"] == 0: params["flag_plot"] = True else: params["flag_plot"] = False @@ -162,20 +160,20 @@ def train_neural_network(): arr_loss_train.append(avg_train_loss) arr_loss.append(avg_loss_val) - if epoch % report_update == 0: + if epoch % params["report_update"] == 0: print( - f"Epoch [{epoch}/{training_epochs}], Training Loss: {avg_train_loss:.4e}, Eval loss: {avg_loss_val:.4e} Min loss: {min_mse:.4e} last min: {epoch - i_at_min} lr: {scheduler.get_last_lr()[0]:.4e}" + f"Epoch [{epoch}/{params["training_epochs"]}], Training Loss: {avg_train_loss:.4e}, Eval loss: {avg_loss_val:.4e} Min loss: {min_mse:.4e} last min: {epoch - i_at_min} lr: {scheduler.get_last_lr()[0]:.4e}" ) - if epoch % plot_update == 0: + if epoch % params["plot_update"] == 0: plot_training_loss( - arr_epochs, arr_loss_train, arr_loss, path_plot_nn_training + arr_epochs, arr_loss_train, arr_loss, path_plot_nn_training, params ) epoch = epoch + 1 # final training plot update - plot_training_loss(arr_epochs, arr_loss_train, arr_loss, path_plot_nn_training) + plot_training_loss(arr_epochs, arr_loss_train, arr_loss, path_plot_nn_training, params) # save NN to file torch.save(NN_TBT.state_dict(), path_nn ) From 385a47d5b4c0da1ba0a9156c6901606575c8bb32 Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Wed, 16 Jul 2025 21:02:10 -0400 Subject: [PATCH 03/18] Adding more constants with appropriate units --- src/python/Constants.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/python/Constants.py b/src/python/Constants.py index 8498b7c..681a2cd 100644 --- a/src/python/Constants.py +++ b/src/python/Constants.py @@ -1,5 +1,7 @@ class Constants: - RADIUS_SUN_M = 6.957e8 - RADIUS_EARTH_M = 6.378e6 - MU_SUN = 1.3e11 + RADIUS_SUN_M = 6.957e8 #m + RADIUS_EARTH_M = 6.378e6 #m + MU_SUN = 1.3e11 + MU_SUN_M = 1.3e20 #m^3/s^2 G0 = 9.80665 # m/s^2 + G0_KM = G0 / 1000 From 69cd81be95380c00c00188afd6c4c7072d12e1c3 Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Wed, 16 Jul 2025 21:03:38 -0400 Subject: [PATCH 04/18] Switching control args to optional in methods --- src/python/Ephemeris.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/python/Ephemeris.py b/src/python/Ephemeris.py index 988df60..486ba32 100644 --- a/src/python/Ephemeris.py +++ b/src/python/Ephemeris.py @@ -23,7 +23,7 @@ def __init__(self): # initialize an empty ephemeris object self.reset() - def add_data(self, et, x, y, vx, vy, m, alpha_x, alpha_y, u): + def add_data(self, et, x, y, vx, vy, m, alpha_x=0.0, alpha_y=0.0, u=0.0): self.arr_et = np.append(self.arr_et, et) self.arr_x = np.append(self.arr_x, x) self.arr_y = np.append(self.arr_y, y) @@ -35,7 +35,7 @@ def add_data(self, et, x, y, vx, vy, m, alpha_x, alpha_y, u): self.arr_u = np.append(self.arr_u, u) self.num_vectors = self.num_vectors + 1 - def add_polar_data(self, et, r, theta, r_dot, v_theta, m, alpha_x, alpha_y, u): + def add_polar_data(self, et, r, theta, r_dot, v_theta, m, alpha_x=0.0, alpha_y=0.0, u=0.0): # convert polar coordinates to cartesian x = r * np.cos(theta) y = r * np.sin(theta) From 1c51555410db9070cf85b0128848ac57c5e18cf4 Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Wed, 16 Jul 2025 21:04:24 -0400 Subject: [PATCH 05/18] Changing name to be more generic --- src/python/NN_Utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/python/NN_Utils.py b/src/python/NN_Utils.py index ad22ed2..adae5f1 100644 --- a/src/python/NN_Utils.py +++ b/src/python/NN_Utils.py @@ -128,7 +128,7 @@ def compare_NN_with_ephem(NN_TBT, sample_ephem_compare, dir_plots, params): vector = sample_ephem_compare.get_vector_at_index(i) - arr_control = query_NN_at_ephem_state(NN_TBT, vector, params) + arr_control = query_NN_at_state(NN_TBT, vector, params) if params["control_data_set"] == "all": arr_u_nn.append(arr_control[0]) @@ -195,7 +195,7 @@ def compare_NN_with_ephem(NN_TBT, sample_ephem_compare, dir_plots, params): fig.savefig(os.path.join(dir_plots,"nn_ephem_compare_alpha.jpg")) -def query_NN_at_ephem_state(NN_TBT, vector, params): +def query_NN_at_state(NN_TBT, vector, params): # unpack components of interest x = vector[1] From 7ed9c0cc092bd02c176a49808bab5c72cf42a7a0 Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Wed, 16 Jul 2025 21:05:13 -0400 Subject: [PATCH 06/18] Bug fix to start indexing at zero --- src/python/NN_Utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/python/NN_Utils.py b/src/python/NN_Utils.py index adae5f1..f38c62e 100644 --- a/src/python/NN_Utils.py +++ b/src/python/NN_Utils.py @@ -198,11 +198,11 @@ def compare_NN_with_ephem(NN_TBT, sample_ephem_compare, dir_plots, params): def query_NN_at_state(NN_TBT, vector, params): # unpack components of interest - x = vector[1] - y = vector[2] - vx = vector[3] - vy = vector[4] - m = vector[5] + x = vector[0] + y = vector[1] + vx = vector[2] + vy = vector[3] + m = vector[4] # pack into an array state = [x, y, vx, vy, m] From 818f24dc9af3f7492294eb97b3e5b52b2af4e6e7 Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Wed, 16 Jul 2025 21:06:34 -0400 Subject: [PATCH 07/18] Check if the loss parameter exists Possible use cases include situations where no loss function is defined, the function should be able to handle this without crashing. --- src/python/NN_Utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/python/NN_Utils.py b/src/python/NN_Utils.py index f38c62e..c24e0c2 100644 --- a/src/python/NN_Utils.py +++ b/src/python/NN_Utils.py @@ -204,7 +204,7 @@ def query_NN_at_state(NN_TBT, vector, params): vy = vector[3] m = vector[4] - # pack into an array + # pack into a new array state = [x, y, vx, vy, m] # non-dimensionalize the state vector @@ -229,7 +229,7 @@ def query_NN_at_state(NN_TBT, vector, params): # if we are using BCE with logits, convert to a thrust action # otherwise the NN directly outputs the control action - if params["loss"] == "BCEWithLogitsLoss": + if "loss" in params and (params["loss"] == "BCEWithLogitsLoss"): probs = torch.sigmoid(nn_output) nn_control = (probs > 0.5).float() else: From 9fa1fafe4125e3fbe49002711f7de11992e66cbc Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Wed, 16 Jul 2025 21:08:20 -0400 Subject: [PATCH 08/18] Adding correct EOM for env updates These need to basically match the Hamiltonian ODEs but without the co-states dictating the action. The action is an input array. --- src/python/Propagation.py | 68 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/python/Propagation.py b/src/python/Propagation.py index fc706e0..2cffb6b 100644 --- a/src/python/Propagation.py +++ b/src/python/Propagation.py @@ -489,3 +489,71 @@ def smoothing_function_homotopic(rho, eps, flag_constrain_u): u_smooth = 1 / 2 + (rho / 2 * eps) return u_smooth + + +def env_EOM_TBT_v2(t, state, params): + + """ + Two-Body Orbit Transfer Gym Environment ode propagation function + ----------------------------------------------------------------------------------- + This is a set of equations of motion that govern a spacecraft in the + 2-dimensional two-body problem, formatted specifically to the Two-Body + Transfer gym environment + + Inputs + ----------------------------------------------------------------------------------- + t: Elapsed time + state: Input state vector (x,y,vx,vy,m) + params: The list of parameters () + + Outputs + ----------------------------------------------------------------------------------- + Derivatives for state vector dy + """ + + # get the number of parameters + num_params = len(params) + + # check parameter length + if num_params != 7: + raise Exception("Invalid number of parameters") + + # unpack the state vector + x, y, vx, vy, m = state[:5] + + # unpack the parameters + mu = params[0] # gravitational parameter of the central body + T_max = params[1] # max thrust of the spacecraft + ISP = params[2] # spacecraft specific impulse + g0 = params[3] # acceleration at Earth surface + u = params[4] # spacecraft throttle control + alpha_x = params[5] # thrust x direction + alpha_y = params[6] # thrust y direction + + # create vectors + r_vec = np.array([x, y]) + v_vec = np.array([vx, vy]) + alpha_vec = np.array([alpha_x,alpha_y]) + + #enfore unit vector + if ((alpha_x**2+alpha_y**2) != 0.0 ): + alpha_vec = alpha_vec / (alpha_x**2+alpha_y**2) + + # Derivative calculation preliminaries + r = np.linalg.norm(r_vec) + + # state vector derivative calculations + dr_vec = v_vec + dv_vec = -mu / r**3 * r_vec + u * T_max / m * alpha_vec + dm = -T_max * u / ISP / g0 + + derivs = np.array( + [ + dr_vec[0], + dr_vec[1], + dv_vec[0], + dv_vec[1], + dm, + ]) + + return derivs From 595deb89f2adb7a9850b2b482138407f9a396e75 Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Wed, 16 Jul 2025 21:09:53 -0400 Subject: [PATCH 09/18] Adding a polar state update method These can now be called interchangeably (with the appropriate input coords) the SC object with update the other set whenever one is called. --- src/python/Spacecraft.py | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/python/Spacecraft.py b/src/python/Spacecraft.py index 71be4ea..99ee9ee 100644 --- a/src/python/Spacecraft.py +++ b/src/python/Spacecraft.py @@ -1,16 +1,16 @@ import numpy as np - +from StateVectorUtilities import polar_to_cartesian, cartesian_to_polar class Spacecraft: def __init__(self, r, theta, r_dot, v_theta, mass, C1, C2): # Initialize the state of the spacecraft - self.update_state(r, theta, r_dot, v_theta, mass) + self.update_state_polar(r, theta, r_dot, v_theta, mass) # Set propulsion parameters self.max_thrust = C1 self.specific_impulse = C2 - def update_state(self, r, theta, r_dot, v_theta, mass): + def update_state_polar(self, r, theta, r_dot, v_theta, mass): # Set state vector polar coordinates coordinates self.r = r self.theta = theta @@ -21,10 +21,7 @@ def update_state(self, r, theta, r_dot, v_theta, mass): self.mass = mass # convert polar coordinates to cartesian - x = r * np.cos(theta) - y = r * np.sin(theta) - vx = r_dot * np.cos(theta) - v_theta * r * np.sin(theta) - vy = r_dot * np.sin(theta) + v_theta * r * np.cos(theta) + x, y, vx, vy = polar_to_cartesian( r, theta, r_dot, v_theta ) # Set state vector cartesian coordinates self.x = x @@ -32,6 +29,23 @@ def update_state(self, r, theta, r_dot, v_theta, mass): self.vx = vx self.vy = vy + def update_state_cartesian(self, x, y, vx, vy, m ): + + self.x = x + self.y = y + self.vx = vx + self.vy = vy + self.mass = m + + r, theta, rdot, v_theta = cartesian_to_polar(x, y, vx, vy ) + + # update polar coordinates + self.r = r + self.theta = theta + self.r_dot = rdot + self.v_theta = v_theta + + def calc_Planar_OE(self, x_cb, y_cb, vx_cb, vy_cb, mu_cb): # determine coordinates relative to central body x_rel = self.x - x_cb @@ -101,3 +115,8 @@ def calc_Planar_OE(self, x_cb, y_cb, vx_cb, vy_cb, mu_cb): theta = 2 * np.pi - np.acos(dotp) return a, e, w, theta + + def get_cartesian_state(self): + + return self.x, self.y, self.vx, self.vy + From 9156d190ab4d47ff2874150cc82dcc4e48d1cee5 Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Wed, 16 Jul 2025 21:10:38 -0400 Subject: [PATCH 10/18] Updating env imports to include new utils --- src/python/TwoBody_Orb2Orb_Transfer_Env.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/python/TwoBody_Orb2Orb_Transfer_Env.py b/src/python/TwoBody_Orb2Orb_Transfer_Env.py index 4f1260e..bab8465 100644 --- a/src/python/TwoBody_Orb2Orb_Transfer_Env.py +++ b/src/python/TwoBody_Orb2Orb_Transfer_Env.py @@ -4,7 +4,8 @@ from scipy.integrate import solve_ivp from Constants import Constants from Spacecraft import Spacecraft -from Propagation import spacecraft_EOM_radial_2D_EB +from Propagation import env_EOM_TBT_v2 +from StateVectorUtilities import polar_to_cartesian, cartesian_to_polar class TwoBody_Orb2Orb_Transfer_Env(gym.Env): From be68b0a3479899c3b8b41bce17e7146af748fe45 Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Wed, 16 Jul 2025 21:12:24 -0400 Subject: [PATCH 11/18] Updating env action/state ranges to be cartesian --- src/python/TwoBody_Orb2Orb_Transfer_Env.py | 24 +++++++++++++--------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/python/TwoBody_Orb2Orb_Transfer_Env.py b/src/python/TwoBody_Orb2Orb_Transfer_Env.py index bab8465..5e4485a 100644 --- a/src/python/TwoBody_Orb2Orb_Transfer_Env.py +++ b/src/python/TwoBody_Orb2Orb_Transfer_Env.py @@ -32,11 +32,12 @@ def __init__(self): self.step_size = 3600.0 # define the action space - # The action space consists of two variables: + # The action space consists of three variables: # 1) a control throlle input (scaled from 0 to 1) - # 2) a thrust vector (0 to 2pi) - low_array_action = np.array([0.0, 0.0], dtype=np.float32) - high_array_action = np.array([1.0, 2 * np.pi], dtype=np.float32) + # 2) a thrust direction x vector component (ranges -1 to 1) + # 3) a thrust direction y vector component (ranges -1 to 1) + low_array_action = np.array([0.0, -1.0, -1.0], dtype=np.float32) + high_array_action = np.array([1.0, 1.0, 1.0], dtype=np.float32) self.action_space = gym.spaces.Box(low=low_array_action, high=high_array_action) def _get_info(self, ode_solution, delta_r): @@ -81,9 +82,12 @@ def reset(self, seed: Optional[int] = None, options: Optional[dict] = None): vx_cb = 0.0 vy_cb = 0.0 + #convert to cartesian coordinates with random theta as input + x, y, vx, vy = polar_to_cartesian( r, theta, r_dot, v_theta ) + # set the initial state of the environment self._state = np.array( - [r, theta, r_dot, v_theta, mass, mu, sma_target], dtype=np.float32 + [x, y, vx, vy, mass, mu, sma_target], dtype=np.float32 ) # set the location of the central body @@ -156,10 +160,10 @@ def _apply_dV_in_VNB_frame(self, dV, X_i, Y_i, VX_i, VY_i): def step(self, action): # unpack the state vector - r = self._state[0] - theta = self._state[1] - r_dot = self._state[2] - v_theta = self._state[3] + x = self._state[0] + y = self._state[1] + vx = self._state[2] + vy = self._state[3] mass = self._state[4] mu = self._state[5] sma_target = self._state[6] @@ -179,7 +183,7 @@ def step(self, action): # step the spacecraft forward t_span = (0.0, self.step_size) - y0 = np.array([r, theta, r_dot, v_theta, mass]) + y0 = np.array([x, y, vx, vy, mass]) params = np.array( [ self.arr_mu[0], From 7e5b6109aada63a23586a16eb5b6c307faf1fbe7 Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Wed, 16 Jul 2025 21:13:39 -0400 Subject: [PATCH 12/18] Changing from beta angle to alpha vec basis --- src/python/TwoBody_Orb2Orb_Transfer_Env.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/python/TwoBody_Orb2Orb_Transfer_Env.py b/src/python/TwoBody_Orb2Orb_Transfer_Env.py index 5e4485a..8798acd 100644 --- a/src/python/TwoBody_Orb2Orb_Transfer_Env.py +++ b/src/python/TwoBody_Orb2Orb_Transfer_Env.py @@ -179,7 +179,8 @@ def step(self, action): # unpack the action vector u = action[0] # throttle control - beta = action[1] # spacecraft attitude + alpha_x = action[1] # spacecraft thrust x-direction + alpha_y = action[2] # spacecraft thrust y-direction # step the spacecraft forward t_span = (0.0, self.step_size) @@ -191,7 +192,8 @@ def step(self, action): sc.max_thrust, sc.specific_impulse, u, - beta, + alpha_x, + alpha_y ], dtype=np.float32, ) From c064e0dc5b2689de809f4b80b5b8e13fa318124f Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Wed, 16 Jul 2025 21:15:46 -0400 Subject: [PATCH 13/18] misc clean up - Removed unneeded central body radius from ODE - Added g0 in km/s^2 for unit consistency - Updated EOM function handle - Removed comment - Updated final SC state update to use cartesian method --- src/python/TwoBody_Orb2Orb_Transfer_Env.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/python/TwoBody_Orb2Orb_Transfer_Env.py b/src/python/TwoBody_Orb2Orb_Transfer_Env.py index 8798acd..056bde2 100644 --- a/src/python/TwoBody_Orb2Orb_Transfer_Env.py +++ b/src/python/TwoBody_Orb2Orb_Transfer_Env.py @@ -188,9 +188,9 @@ def step(self, action): params = np.array( [ self.arr_mu[0], - self.planet_radii[0], sc.max_thrust, sc.specific_impulse, + Constants.G0_KM, u, alpha_x, alpha_y @@ -200,7 +200,7 @@ def step(self, action): # solve ODE solution = solve_ivp( - spacecraft_EOM_radial_2D_EB, t_span, y0, method="RK45", args=(params,) + env_EOM_TBT_v2, t_span, y0, method="RK45", args=(params,) ) # extract the final state vector from ODE solution (last column in y) @@ -212,10 +212,9 @@ def step(self, action): # update the state and elapsed time self.elapsed_t = self.elapsed_t + self.step_size self._state = np.append(y_final, [mu, sma_target]) - # self._state[4] and self._state[5] are constant # update the spacecraft object - sc.update_state(*y_final) + sc.update_state_cartesian(*y_final) # update the environment spacecraft object self._spacecraft = sc From 35608dd2a93a81faf7172f3a9b16d75097d07881 Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Wed, 16 Jul 2025 21:20:08 -0400 Subject: [PATCH 14/18] Updates to test to ensure TBO transfer test works I added an arbitrary thrust profile thats burning constantly with a smoothly changing attitude. The env reset method uses a fixed seed for reproducibility. --- src/tests/TBO_Transfer_Env_Test.py | 43 ++++++++++++++---------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/src/tests/TBO_Transfer_Env_Test.py b/src/tests/TBO_Transfer_Env_Test.py index 243e276..2606d16 100644 --- a/src/tests/TBO_Transfer_Env_Test.py +++ b/src/tests/TBO_Transfer_Env_Test.py @@ -8,7 +8,9 @@ from gymnasium.envs.registration import register # Adding python src code directory -sys.path.append(os.path.abspath("../python")) +current_dir = os.path.dirname(__file__) +python_src_dir = os.path.abspath(os.path.join(current_dir, "..", "python")) +sys.path.append(python_src_dir) from Ephemeris import Ephemeris @@ -25,7 +27,7 @@ env = gym.make("TwoBody_Orb2Orb_Transfer_Env-v0") -steps_per_traj = 365 * 24 * 3 +steps_per_traj = 86400 * 365 * 1.1 / 3600 num_traj = 1 @@ -35,21 +37,20 @@ def test_runnable_env(env, num_trajectories, num_steps_per_traj): arr_reward_totals = np.array([]) total_steps_in_env = 0 - for count_traj in range(0, num_traj): + for count_traj in range(0, num_trajectories): # reset the environment steps = 0 r_tot = 0.0 eph = Ephemeris() - observation, info = env.reset() + observation, info = env.reset(seed=42) while steps < steps_per_traj: - # Sample randomly from the action space. Since the action is a delta-V - # magnitude in km/s, and the action space is unbounded (-inf to inf) the - # test maneuver that is returned will be sampled from a Gaussian normal - # distribution with a mean of 0 and a standard deviation of 1. We - # devide by 1000 in this test case to give relatively small maneuvers. + # Arbitrary test action action = env.action_space.sample() + action[0] = 1.0 + action[1] = -1.0 + np.tanh(steps/steps_per_traj) + action[2] = -1.0 + np.tanh(2*steps/steps_per_traj) observation, reward, terminated, truncated, info = env.step(action) @@ -60,13 +61,19 @@ def test_runnable_env(env, num_trajectories, num_steps_per_traj): if terminated: break - eph.add_polar_data( + if truncated: + break + + eph.add_data( elapsed_time, observation[0], observation[1], observation[2], observation[3], observation[4], + action[1], + action[2], + action[0] ) # print( elapsed_time, a, e, reward ) @@ -89,20 +96,10 @@ def test_runnable_env(env, num_trajectories, num_steps_per_traj): if count_traj == num_traj - 1: print("Plotting last trajectory...") - fig = eph.plot_xy(info["planet_radii"]) - plot.show(fig) - - fig_reward, ax = plot.subplots(figsize=(6, 6)) - - ax.plot(arr_episodes, arr_reward_totals, label="Total Reward") + eph.plot_xy(info["planet_radii"]) + eph.plot_xy_ref_orbit(observation[6],"Earth Orbit") + eph.plot_all_ephemeris_data() - # Customize the figure - ax.set_title("Total Reward Per Episode") - ax.set_xlabel("Episode Count") - ax.set_ylabel("Total R") - ax.legend() - ax.grid(False) - plot.show(fig_reward) print("Test successful") From 530821e635734385c6ad487f1333ff1908e4a44e Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Wed, 16 Jul 2025 21:22:32 -0400 Subject: [PATCH 15/18] Adding three regression tests for gym env Each of these methods involves testing stepping in the environment for one step using 1) no maneuvers, 2) an arbitrary maneuver, and 3) a maneuver queried from a neural network. They both product log files with numerical results from the step/propagation and check against truth files. Meant to ensure no unintended behavior happens with env or propagation. --- src/tests/test_env_step_no_action.py | 119 +++++++++++++++++ src/tests/test_env_step_with_action.py | 121 +++++++++++++++++ src/tests/test_env_step_with_nn_action.py | 153 ++++++++++++++++++++++ 3 files changed, 393 insertions(+) create mode 100644 src/tests/test_env_step_no_action.py create mode 100644 src/tests/test_env_step_with_action.py create mode 100644 src/tests/test_env_step_with_nn_action.py diff --git a/src/tests/test_env_step_no_action.py b/src/tests/test_env_step_no_action.py new file mode 100644 index 0000000..8a50ce0 --- /dev/null +++ b/src/tests/test_env_step_no_action.py @@ -0,0 +1,119 @@ +import numpy as np +import gymnasium as gym +import matplotlib.pyplot as plot +import sys +import os +import filecmp + +from gymnasium import envs +from gymnasium.envs.registration import register + +# Adding python src code directory +current_dir = os.path.dirname(__file__) +python_src_dir = os.path.abspath(os.path.join(current_dir, "..", "python")) +sys.path.append(python_src_dir) + + +# register the environment if it isn't registered +if "TwoBody_Orb2Orb_Transfer_Env-v0" not in envs.registry.keys(): + register( + id="TwoBody_Orb2Orb_Transfer_Env-v0", + entry_point="TwoBody_Orb2Orb_Transfer_Env:TwoBody_Orb2Orb_Transfer_Env", + ) + + +# initialize the environment +env = gym.make("TwoBody_Orb2Orb_Transfer_Env-v0") +seed_in = 42 + +def log(info, log, flag_report_to_console=False): + + log.append(info) + + if flag_report_to_console: + print(info) + + return log + + +def test_env_step_no_action(env,seed_in): + + test_log = [] + test_log = log("Test Environment Step with No Action", test_log, True) + + total_steps_in_env = 0 + + observation_2, info = env.reset(seed=seed_in) + test_log = log("Environment has been reset", test_log, True) + test_log = log("Seed: " + str(seed_in), test_log, True) + + test_log = log("Observation: ", test_log, True) + test_log = log("X: " + str(observation_2[0]), test_log, True) + test_log = log("Y: " + str(observation_2[1]), test_log, True) + test_log = log("VX: " + str(observation_2[2]), test_log, True) + test_log = log("VY: " + str(observation_2[3]), test_log, True) + test_log = log("m: " + str(observation_2[4]), test_log, True) + test_log = log("mu: " + str(observation_2[5]), test_log, True) + test_log = log("sma_target: " + str(observation_2[6]), test_log, True) + test_log = log("\n", test_log, True) + + test_log = log("Info: ", test_log, True) + for key in info.keys(): + test_log = log(str(key) + ": " + str(info[key]), test_log, True) + + test_log = log("\n", test_log, True) + + #zero action + action = np.array([0.0, 0.0, 0.0]) + + test_log = log("Action", test_log, True) + test_log = log("u: " + str(action[0]), test_log, True) + test_log = log("alpha_x: " + str(action[1]), test_log, True) + test_log = log("alpha_y: " + str(action[2]), test_log, True) + test_log = log("\n", test_log, True) + + #step the environment + observation_2, reward, terminated, truncated, info_2 = env.step(action) + + #update observation_2 + test_log = log("Observation: ", test_log, True) + test_log = log("X: " + str(observation_2[0]), test_log, True) + test_log = log("Y: " + str(observation_2[1]), test_log, True) + test_log = log("VX: " + str(observation_2[2]), test_log, True) + test_log = log("VY: " + str(observation_2[3]), test_log, True) + test_log = log("m: " + str(observation_2[4]), test_log, True) + test_log = log("mu: " + str(observation_2[5]), test_log, True) + test_log = log("sma_target: " + str(observation_2[6]), test_log, True) + test_log = log("\n", test_log, True) + + #Report the reward + test_log = log("reward: " + str(reward), test_log, True) + test_log = log("terminated: " + str(terminated), test_log, True) + test_log = log("truncated: " + str(truncated), test_log, True) + test_log = log("\n", test_log, True) + + test_log = log("Info post-step: ", test_log, True) + for key in info_2.keys(): + test_log = log(str(key) + ": " + str(info_2[key]), test_log, True) + + test_log = log("\n", test_log, True) + + #write the log to a text file + dir_test = os.path.normpath(os.path.join(os.getcwd(), "data\\test_data\\test_env_step_no_action\\")) + path_test_report = os.path.normpath(os.path.join(dir_test, "output_test_env_step_no_action_log.txt")) + path_test_truth = os.path.normpath(os.path.join(dir_test, "truth_test_env_step_no_action_log.txt")) + with open(path_test_report, "w", encoding="utf-8") as f: + for line in test_log: + f.write(line + "\n") + + #compare the two files + print("output log: ", path_test_report) + print("truth log: ", path_test_truth) + are_same = filecmp.cmp(path_test_report, path_test_truth, shallow=False) + print("Test passed? ", are_same) + + + +test_env_step_no_action(env, seed_in) + + diff --git a/src/tests/test_env_step_with_action.py b/src/tests/test_env_step_with_action.py new file mode 100644 index 0000000..b3a3c27 --- /dev/null +++ b/src/tests/test_env_step_with_action.py @@ -0,0 +1,121 @@ +import numpy as np +import gymnasium as gym +import matplotlib.pyplot as plot +import sys +import os +import filecmp + +from gymnasium import envs +from gymnasium.envs.registration import register + +# Adding python src code directory +current_dir = os.path.dirname(__file__) +python_src_dir = os.path.abspath(os.path.join(current_dir, "..", "python")) +sys.path.append(python_src_dir) + + +# register the environment if it isn't registered +if "TwoBody_Orb2Orb_Transfer_Env-v0" not in envs.registry.keys(): + register( + id="TwoBody_Orb2Orb_Transfer_Env-v0", + entry_point="TwoBody_Orb2Orb_Transfer_Env:TwoBody_Orb2Orb_Transfer_Env", + ) + + +# initialize the environment +env = gym.make("TwoBody_Orb2Orb_Transfer_Env-v0") +seed_in = 42 + +def log(info, log, flag_report_to_console=False): + + log.append(info) + + if flag_report_to_console: + print(info) + + return log + + +def test_env_step_no_action(env,seed_in): + + test_log = [] + test_log = log("Test Environment Step with Thrust Action", test_log, True) + + total_steps_in_env = 0 + + observation_2, info = env.reset(seed=seed_in) + test_log = log("Environment has been reset", test_log, True) + test_log = log("Seed: " + str(seed_in), test_log, True) + + test_log = log("Observation: ", test_log, True) + test_log = log("X: " + str(observation_2[0]), test_log, True) + test_log = log("Y: " + str(observation_2[1]), test_log, True) + test_log = log("VX: " + str(observation_2[2]), test_log, True) + test_log = log("VY: " + str(observation_2[3]), test_log, True) + test_log = log("m: " + str(observation_2[4]), test_log, True) + test_log = log("mu: " + str(observation_2[5]), test_log, True) + test_log = log("sma_target: " + str(observation_2[6]), test_log, True) + test_log = log("\n", test_log, True) + + test_log = log("Info: ", test_log, True) + for key in info.keys(): + test_log = log(str(key) + ": " + str(info[key]), test_log, True) + + test_log = log("\n", test_log, True) + + #throttle is 1 and unit vector components are 1 and 1 (vector is normalized in "step") + action = np.array([1.0, 1.0, 1.0]) + + test_log = log("Action", test_log, True) + test_log = log("u: " + str(action[0]), test_log, True) + test_log = log("alpha_x: " + str(action[1]), test_log, True) + test_log = log("alpha_y: " + str(action[2]), test_log, True) + test_log = log("\n", test_log, True) + + #step the environment + observation_2, reward, terminated, truncated, info_2 = env.step(action) + + #update observation_2 + test_log = log("Observation: ", test_log, True) + test_log = log("X: " + str(observation_2[0]), test_log, True) + test_log = log("Y: " + str(observation_2[1]), test_log, True) + test_log = log("VX: " + str(observation_2[2]), test_log, True) + test_log = log("VY: " + str(observation_2[3]), test_log, True) + test_log = log("m: " + str(observation_2[4]), test_log, True) + test_log = log("mu: " + str(observation_2[5]), test_log, True) + test_log = log("sma_target: " + str(observation_2[6]), test_log, True) + test_log = log("\n", test_log, True) + + #Report the reward + test_log = log("reward: " + str(reward), test_log, True) + test_log = log("terminated: " + str(terminated), test_log, True) + test_log = log("truncated: " + str(truncated), test_log, True) + test_log = log("\n", test_log, True) + + test_log = log("Info post-step: ", test_log, True) + for key in info_2.keys(): + test_log = log(str(key) + ": " + str(info_2[key]), test_log, True) + + test_log = log("\n", test_log, True) + + #write the log to a text file + dir_test = os.path.normpath(os.path.join(os.getcwd(), "data\\test_data\\test_env_step_with_action\\")) + path_test_report = os.path.normpath(os.path.join(dir_test, "output_test_env_step_with_action_log.txt")) + path_test_truth = os.path.normpath(os.path.join(dir_test, "truth_test_env_step_with_action_log.txt")) + with open(path_test_report, "w", encoding="utf-8") as f: + for line in test_log: + f.write(line + "\n") + + #compare the two files + print("output log: ", path_test_report) + print("truth log: ", path_test_truth) + are_same = filecmp.cmp(path_test_report, path_test_truth, shallow=False) + print("Test passed? ", are_same) + + + + + +test_env_step_no_action(env, seed_in) + + diff --git a/src/tests/test_env_step_with_nn_action.py b/src/tests/test_env_step_with_nn_action.py new file mode 100644 index 0000000..aa07da3 --- /dev/null +++ b/src/tests/test_env_step_with_nn_action.py @@ -0,0 +1,153 @@ +import numpy as np +import gymnasium as gym +import sys +import os +import torch +import filecmp + +from gymnasium import envs +from gymnasium.envs.registration import register + +# Adding python src code directory +current_dir = os.path.dirname(__file__) +python_src_dir = os.path.abspath(os.path.join(current_dir, "..", "python")) +sys.path.append(python_src_dir) + +from NN_Utils import query_NN_at_state +from Constants import Constants +from Neural_Net_Controllers import NN_TBT_Controller + + +# register the environment if it isn't registered +if "TwoBody_Orb2Orb_Transfer_Env-v0" not in envs.registry.keys(): + register( + id="TwoBody_Orb2Orb_Transfer_Env-v0", + entry_point="TwoBody_Orb2Orb_Transfer_Env:TwoBody_Orb2Orb_Transfer_Env", + ) + + +# initialize the environment +env = gym.make("TwoBody_Orb2Orb_Transfer_Env-v0") +seed_in = 42 + +def log(info, log, flag_report_to_console=False): + + log.append(info) + + if flag_report_to_console: + print(info) + + return log + + +def test_env_step_no_action(env,seed_in): + + test_log = [] + test_log = log("Test Environment Step with Neural Network Thrust Action", test_log, True) + + #paths + path_test_dir = os.path.normpath(os.path.join(os.getcwd(), "data\\test_data\\test_env_step_with_nn_action\\")) + path_test_report = os.path.normpath(os.path.join(path_test_dir, "output_test_env_step_with_nn_action_log.txt")) + path_test_truth = os.path.normpath(os.path.join(path_test_dir, "truth_test_env_step_with_nn_action_log.txt")) + path_input_nn = os.path.normpath(os.path.join(path_test_dir, "nn_controller_weights_smoothed_full_10e3_epochs.pth")) + + + observation, info = env.reset(seed=seed_in) + test_log = log("Environment has been reset", test_log, True) + test_log = log("Seed: " + str(seed_in), test_log, True) + + #load neural network from file + nn_controller = NN_TBT_Controller() #instantiate NN object + nn_control_param_dict = torch.load(path_input_nn) #load parameter dictionary from file + nn_controller.load_state_dict(nn_control_param_dict) #load the state parameter dictionary + + + test_log = log("Neural Net loaded from: " + str(path_input_nn), test_log, True) + + test_log = log("Observation: ", test_log, True) + test_log = log("X: " + str(observation[0]), test_log, True) + test_log = log("Y: " + str(observation[1]), test_log, True) + test_log = log("VX: " + str(observation[2]), test_log, True) + test_log = log("VY: " + str(observation[3]), test_log, True) + test_log = log("m: " + str(observation[4]), test_log, True) + test_log = log("mu: " + str(observation[5]), test_log, True) + test_log = log("sma_target: " + str(observation[6]), test_log, True) + test_log = log("\n", test_log, True) + + test_log = log("Info: ", test_log, True) + for key in info.keys(): + test_log = log(str(key) + ": " + str(info[key]), test_log, True) + + test_log = log("\n", test_log, True) + + #pack NN state + x = observation[0] + y = observation[1] + vx = observation[2] + vy = observation[3] + m = observation[4] + state = [ x, y, vx, vy, m ] + + #define normalization parameters + params = { + "mu": Constants.MU_SUN * 10 ** (9), # sun mu [m^3/s^2] + "max_T": 1.33, # max spacecraft thrust [N] + "ISP": 3872.0, # spacecraft specific impulse [s] + "TOF": 1.1 * 365.25 * 24 * 60 * 60, # assumed time of flight [s] + "l_star": 149598023000, # characteristic length = Earth SMA [m] + "m_star": 3366.0, # characteristic mass = SC initial mass [kg] + "t_star": (149598023000**3 / (Constants.MU_SUN * 10 ** (9))) + ** 0.5, # characteristic time - derived + "g0": Constants.G0, # gravtational acceleration at Earth surface [m/s^2] + } + + #get action from NN + action = query_NN_at_state( nn_controller, state, params ) + + test_log = log("Action", test_log, True) + test_log = log("u: " + str(action[0]), test_log, True) + test_log = log("alpha_x: " + str(action[1]), test_log, True) + test_log = log("alpha_y: " + str(action[2]), test_log, True) + test_log = log("\n", test_log, True) + + #step the environment + observation, reward, terminated, truncated, info_2 = env.step(action) + + #update observation + test_log = log("Observation: ", test_log, True) + test_log = log("X: " + str(observation[0]), test_log, True) + test_log = log("Y: " + str(observation[1]), test_log, True) + test_log = log("VX: " + str(observation[2]), test_log, True) + test_log = log("VY: " + str(observation[3]), test_log, True) + test_log = log("m: " + str(observation[4]), test_log, True) + test_log = log("mu: " + str(observation[5]), test_log, True) + test_log = log("sma_target: " + str(observation[6]), test_log, True) + test_log = log("\n", test_log, True) + + #Report the reward + test_log = log("reward: " + str(reward), test_log, True) + test_log = log("terminated: " + str(terminated), test_log, True) + test_log = log("truncated: " + str(truncated), test_log, True) + test_log = log("\n", test_log, True) + + test_log = log("Info post-step: ", test_log, True) + for key in info_2.keys(): + test_log = log(str(key) + ": " + str(info_2[key]), test_log, True) + + test_log = log("\n", test_log, True) + + #write the log to a text file + with open(path_test_report, "w", encoding="utf-8") as f: + for line in test_log: + f.write(line + "\n") + + #compare the two files + print("output log: ", path_test_report) + print("truth log: ", path_test_truth) + are_same = filecmp.cmp(path_test_report, path_test_truth, shallow=False) + print("Test passed? ", are_same) + + +test_env_step_no_action(env, seed_in) + + From bbb035bc1a09763eb07c750da744ed5621acb3e0 Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Wed, 16 Jul 2025 21:23:17 -0400 Subject: [PATCH 16/18] Adding regression test data --- .../output_test_env_step_no_action_log.txt | 76 +++++++++++++++++ .../truth_test_env_step_no_action_log.txt | 76 +++++++++++++++++ .../output_test_env_step_with_action_log.txt | 76 +++++++++++++++++ .../truth_test_env_step_with_action_log.txt | 76 +++++++++++++++++ ...ller_weights_smoothed_full_10e3_epochs.pth | Bin 0 -> 22907 bytes ...utput_test_env_step_with_nn_action_log.txt | 77 ++++++++++++++++++ ...truth_test_env_step_with_nn_action_log.txt | 77 ++++++++++++++++++ 7 files changed, 458 insertions(+) create mode 100644 data/test_data/test_env_step_no_action/output_test_env_step_no_action_log.txt create mode 100644 data/test_data/test_env_step_no_action/truth_test_env_step_no_action_log.txt create mode 100644 data/test_data/test_env_step_with_action/output_test_env_step_with_action_log.txt create mode 100644 data/test_data/test_env_step_with_action/truth_test_env_step_with_action_log.txt create mode 100644 data/test_data/test_env_step_with_nn_action/nn_controller_weights_smoothed_full_10e3_epochs.pth create mode 100644 data/test_data/test_env_step_with_nn_action/output_test_env_step_with_nn_action_log.txt create mode 100644 data/test_data/test_env_step_with_nn_action/truth_test_env_step_with_nn_action_log.txt diff --git a/data/test_data/test_env_step_no_action/output_test_env_step_no_action_log.txt b/data/test_data/test_env_step_no_action/output_test_env_step_no_action_log.txt new file mode 100644 index 0000000..1f08793 --- /dev/null +++ b/data/test_data/test_env_step_no_action/output_test_env_step_no_action_log.txt @@ -0,0 +1,76 @@ +Test Environment Step with No Action +Environment has been reset +Seed: 42 +Observation: +X: 3.486322e+07 +Y: -2.2986622e+08 +VX: 23.379026 +VY: 3.545837 +m: 3366.0 +mu: 1.3e+11 +sma_target: 1.4959802e+08 + + +Info: +Elapsed time: 0.0 +ODE Solution: None +delta_state: None +planet_radii: [695700.] +a: 2.32495e+08 +e: 1.2412671e-16 +w: 26.56505 +theta: 342.0591 +max_thrust: 0.00133 +ISP: 3872.0 + + +Action +u: 0.0 +alpha_x: 0.0 +alpha_y: 0.0 + + +Observation: +X: 3.4947384e+07 +Y: -2.2985344e+08 +VX: 23.377726 +VY: 3.5543969 +m: 3366.0 +mu: 1.3e+11 +sma_target: 1.4959802e+08 + + +reward: 0.0 +terminated: False +truncated: False + + +Info post-step: +Elapsed time: 3600.0 +ODE Solution: message: The solver successfully reached the end of the integration interval. + success: True + status: 0 + t: [ 0.000e+00 1.881e+00 2.070e+01 2.088e+02 2.090e+03 + 3.600e+03] + y: [[ 3.486e+07 3.486e+07 ... 3.491e+07 3.495e+07] + [-2.299e+08 -2.299e+08 ... -2.299e+08 -2.299e+08] + ... + [ 3.546e+00 3.546e+00 ... 3.551e+00 3.554e+00] + [ 3.366e+03 3.366e+03 ... 3.366e+03 3.366e+03]] + sol: None + t_events: None + y_events: None + nfev: 32 + njev: 0 + nlu: 0 +delta_state: [ 8.4164000e+04 1.2784000e+04 -1.3008118e-03 8.5599422e-03 + 0.0000000e+00] +planet_radii: [695700.] +a: 2.3249498e+08 +e: 9.084384e-08 +w: 170.61444 +theta: 179.25969 +max_thrust: 0.00133 +ISP: 3872.0 + + diff --git a/data/test_data/test_env_step_no_action/truth_test_env_step_no_action_log.txt b/data/test_data/test_env_step_no_action/truth_test_env_step_no_action_log.txt new file mode 100644 index 0000000..1f08793 --- /dev/null +++ b/data/test_data/test_env_step_no_action/truth_test_env_step_no_action_log.txt @@ -0,0 +1,76 @@ +Test Environment Step with No Action +Environment has been reset +Seed: 42 +Observation: +X: 3.486322e+07 +Y: -2.2986622e+08 +VX: 23.379026 +VY: 3.545837 +m: 3366.0 +mu: 1.3e+11 +sma_target: 1.4959802e+08 + + +Info: +Elapsed time: 0.0 +ODE Solution: None +delta_state: None +planet_radii: [695700.] +a: 2.32495e+08 +e: 1.2412671e-16 +w: 26.56505 +theta: 342.0591 +max_thrust: 0.00133 +ISP: 3872.0 + + +Action +u: 0.0 +alpha_x: 0.0 +alpha_y: 0.0 + + +Observation: +X: 3.4947384e+07 +Y: -2.2985344e+08 +VX: 23.377726 +VY: 3.5543969 +m: 3366.0 +mu: 1.3e+11 +sma_target: 1.4959802e+08 + + +reward: 0.0 +terminated: False +truncated: False + + +Info post-step: +Elapsed time: 3600.0 +ODE Solution: message: The solver successfully reached the end of the integration interval. + success: True + status: 0 + t: [ 0.000e+00 1.881e+00 2.070e+01 2.088e+02 2.090e+03 + 3.600e+03] + y: [[ 3.486e+07 3.486e+07 ... 3.491e+07 3.495e+07] + [-2.299e+08 -2.299e+08 ... -2.299e+08 -2.299e+08] + ... + [ 3.546e+00 3.546e+00 ... 3.551e+00 3.554e+00] + [ 3.366e+03 3.366e+03 ... 3.366e+03 3.366e+03]] + sol: None + t_events: None + y_events: None + nfev: 32 + njev: 0 + nlu: 0 +delta_state: [ 8.4164000e+04 1.2784000e+04 -1.3008118e-03 8.5599422e-03 + 0.0000000e+00] +planet_radii: [695700.] +a: 2.3249498e+08 +e: 9.084384e-08 +w: 170.61444 +theta: 179.25969 +max_thrust: 0.00133 +ISP: 3872.0 + + diff --git a/data/test_data/test_env_step_with_action/output_test_env_step_with_action_log.txt b/data/test_data/test_env_step_with_action/output_test_env_step_with_action_log.txt new file mode 100644 index 0000000..e010724 --- /dev/null +++ b/data/test_data/test_env_step_with_action/output_test_env_step_with_action_log.txt @@ -0,0 +1,76 @@ +Test Environment Step with Thrust Action +Environment has been reset +Seed: 42 +Observation: +X: 3.486322e+07 +Y: -2.2986622e+08 +VX: 23.379026 +VY: 3.545837 +m: 3366.0 +mu: 1.3e+11 +sma_target: 1.4959802e+08 + + +Info: +Elapsed time: 0.0 +ODE Solution: None +delta_state: None +planet_radii: [695700.] +a: 2.32495e+08 +e: 1.2412671e-16 +w: 26.56505 +theta: 342.0591 +max_thrust: 0.00133 +ISP: 3872.0 + + +Action +u: 1.0 +alpha_x: 1.0 +alpha_y: 1.0 + + +Observation: +X: 3.4947384e+07 +Y: -2.2985344e+08 +VX: 23.378437 +VY: 3.555108 +m: 3365.874 +mu: 1.3e+11 +sma_target: 1.4959802e+08 + + +reward: 0.0 +terminated: False +truncated: False + + +Info post-step: +Elapsed time: 3600.0 +ODE Solution: message: The solver successfully reached the end of the integration interval. + success: True + status: 0 + t: [ 0.000e+00 1.866e+00 2.053e+01 2.071e+02 2.073e+03 + 3.600e+03] + y: [[ 3.486e+07 3.486e+07 ... 3.491e+07 3.495e+07] + [-2.299e+08 -2.299e+08 ... -2.299e+08 -2.299e+08] + ... + [ 3.546e+00 3.546e+00 ... 3.551e+00 3.555e+00] + [ 3.366e+03 3.366e+03 ... 3.366e+03 3.366e+03]] + sol: None + t_events: None + y_events: None + nfev: 32 + njev: 0 + nlu: 0 +delta_state: [ 8.4164000e+04 1.2784000e+04 -5.8937073e-04 9.2711449e-03 + -1.2597656e-01] +planet_radii: [695700.] +a: 2.3251091e+08 +e: 7.2937924e-05 +w: 28.867548 +theta: 339.77762 +max_thrust: 0.00133 +ISP: 3872.0 + + diff --git a/data/test_data/test_env_step_with_action/truth_test_env_step_with_action_log.txt b/data/test_data/test_env_step_with_action/truth_test_env_step_with_action_log.txt new file mode 100644 index 0000000..e010724 --- /dev/null +++ b/data/test_data/test_env_step_with_action/truth_test_env_step_with_action_log.txt @@ -0,0 +1,76 @@ +Test Environment Step with Thrust Action +Environment has been reset +Seed: 42 +Observation: +X: 3.486322e+07 +Y: -2.2986622e+08 +VX: 23.379026 +VY: 3.545837 +m: 3366.0 +mu: 1.3e+11 +sma_target: 1.4959802e+08 + + +Info: +Elapsed time: 0.0 +ODE Solution: None +delta_state: None +planet_radii: [695700.] +a: 2.32495e+08 +e: 1.2412671e-16 +w: 26.56505 +theta: 342.0591 +max_thrust: 0.00133 +ISP: 3872.0 + + +Action +u: 1.0 +alpha_x: 1.0 +alpha_y: 1.0 + + +Observation: +X: 3.4947384e+07 +Y: -2.2985344e+08 +VX: 23.378437 +VY: 3.555108 +m: 3365.874 +mu: 1.3e+11 +sma_target: 1.4959802e+08 + + +reward: 0.0 +terminated: False +truncated: False + + +Info post-step: +Elapsed time: 3600.0 +ODE Solution: message: The solver successfully reached the end of the integration interval. + success: True + status: 0 + t: [ 0.000e+00 1.866e+00 2.053e+01 2.071e+02 2.073e+03 + 3.600e+03] + y: [[ 3.486e+07 3.486e+07 ... 3.491e+07 3.495e+07] + [-2.299e+08 -2.299e+08 ... -2.299e+08 -2.299e+08] + ... + [ 3.546e+00 3.546e+00 ... 3.551e+00 3.555e+00] + [ 3.366e+03 3.366e+03 ... 3.366e+03 3.366e+03]] + sol: None + t_events: None + y_events: None + nfev: 32 + njev: 0 + nlu: 0 +delta_state: [ 8.4164000e+04 1.2784000e+04 -5.8937073e-04 9.2711449e-03 + -1.2597656e-01] +planet_radii: [695700.] +a: 2.3251091e+08 +e: 7.2937924e-05 +w: 28.867548 +theta: 339.77762 +max_thrust: 0.00133 +ISP: 3872.0 + + diff --git a/data/test_data/test_env_step_with_nn_action/nn_controller_weights_smoothed_full_10e3_epochs.pth b/data/test_data/test_env_step_with_nn_action/nn_controller_weights_smoothed_full_10e3_epochs.pth new file mode 100644 index 0000000000000000000000000000000000000000..6017479da3940967fb5d6418511e1b00659359c0 GIT binary patch literal 22907 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 L^APO+>%RX132d^h literal 0 HcmV?d00001 diff --git a/data/test_data/test_env_step_with_nn_action/output_test_env_step_with_nn_action_log.txt b/data/test_data/test_env_step_with_nn_action/output_test_env_step_with_nn_action_log.txt new file mode 100644 index 0000000..2208fb4 --- /dev/null +++ b/data/test_data/test_env_step_with_nn_action/output_test_env_step_with_nn_action_log.txt @@ -0,0 +1,77 @@ +Test Environment Step with Neural Network Thrust Action +Environment has been reset +Seed: 42 +Neural Net loaded from: C:\Users\micha\MSI_Data\Masters_Thesis\astro_compass\data\test_data\test_env_step_with_nn_action\nn_controller_weights_smoothed_full_10e3_epochs.pth +Observation: +X: 3.486322e+07 +Y: -2.2986622e+08 +VX: 23.379026 +VY: 3.545837 +m: 3366.0 +mu: 1.3e+11 +sma_target: 1.4959802e+08 + + +Info: +Elapsed time: 0.0 +ODE Solution: None +delta_state: None +planet_radii: [695700.] +a: 2.32495e+08 +e: 1.2412671e-16 +w: 26.56505 +theta: 342.0591 +max_thrust: 0.00133 +ISP: 3872.0 + + +Action +u: 0.28366512 +alpha_x: -0.22682723 +alpha_y: -0.17161115 + + +Observation: +X: 3.494738e+07 +Y: -2.2985344e+08 +VX: 23.376595 +VY: 3.553541 +m: 3365.964 +mu: 1.3e+11 +sma_target: 1.4959802e+08 + + +reward: 0.0 +terminated: False +truncated: False + + +Info post-step: +Elapsed time: 3600.0 +ODE Solution: message: The solver successfully reached the end of the integration interval. + success: True + status: 0 + t: [ 0.000e+00 1.900e+00 2.090e+01 2.109e+02 2.111e+03 + 3.600e+03] + y: [[ 3.486e+07 3.486e+07 ... 3.491e+07 3.495e+07] + [-2.299e+08 -2.299e+08 ... -2.299e+08 -2.299e+08] + ... + [ 3.546e+00 3.546e+00 ... 3.550e+00 3.554e+00] + [ 3.366e+03 3.366e+03 ... 3.366e+03 3.366e+03]] + sol: None + t_events: None + y_events: None + nfev: 32 + njev: 0 + nlu: 0 +delta_state: [ 8.4160000e+04 1.2784000e+04 -2.4318695e-03 7.7040195e-03 + -3.5888672e-02] +planet_radii: [695700.] +a: 2.3247046e+08 +e: 0.00010934778 +w: 156.20451 +theta: 164.84969 +max_thrust: 0.00133 +ISP: 3872.0 + + diff --git a/data/test_data/test_env_step_with_nn_action/truth_test_env_step_with_nn_action_log.txt b/data/test_data/test_env_step_with_nn_action/truth_test_env_step_with_nn_action_log.txt new file mode 100644 index 0000000..2208fb4 --- /dev/null +++ b/data/test_data/test_env_step_with_nn_action/truth_test_env_step_with_nn_action_log.txt @@ -0,0 +1,77 @@ +Test Environment Step with Neural Network Thrust Action +Environment has been reset +Seed: 42 +Neural Net loaded from: C:\Users\micha\MSI_Data\Masters_Thesis\astro_compass\data\test_data\test_env_step_with_nn_action\nn_controller_weights_smoothed_full_10e3_epochs.pth +Observation: +X: 3.486322e+07 +Y: -2.2986622e+08 +VX: 23.379026 +VY: 3.545837 +m: 3366.0 +mu: 1.3e+11 +sma_target: 1.4959802e+08 + + +Info: +Elapsed time: 0.0 +ODE Solution: None +delta_state: None +planet_radii: [695700.] +a: 2.32495e+08 +e: 1.2412671e-16 +w: 26.56505 +theta: 342.0591 +max_thrust: 0.00133 +ISP: 3872.0 + + +Action +u: 0.28366512 +alpha_x: -0.22682723 +alpha_y: -0.17161115 + + +Observation: +X: 3.494738e+07 +Y: -2.2985344e+08 +VX: 23.376595 +VY: 3.553541 +m: 3365.964 +mu: 1.3e+11 +sma_target: 1.4959802e+08 + + +reward: 0.0 +terminated: False +truncated: False + + +Info post-step: +Elapsed time: 3600.0 +ODE Solution: message: The solver successfully reached the end of the integration interval. + success: True + status: 0 + t: [ 0.000e+00 1.900e+00 2.090e+01 2.109e+02 2.111e+03 + 3.600e+03] + y: [[ 3.486e+07 3.486e+07 ... 3.491e+07 3.495e+07] + [-2.299e+08 -2.299e+08 ... -2.299e+08 -2.299e+08] + ... + [ 3.546e+00 3.546e+00 ... 3.550e+00 3.554e+00] + [ 3.366e+03 3.366e+03 ... 3.366e+03 3.366e+03]] + sol: None + t_events: None + y_events: None + nfev: 32 + njev: 0 + nlu: 0 +delta_state: [ 8.4160000e+04 1.2784000e+04 -2.4318695e-03 7.7040195e-03 + -3.5888672e-02] +planet_radii: [695700.] +a: 2.3247046e+08 +e: 0.00010934778 +w: 156.20451 +theta: 164.84969 +max_thrust: 0.00133 +ISP: 3872.0 + + From 9314d16a374997c8f43205c138e6a944cebff23f Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Wed, 16 Jul 2025 21:23:52 -0400 Subject: [PATCH 17/18] Adding these nns previously stored offline --- .../nn_controller_weights_bang_bang_alpha.pth | Bin 0 -> 22779 bytes .../nn_controller_weights_bang_bang_u_only.pth | Bin 0 -> 22651 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/neural_networks/nn_controller_weights_bang_bang_alpha.pth create mode 100644 data/neural_networks/nn_controller_weights_bang_bang_u_only.pth diff --git a/data/neural_networks/nn_controller_weights_bang_bang_alpha.pth b/data/neural_networks/nn_controller_weights_bang_bang_alpha.pth new file mode 100644 index 0000000000000000000000000000000000000000..e4eeaf086b25ae46b3ba3795329cc8ba97b53540 GIT binary patch literal 22779 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 Date: Wed, 16 Jul 2025 22:03:41 -0400 Subject: [PATCH 18/18] Linting and formatting --- src/python/Constants.py | 8 +-- src/python/Ephemeris.py | 10 ++-- src/python/NN_Utils.py | 18 +++--- src/python/Propagation.py | 22 ++++---- src/python/Spacecraft.py | 13 ++--- src/python/TwoBody_Orb2Orb_Transfer_Env.py | 20 +++---- src/tests/TBO_Transfer_Env_Test.py | 12 ++-- src/tests/test_env_step_no_action.py | 33 +++++------ src/tests/test_env_step_with_action.py | 35 ++++++------ src/tests/test_env_step_with_nn_action.py | 64 +++++++++++++--------- 10 files changed, 123 insertions(+), 112 deletions(-) diff --git a/src/python/Constants.py b/src/python/Constants.py index 681a2cd..675abbb 100644 --- a/src/python/Constants.py +++ b/src/python/Constants.py @@ -1,7 +1,7 @@ class Constants: - RADIUS_SUN_M = 6.957e8 #m - RADIUS_EARTH_M = 6.378e6 #m - MU_SUN = 1.3e11 - MU_SUN_M = 1.3e20 #m^3/s^2 + RADIUS_SUN_M = 6.957e8 # m + RADIUS_EARTH_M = 6.378e6 # m + MU_SUN = 1.3e11 + MU_SUN_M = 1.3e20 # m^3/s^2 G0 = 9.80665 # m/s^2 G0_KM = G0 / 1000 diff --git a/src/python/Ephemeris.py b/src/python/Ephemeris.py index 486ba32..0be8921 100644 --- a/src/python/Ephemeris.py +++ b/src/python/Ephemeris.py @@ -35,7 +35,9 @@ def add_data(self, et, x, y, vx, vy, m, alpha_x=0.0, alpha_y=0.0, u=0.0): self.arr_u = np.append(self.arr_u, u) self.num_vectors = self.num_vectors + 1 - def add_polar_data(self, et, r, theta, r_dot, v_theta, m, alpha_x=0.0, alpha_y=0.0, u=0.0): + def add_polar_data( + self, et, r, theta, r_dot, v_theta, m, alpha_x=0.0, alpha_y=0.0, u=0.0 + ): # convert polar coordinates to cartesian x = r * np.cos(theta) y = r * np.sin(theta) @@ -150,7 +152,7 @@ def plot_xy_ref_orbit(self, orbit_sma, label): def plot_all_ephemeris_data(self): figs = [] - + fig, ax = plot.subplots(figsize=(6, 6)) ax.plot(self.arr_et, self.arr_m, label="Spacecraft Mass") ax.set_title("Spacecraft Mass over Time") @@ -181,8 +183,8 @@ def plot_all_ephemeris_data(self): ax.legend(loc="lower right") plot.show() figs.append(fig) - - return figs; + + return figs def write_to_file(self, file_path, mod_vector_write_frequency=1): file_name_base = os.path.basename(file_path) diff --git a/src/python/NN_Utils.py b/src/python/NN_Utils.py index c24e0c2..9951ec9 100644 --- a/src/python/NN_Utils.py +++ b/src/python/NN_Utils.py @@ -41,7 +41,7 @@ def evaluate_neural_network( ax.set_ylabel(r"Control deltas u") ax.legend() fig.tight_layout() - fig.savefig(os.path.join(dir_plots,"nn_val_compare_u.jpg")) + fig.savefig(os.path.join(dir_plots, "nn_val_compare_u.jpg")) fig, ax = plt.subplots(figsize=(6, 6)) ax.scatter( @@ -52,7 +52,7 @@ def evaluate_neural_network( ax.set_ylabel(r"Control deltas $\alpha_y$") ax.legend() fig.tight_layout() - fig.savefig(os.path.join(dir_plots,"nn_val_compare_alpha_x.jpg")) + fig.savefig(os.path.join(dir_plots, "nn_val_compare_alpha_x.jpg")) fig, ax = plt.subplots(figsize=(6, 6)) ax.scatter( @@ -63,7 +63,7 @@ def evaluate_neural_network( ax.set_ylabel(r"Control deltas $\alpha_y$") ax.legend() fig.tight_layout() - fig.savefig(os.path.join(dir_plots,"nn_val_compare_alpha_y.jpg")) + fig.savefig(os.path.join(dir_plots, "nn_val_compare_alpha_y.jpg")) elif params["control_data_set"] == "u": @@ -80,7 +80,7 @@ def evaluate_neural_network( ax.set_ylabel(r"Control deltas u") ax.legend() fig.tight_layout() - fig.savefig(os.path.join(dir_plots,"nn_val_compare_u.jpg")) + fig.savefig(os.path.join(dir_plots, "nn_val_compare_u.jpg")) elif params["control_data_set"] == "alpha": @@ -93,7 +93,9 @@ def evaluate_neural_network( ax.set_ylabel(r"Control deltas $\alpha_x$") ax.legend() fig.tight_layout() - fig.savefig(os.path.join(dir_plots,"nn_val_compare_alpha_x.jpg")) # Vector format + fig.savefig( + os.path.join(dir_plots, "nn_val_compare_alpha_x.jpg") + ) # Vector format fig, ax = plt.subplots(figsize=(6, 6)) ax.scatter( @@ -104,7 +106,7 @@ def evaluate_neural_network( ax.set_ylabel(r"Control deltas $\alpha_y$") ax.legend() fig.tight_layout() - fig.savefig(os.path.join(dir_plots,"nn_val_compare_alpha_y.jpg")) + fig.savefig(os.path.join(dir_plots, "nn_val_compare_alpha_y.jpg")) else: raise Exception( @@ -159,7 +161,7 @@ def compare_NN_with_ephem(NN_TBT, sample_ephem_compare, dir_plots, params): ax.set_ylabel("Ephemeris Throttle u") ax.legend() fig.tight_layout() - fig.savefig(os.path.join(dir_plots,"nn_ephem_compare_u.jpg")) + fig.savefig(os.path.join(dir_plots, "nn_ephem_compare_u.jpg")) fig, ax = plt.subplots(figsize=(6, 6)) ax.plot( @@ -192,7 +194,7 @@ def compare_NN_with_ephem(NN_TBT, sample_ephem_compare, dir_plots, params): ax.set_ylabel("Ephemeris Thrust Direction") ax.legend() fig.tight_layout() - fig.savefig(os.path.join(dir_plots,"nn_ephem_compare_alpha.jpg")) + fig.savefig(os.path.join(dir_plots, "nn_ephem_compare_alpha.jpg")) def query_NN_at_state(NN_TBT, vector, params): diff --git a/src/python/Propagation.py b/src/python/Propagation.py index 2cffb6b..7d2c33b 100644 --- a/src/python/Propagation.py +++ b/src/python/Propagation.py @@ -492,7 +492,6 @@ def smoothing_function_homotopic(rho, eps, flag_constrain_u): def env_EOM_TBT_v2(t, state, params): - """ Two-Body Orbit Transfer Gym Environment ode propagation function ----------------------------------------------------------------------------------- @@ -517,7 +516,7 @@ def env_EOM_TBT_v2(t, state, params): # check parameter length if num_params != 7: raise Exception("Invalid number of parameters") - + # unpack the state vector x, y, vx, vy, m = state[:5] @@ -526,18 +525,18 @@ def env_EOM_TBT_v2(t, state, params): T_max = params[1] # max thrust of the spacecraft ISP = params[2] # spacecraft specific impulse g0 = params[3] # acceleration at Earth surface - u = params[4] # spacecraft throttle control - alpha_x = params[5] # thrust x direction - alpha_y = params[6] # thrust y direction + u = params[4] # spacecraft throttle control + alpha_x = params[5] # thrust x direction + alpha_y = params[6] # thrust y direction # create vectors r_vec = np.array([x, y]) v_vec = np.array([vx, vy]) - alpha_vec = np.array([alpha_x,alpha_y]) + alpha_vec = np.array([alpha_x, alpha_y]) - #enfore unit vector - if ((alpha_x**2+alpha_y**2) != 0.0 ): - alpha_vec = alpha_vec / (alpha_x**2+alpha_y**2) + # enfore unit vector + if (alpha_x**2 + alpha_y**2) != 0.0: + alpha_vec = alpha_vec / (alpha_x**2 + alpha_y**2) # Derivative calculation preliminaries r = np.linalg.norm(r_vec) @@ -554,6 +553,7 @@ def env_EOM_TBT_v2(t, state, params): dv_vec[0], dv_vec[1], dm, - ]) - + ] + ) + return derivs diff --git a/src/python/Spacecraft.py b/src/python/Spacecraft.py index 99ee9ee..c37b0bc 100644 --- a/src/python/Spacecraft.py +++ b/src/python/Spacecraft.py @@ -1,6 +1,7 @@ import numpy as np from StateVectorUtilities import polar_to_cartesian, cartesian_to_polar + class Spacecraft: def __init__(self, r, theta, r_dot, v_theta, mass, C1, C2): # Initialize the state of the spacecraft @@ -21,7 +22,7 @@ def update_state_polar(self, r, theta, r_dot, v_theta, mass): self.mass = mass # convert polar coordinates to cartesian - x, y, vx, vy = polar_to_cartesian( r, theta, r_dot, v_theta ) + x, y, vx, vy = polar_to_cartesian(r, theta, r_dot, v_theta) # Set state vector cartesian coordinates self.x = x @@ -29,7 +30,7 @@ def update_state_polar(self, r, theta, r_dot, v_theta, mass): self.vx = vx self.vy = vy - def update_state_cartesian(self, x, y, vx, vy, m ): + def update_state_cartesian(self, x, y, vx, vy, m): self.x = x self.y = y @@ -37,15 +38,14 @@ def update_state_cartesian(self, x, y, vx, vy, m ): self.vy = vy self.mass = m - r, theta, rdot, v_theta = cartesian_to_polar(x, y, vx, vy ) + r, theta, rdot, v_theta = cartesian_to_polar(x, y, vx, vy) # update polar coordinates self.r = r - self.theta = theta + self.theta = theta self.r_dot = rdot self.v_theta = v_theta - def calc_Planar_OE(self, x_cb, y_cb, vx_cb, vy_cb, mu_cb): # determine coordinates relative to central body x_rel = self.x - x_cb @@ -115,8 +115,7 @@ def calc_Planar_OE(self, x_cb, y_cb, vx_cb, vy_cb, mu_cb): theta = 2 * np.pi - np.acos(dotp) return a, e, w, theta - + def get_cartesian_state(self): return self.x, self.y, self.vx, self.vy - diff --git a/src/python/TwoBody_Orb2Orb_Transfer_Env.py b/src/python/TwoBody_Orb2Orb_Transfer_Env.py index 056bde2..03242b3 100644 --- a/src/python/TwoBody_Orb2Orb_Transfer_Env.py +++ b/src/python/TwoBody_Orb2Orb_Transfer_Env.py @@ -5,7 +5,7 @@ from Constants import Constants from Spacecraft import Spacecraft from Propagation import env_EOM_TBT_v2 -from StateVectorUtilities import polar_to_cartesian, cartesian_to_polar +from StateVectorUtilities import polar_to_cartesian class TwoBody_Orb2Orb_Transfer_Env(gym.Env): @@ -27,7 +27,9 @@ def __init__(self): # list of environment parameters (Sun is the central body) self.arr_mu = np.array([Constants.MU_SUN]) # solar mu [km^3/s^2] - self.planet_radii = np.array([Constants.RADIUS_SUN_M/1000]) # solar radius [km] + self.planet_radii = np.array( + [Constants.RADIUS_SUN_M / 1000] + ) # solar radius [km] self.elapsed_t = 0.0 self.step_size = 3600.0 @@ -82,13 +84,11 @@ def reset(self, seed: Optional[int] = None, options: Optional[dict] = None): vx_cb = 0.0 vy_cb = 0.0 - #convert to cartesian coordinates with random theta as input - x, y, vx, vy = polar_to_cartesian( r, theta, r_dot, v_theta ) + # convert to cartesian coordinates with random theta as input + x, y, vx, vy = polar_to_cartesian(r, theta, r_dot, v_theta) # set the initial state of the environment - self._state = np.array( - [x, y, vx, vy, mass, mu, sma_target], dtype=np.float32 - ) + self._state = np.array([x, y, vx, vy, mass, mu, sma_target], dtype=np.float32) # set the location of the central body self._arr_cb = np.array([x_cb, y_cb, vx_cb, vy_cb], dtype=np.float32) @@ -193,15 +193,13 @@ def step(self, action): Constants.G0_KM, u, alpha_x, - alpha_y + alpha_y, ], dtype=np.float32, ) # solve ODE - solution = solve_ivp( - env_EOM_TBT_v2, t_span, y0, method="RK45", args=(params,) - ) + solution = solve_ivp(env_EOM_TBT_v2, t_span, y0, method="RK45", args=(params,)) # extract the final state vector from ODE solution (last column in y) y_final = (solution.y[:, -1]).astype(np.float32) diff --git a/src/tests/TBO_Transfer_Env_Test.py b/src/tests/TBO_Transfer_Env_Test.py index 2606d16..403d049 100644 --- a/src/tests/TBO_Transfer_Env_Test.py +++ b/src/tests/TBO_Transfer_Env_Test.py @@ -1,6 +1,5 @@ import numpy as np import gymnasium as gym -import matplotlib.pyplot as plot import sys import os @@ -48,9 +47,9 @@ def test_runnable_env(env, num_trajectories, num_steps_per_traj): while steps < steps_per_traj: # Arbitrary test action action = env.action_space.sample() - action[0] = 1.0 - action[1] = -1.0 + np.tanh(steps/steps_per_traj) - action[2] = -1.0 + np.tanh(2*steps/steps_per_traj) + action[0] = 1.0 + action[1] = -1.0 + np.tanh(steps / steps_per_traj) + action[2] = -1.0 + np.tanh(2 * steps / steps_per_traj) observation, reward, terminated, truncated, info = env.step(action) @@ -73,7 +72,7 @@ def test_runnable_env(env, num_trajectories, num_steps_per_traj): observation[4], action[1], action[2], - action[0] + action[0], ) # print( elapsed_time, a, e, reward ) @@ -97,10 +96,9 @@ def test_runnable_env(env, num_trajectories, num_steps_per_traj): if count_traj == num_traj - 1: print("Plotting last trajectory...") eph.plot_xy(info["planet_radii"]) - eph.plot_xy_ref_orbit(observation[6],"Earth Orbit") + eph.plot_xy_ref_orbit(observation[6], "Earth Orbit") eph.plot_all_ephemeris_data() - print("Test successful") diff --git a/src/tests/test_env_step_no_action.py b/src/tests/test_env_step_no_action.py index 8a50ce0..9d6a302 100644 --- a/src/tests/test_env_step_no_action.py +++ b/src/tests/test_env_step_no_action.py @@ -1,6 +1,5 @@ import numpy as np import gymnasium as gym -import matplotlib.pyplot as plot import sys import os import filecmp @@ -26,6 +25,7 @@ env = gym.make("TwoBody_Orb2Orb_Transfer_Env-v0") seed_in = 42 + def log(info, log, flag_report_to_console=False): log.append(info) @@ -36,13 +36,11 @@ def log(info, log, flag_report_to_console=False): return log -def test_env_step_no_action(env,seed_in): +def test_env_step_no_action(env, seed_in): test_log = [] test_log = log("Test Environment Step with No Action", test_log, True) - total_steps_in_env = 0 - observation_2, info = env.reset(seed=seed_in) test_log = log("Environment has been reset", test_log, True) test_log = log("Seed: " + str(seed_in), test_log, True) @@ -63,7 +61,7 @@ def test_env_step_no_action(env,seed_in): test_log = log("\n", test_log, True) - #zero action + # zero action action = np.array([0.0, 0.0, 0.0]) test_log = log("Action", test_log, True) @@ -72,10 +70,10 @@ def test_env_step_no_action(env,seed_in): test_log = log("alpha_y: " + str(action[2]), test_log, True) test_log = log("\n", test_log, True) - #step the environment + # step the environment observation_2, reward, terminated, truncated, info_2 = env.step(action) - #update observation_2 + # update observation_2 test_log = log("Observation: ", test_log, True) test_log = log("X: " + str(observation_2[0]), test_log, True) test_log = log("Y: " + str(observation_2[1]), test_log, True) @@ -86,7 +84,7 @@ def test_env_step_no_action(env,seed_in): test_log = log("sma_target: " + str(observation_2[6]), test_log, True) test_log = log("\n", test_log, True) - #Report the reward + # Report the reward test_log = log("reward: " + str(reward), test_log, True) test_log = log("terminated: " + str(terminated), test_log, True) test_log = log("truncated: " + str(truncated), test_log, True) @@ -98,22 +96,25 @@ def test_env_step_no_action(env,seed_in): test_log = log("\n", test_log, True) - #write the log to a text file - dir_test = os.path.normpath(os.path.join(os.getcwd(), "data\\test_data\\test_env_step_no_action\\")) - path_test_report = os.path.normpath(os.path.join(dir_test, "output_test_env_step_no_action_log.txt")) - path_test_truth = os.path.normpath(os.path.join(dir_test, "truth_test_env_step_no_action_log.txt")) + # write the log to a text file + dir_test = os.path.normpath( + os.path.join(os.getcwd(), "data\\test_data\\test_env_step_no_action\\") + ) + path_test_report = os.path.normpath( + os.path.join(dir_test, "output_test_env_step_no_action_log.txt") + ) + path_test_truth = os.path.normpath( + os.path.join(dir_test, "truth_test_env_step_no_action_log.txt") + ) with open(path_test_report, "w", encoding="utf-8") as f: for line in test_log: f.write(line + "\n") - #compare the two files + # compare the two files print("output log: ", path_test_report) print("truth log: ", path_test_truth) are_same = filecmp.cmp(path_test_report, path_test_truth, shallow=False) print("Test passed? ", are_same) - test_env_step_no_action(env, seed_in) - - diff --git a/src/tests/test_env_step_with_action.py b/src/tests/test_env_step_with_action.py index b3a3c27..d6a55c5 100644 --- a/src/tests/test_env_step_with_action.py +++ b/src/tests/test_env_step_with_action.py @@ -1,6 +1,5 @@ import numpy as np import gymnasium as gym -import matplotlib.pyplot as plot import sys import os import filecmp @@ -26,6 +25,7 @@ env = gym.make("TwoBody_Orb2Orb_Transfer_Env-v0") seed_in = 42 + def log(info, log, flag_report_to_console=False): log.append(info) @@ -36,13 +36,11 @@ def log(info, log, flag_report_to_console=False): return log -def test_env_step_no_action(env,seed_in): +def test_env_step_no_action(env, seed_in): test_log = [] test_log = log("Test Environment Step with Thrust Action", test_log, True) - total_steps_in_env = 0 - observation_2, info = env.reset(seed=seed_in) test_log = log("Environment has been reset", test_log, True) test_log = log("Seed: " + str(seed_in), test_log, True) @@ -63,7 +61,7 @@ def test_env_step_no_action(env,seed_in): test_log = log("\n", test_log, True) - #throttle is 1 and unit vector components are 1 and 1 (vector is normalized in "step") + # throttle is 1 and unit vector components are 1 and 1 (vector is normalized in "step") action = np.array([1.0, 1.0, 1.0]) test_log = log("Action", test_log, True) @@ -72,10 +70,10 @@ def test_env_step_no_action(env,seed_in): test_log = log("alpha_y: " + str(action[2]), test_log, True) test_log = log("\n", test_log, True) - #step the environment + # step the environment observation_2, reward, terminated, truncated, info_2 = env.step(action) - #update observation_2 + # update observation_2 test_log = log("Observation: ", test_log, True) test_log = log("X: " + str(observation_2[0]), test_log, True) test_log = log("Y: " + str(observation_2[1]), test_log, True) @@ -86,7 +84,7 @@ def test_env_step_no_action(env,seed_in): test_log = log("sma_target: " + str(observation_2[6]), test_log, True) test_log = log("\n", test_log, True) - #Report the reward + # Report the reward test_log = log("reward: " + str(reward), test_log, True) test_log = log("terminated: " + str(terminated), test_log, True) test_log = log("truncated: " + str(truncated), test_log, True) @@ -98,24 +96,25 @@ def test_env_step_no_action(env,seed_in): test_log = log("\n", test_log, True) - #write the log to a text file - dir_test = os.path.normpath(os.path.join(os.getcwd(), "data\\test_data\\test_env_step_with_action\\")) - path_test_report = os.path.normpath(os.path.join(dir_test, "output_test_env_step_with_action_log.txt")) - path_test_truth = os.path.normpath(os.path.join(dir_test, "truth_test_env_step_with_action_log.txt")) + # write the log to a text file + dir_test = os.path.normpath( + os.path.join(os.getcwd(), "data\\test_data\\test_env_step_with_action\\") + ) + path_test_report = os.path.normpath( + os.path.join(dir_test, "output_test_env_step_with_action_log.txt") + ) + path_test_truth = os.path.normpath( + os.path.join(dir_test, "truth_test_env_step_with_action_log.txt") + ) with open(path_test_report, "w", encoding="utf-8") as f: for line in test_log: f.write(line + "\n") - #compare the two files + # compare the two files print("output log: ", path_test_report) print("truth log: ", path_test_truth) are_same = filecmp.cmp(path_test_report, path_test_truth, shallow=False) print("Test passed? ", are_same) - - - test_env_step_no_action(env, seed_in) - - diff --git a/src/tests/test_env_step_with_nn_action.py b/src/tests/test_env_step_with_nn_action.py index aa07da3..6f7550b 100644 --- a/src/tests/test_env_step_with_nn_action.py +++ b/src/tests/test_env_step_with_nn_action.py @@ -1,4 +1,3 @@ -import numpy as np import gymnasium as gym import sys import os @@ -30,6 +29,7 @@ env = gym.make("TwoBody_Orb2Orb_Transfer_Env-v0") seed_in = 42 + def log(info, log, flag_report_to_console=False): log.append(info) @@ -40,27 +40,41 @@ def log(info, log, flag_report_to_console=False): return log -def test_env_step_no_action(env,seed_in): +def test_env_step_no_action(env, seed_in): test_log = [] - test_log = log("Test Environment Step with Neural Network Thrust Action", test_log, True) - - #paths - path_test_dir = os.path.normpath(os.path.join(os.getcwd(), "data\\test_data\\test_env_step_with_nn_action\\")) - path_test_report = os.path.normpath(os.path.join(path_test_dir, "output_test_env_step_with_nn_action_log.txt")) - path_test_truth = os.path.normpath(os.path.join(path_test_dir, "truth_test_env_step_with_nn_action_log.txt")) - path_input_nn = os.path.normpath(os.path.join(path_test_dir, "nn_controller_weights_smoothed_full_10e3_epochs.pth")) + test_log = log( + "Test Environment Step with Neural Network Thrust Action", test_log, True + ) + # paths + path_test_dir = os.path.normpath( + os.path.join(os.getcwd(), "data\\test_data\\test_env_step_with_nn_action\\") + ) + path_test_report = os.path.normpath( + os.path.join(path_test_dir, "output_test_env_step_with_nn_action_log.txt") + ) + path_test_truth = os.path.normpath( + os.path.join(path_test_dir, "truth_test_env_step_with_nn_action_log.txt") + ) + path_input_nn = os.path.normpath( + os.path.join( + path_test_dir, "nn_controller_weights_smoothed_full_10e3_epochs.pth" + ) + ) observation, info = env.reset(seed=seed_in) test_log = log("Environment has been reset", test_log, True) test_log = log("Seed: " + str(seed_in), test_log, True) - #load neural network from file - nn_controller = NN_TBT_Controller() #instantiate NN object - nn_control_param_dict = torch.load(path_input_nn) #load parameter dictionary from file - nn_controller.load_state_dict(nn_control_param_dict) #load the state parameter dictionary - + # load neural network from file + nn_controller = NN_TBT_Controller() # instantiate NN object + nn_control_param_dict = torch.load( + path_input_nn + ) # load parameter dictionary from file + nn_controller.load_state_dict( + nn_control_param_dict + ) # load the state parameter dictionary test_log = log("Neural Net loaded from: " + str(path_input_nn), test_log, True) @@ -80,15 +94,15 @@ def test_env_step_no_action(env,seed_in): test_log = log("\n", test_log, True) - #pack NN state + # pack NN state x = observation[0] y = observation[1] vx = observation[2] vy = observation[3] m = observation[4] - state = [ x, y, vx, vy, m ] + state = [x, y, vx, vy, m] - #define normalization parameters + # define normalization parameters params = { "mu": Constants.MU_SUN * 10 ** (9), # sun mu [m^3/s^2] "max_T": 1.33, # max spacecraft thrust [N] @@ -101,8 +115,8 @@ def test_env_step_no_action(env,seed_in): "g0": Constants.G0, # gravtational acceleration at Earth surface [m/s^2] } - #get action from NN - action = query_NN_at_state( nn_controller, state, params ) + # get action from NN + action = query_NN_at_state(nn_controller, state, params) test_log = log("Action", test_log, True) test_log = log("u: " + str(action[0]), test_log, True) @@ -110,10 +124,10 @@ def test_env_step_no_action(env,seed_in): test_log = log("alpha_y: " + str(action[2]), test_log, True) test_log = log("\n", test_log, True) - #step the environment + # step the environment observation, reward, terminated, truncated, info_2 = env.step(action) - #update observation + # update observation test_log = log("Observation: ", test_log, True) test_log = log("X: " + str(observation[0]), test_log, True) test_log = log("Y: " + str(observation[1]), test_log, True) @@ -124,7 +138,7 @@ def test_env_step_no_action(env,seed_in): test_log = log("sma_target: " + str(observation[6]), test_log, True) test_log = log("\n", test_log, True) - #Report the reward + # Report the reward test_log = log("reward: " + str(reward), test_log, True) test_log = log("terminated: " + str(terminated), test_log, True) test_log = log("truncated: " + str(truncated), test_log, True) @@ -136,12 +150,12 @@ def test_env_step_no_action(env,seed_in): test_log = log("\n", test_log, True) - #write the log to a text file + # write the log to a text file with open(path_test_report, "w", encoding="utf-8") as f: for line in test_log: f.write(line + "\n") - #compare the two files + # compare the two files print("output log: ", path_test_report) print("truth log: ", path_test_truth) are_same = filecmp.cmp(path_test_report, path_test_truth, shallow=False) @@ -149,5 +163,3 @@ def test_env_step_no_action(env,seed_in): test_env_step_no_action(env, seed_in) - -