Compute Lookup Table

This example demonstrates lookup table generation when the spacecraft has moveable appendages

compute_lut.py
  1### -------------------------------------------------------------------- ###
  2
  3#					    LOOK UP TABLE OBJECT BUILDING
  4
  5# Example case:
  6# Generate the normalized optical response lookup table for LRO.
  7
  8# The look up table is designed to store the "geometry vector" of a spacecraft.
  9# This vector is representative of how the spacecraft’s shape and surface 
 10# properties interact with radiation coming from a specific direction. 
 11# Alternatively, the look up table can be generated to store the cross-section
 12# of the spacecraft for atmospheric drag computation.
 13
 14
 15# The lookup table helps to speed up the computation of non-gravitational
 16# accelerations and is MANDATORY for albedo, thermal infrared and drag
 17# acceleration.
 18
 19# To compute a lookup table, we need to define a grid of right ascensions
 20# and declinations that represents the directions from which the radiation 
 21# is coming in the spacecraft body-fixed frame. 
 22
 23# Also, we need to specify every frame that is not fixed with respect to 
 24# the body-fixed frame. These frames are stored in the variable 'moving_frames'. 
 25# If a frame is not specified inside 'moving_frames', it is represented 
 26# fixed with respect to the spacecraft body frame. For every moving_frame, 
 27# a range of euler angles in a specific euler set must be defined.
 28
 29# The LUT have two computational mode:
 30# - If type is 'accel', the LUT computes the acceleration for a 
 31#   normalized radiation flux of 1 W/m**2 and a mass of 1 kg. 
 32#   The LUT values will be 3x1 vectors. This mode is for srp, albedo and 
 33#   thermal infrared acceleration.
 34# - If type is 'cross-section', the LUT computes the cross-section for the
 35#	drag acceleration. The LUT values will be single float. 
 36
 37# NOTE: Here we compute the lookup table for the Lunar Redonnaissance orbiter
 38# by varying the orientation of the solar array. The High Gain Antenna
 39# is considered fixed with respect to the bus. 
 40
 41### -------------------------------------------------------------------- ###
 42### IMPORTS
 43
 44import time
 45import xarray as xr
 46import spiceypy as sp
 47import numpy as np
 48import pickle as pkl
 49
 50from numpy import floor, mod
 51from math import ceil
 52
 53from pyRTX.classes.SRP import SolarPressure
 54from pyRTX.classes.Spacecraft import Spacecraft
 55from pyRTX.classes.PixelPlane import PixelPlane
 56from pyRTX.classes.RayTracer import RayTracer
 57from pyRTX.core.analysis_utils import epochRange2
 58
 59import multiprocessing as mproc
 60
 61import timeit, os, itertools, logging
 62
 63### -------------------------------------------------------------------- ###
 64### INPUTS & LUT CONFIG
 65			  	
 66n_cores    =  10							      # number of cores for parallel computation
 67grid_res   =  20							      # angular resolution for RA, DEC
 68angle_res  =  10						          # angular resolution for moving parts
 69spacing	   =  0.01								  # spacing between rays 
 70
 71ref_epc	    = "2010 may 10 09:25:00"	          # reference epoch
 72duration    = 10000  							  # seconds
 73timestep    = 100
 74
 75sc_mass 	  = 1								  # the sc mass must be 1 for LUT computation
 76base_frame    = 'LRO_SC_BUS'				      # sc body-fixed frame
 77moving_frames = ['LRO_SA',]					      # frames that are not fixed wrt the sc base frame
 78
 79eul_set = (2,1,3)								  # euler representation for moving frames attitude
 80
 81obj_path = '../example_data/LRO/'		          # path for 3D shape elements
 82
 83METAKR = '../example_data/LRO/metakernel_lro.tm'  # metakernel
 84
 85type = 'accel'	                                  # method: 'accel' or 'cross-section'
 86
 87lutfile = 'luts/lro_accel_lut.nc'	    		  # output file
 88
 89### --------------------------------------------------------------------------- ###
 90### LUT LIMITS
 91
 92sp.furnsh(METAKR)
 93
 94# List of right ascension and declination values for the incoming rays
 95RA   = np.linspace(0, 360, int(360/grid_res) + 1) * np.pi / 180
 96DEC  = np.linspace(-90, 90, int(180/grid_res) + 1) * np.pi / 180
 97
 98###  Here we find the limits for every moving frame, in terms of euler angles  ###
 99
100# Define timespan
101eul_limits = {frame: {e: [] for e in eul_set} for frame in moving_frames}
102epc_et0  = sp.str2et( ref_epc )
103epc_et1  = epc_et0 + duration
104epochs   = epochRange2(startEpoch = epc_et0, endEpoch = epc_et1, step = timestep)
105
106# Find euler limits
107for frame in moving_frames:
108    
109	EUL = np.zeros((len(epochs),3))
110
111	for i, epc in enumerate(epochs):
112
113		rot = sp.pxform(frame, base_frame, epc)
114		EUL[i,:] = np.array(sp.m2eul(rot, *eul_set)) * 180 / np.pi
115 
116	for i, e in enumerate(eul_set):
117  
118		if max(abs(EUL[:,i].max()), abs(EUL[:,i].min())) >= grid_res:
119			eul_limits[frame][e] = [EUL[:,i].min() - 0.05, EUL[:,i].max() + 0.05]
120
121sp.unload(METAKR)
122
123### --------------------------------------------------------------------------- ###
124### OBJECT DEFINITION
125
126# Define a spacecraft model
127spacecraft_model = {						        
128                          
129					'LRO_BUS': { 
130							 'file' : obj_path + 'bus_rotated.obj',	 # .obj file of the spacecraft component
131							 'frame_type': 'Spice',				     # type of frame (can be 'Spice' or 'UD'
132							 'frame_name': 'LRO_SC_BUS',			 # Name of the frame
133							 'center': [0.0,0.0,0.0],			     # Origin of the component
134							 'diffuse': 0.1,				         # Diffuse reflect. coefficient
135							 'specular': 0.3,				         # Specular reflect. coefficient
136							 },
137
138					'LRO_SA': {	
139							'file': obj_path + 'SA_recentred.obj',
140							'frame_type': 'Spice',
141							'frame_name': 'LRO_SA',
142							'center': [-1,-1.1, -0.1],
143							'diffuse': 0,
144							'specular': 0.3,
145							},
146
147
148					'LRO_HGA': { 	
149							'file': obj_path + 'HGA_recentred.obj',
150							'frame_type': 'Spice',
151							'frame_name': 'LRO_HGA',
152							'center':[-0.99,    -0.3,  -3.1],
153							'diffuse': 0.2,
154							'specular': 0.1,
155							},
156					}
157 
158 # Elements with moving frames must have an user defined rotation
159for elem in spacecraft_model.keys():
160    if any([spacecraft_model[elem]['frame_name'] == frame for frame in moving_frames]):
161        spacecraft_model[elem]['frame_type'] = 'UD'
162        spacecraft_model[elem]['UD_rotation'] = np.identity(4)
163
164# Define the Spacecraft Object (Refer to the class documentation for further details)
165lro = Spacecraft( name = 'LRO',     
166				  base_frame = 'LRO_SC_BUS', 					     # Name of the spacecraft body-fixed frame
167                  mass = sc_mass,									 # The mass should be 1 for LUT computation 
168				  spacecraft_model = spacecraft_model,
169				)
170
171# Define the Sun rays object
172rays = PixelPlane( spacecraft = lro,
173				   mode = 'Fixed',
174				   width = 15,
175				   height = 15,
176				   ray_spacing = spacing, 
177				   lon = 0,
178				   lat = 0,
179				   distance = 30)
180
181# Define the Ray Tracer
182rtx = RayTracer( lro,                    # Spacecraft object
183				 rays,                   # pixelPlane object
184				 kernel = 'Embree3',     # The RTX kernel to use (use Embree 3)
185				 bounces = 1,            # The number of bounces to account for
186				 diffusion = False,      # Account for secondary diffusion
187				 ) 
188
189# Define the Solar Pressure Object 
190# NOTE: for LUT computation the baseflux must be set to None.
191srp = SolarPressure( lro, rtx, baseflux = None, )
192
193### -------------------------------------------------------------------- ###
194### LUT INITIALIZATION
195
196# Time initialization
197tic = timeit.default_timer()
198
199# Refresh inputs and output directiories
200if not os.path.exists('inputs'): os.system('mkdir inputs/')
201if not os.path.exists('outputs'): os.system('mkdir outputs/')
202if not os.path.exists('luts'): os.system('mkdir luts/')
203os.system('rm inputs/*')
204os.system('rm outputs/*')
205
206# Save srp object
207with open('inputs/srp.pkl', 'wb') as f: pkl.dump(srp, f)
208
209# Deactivate trimesh logging
210log = logging.getLogger('trimesh')
211log.disabled = True
212
213print('\n *** Calculating dimension ...')
214
215# Build dimensions and axes
216axes = []
217dims = []
218for name in moving_frames:
219    for ax in eul_set:
220        
221        if not len(eul_limits[name][ax]): continue
222        lb = eul_limits[name][ax][0]
223        ub = eul_limits[name][ax][1]
224        
225        dims.append('%s%d'%(name,ax))
226        axes.append(np.linspace(lb, ub, ceil((ub-lb)/angle_res) + 1)*np.pi/180)	  
227
228# Append ra and dec
229dims.append('ra')
230dims.append('dec')
231dims.append('value')
232axes.append(RA) 
233axes.append(DEC) 
234
235# Build attribute dictionary for xarray
236attrs = {
237	'moving_frames': ",".join(moving_frames),
238	'base_frame': base_frame,
239	'type': type,
240	'ref_epoch': ref_epc,
241	'eul_set': ",".join([str(e) for e in eul_set]),
242	'dims': ",".join(dims),
243	}
244
245# Save attributes dictionary
246with open('inputs/attrs.pkl', 'wb') as f: pkl.dump(attrs, f)
247
248# Compute shape for xarray
249shape = tuple([len(r) for r in axes] + [3])
250
251# Build coordinates
252coords = {dims[i]: vals for i, vals in enumerate(axes)}
253
254### -------------------------------------------------------------------- ###
255### LUT PARALLEL COMPUTATION 
256
257print(f'\n *** LUT size: {shape} ...')
258
259# Init data array
260data  = np.zeros(shape)
261
262# Find all permutations
263SEQ    = list(itertools.product(*axes))
264IDX    = list(itertools.product(*[range(l) for l in shape[:-1]]))
265steps  = [int(i) for i in np.linspace(0,len(SEQ),n_cores)]
266SEQ    = [SEQ[i:j] for (i,j) in zip(steps[:-1],steps[1:])]
267IDX    = [IDX[i:j] for (i,j) in zip(steps[:-1],steps[1:])]
268INPUTS = [(I,S) for I,S in zip(IDX,SEQ)]
269
270# Save inputs file
271with open('inputs/inputs.pkl', 'wb') as f: pkl.dump(INPUTS, f)
272
273### -------------------------------------------------- ###
274### Multiprocessing target function
275
276def process(ID, METAKR):
277	os.system(f'python task_lut.py {ID} {METAKR}')
278	return
279
280### -------------------------------------------------- ###
281
282print('\n *** Filling the LUT values ...')
283
284# --------------------------------- #
285# PYTHON MULTIPROCESSING
286
287p = [0.] * len(INPUTS)
288
289# Process in parallel
290for ID in range(len(INPUTS)):
291	p[ID] = mproc.Process( target=process, args=(ID, METAKR,) )
292
293for ID in range(len(INPUTS)):       
294	p[ID].start()
295	while sum([pi.is_alive() for pi in p]) >= n_cores: time.sleep(0.5)
296
297for ID in range(len(INPUTS)): 
298	while(p[ID].is_alive()): p[ID].join(1)
299
300# --------------------------------- #
301
302# Fill LUT values
303for ID in range(len(INPUTS)):
304    result = np.load(f'outputs/output{ID}.npy')
305    for i, idxs in enumerate(INPUTS[ID][0]):
306        data[tuple(idxs)] = result[i]
307
308print('\n *** Generating the x-array ...')
309
310# Define X-array LUT
311data = xr.Dataset( data_vars = {'look_up_table': (dims, data)}, 
312				   coords    = coords,
313				   attrs     = attrs,)
314
315print(f'\n *** LUT completed!\n')
316
317# Reactivate trimesh logging
318log.disabled = False
319
320# Save
321data.to_netcdf(lutfile, encoding = data.encoding.update({'zlib': True, 'complevel': 1}))
322
323### ... Elapsed time
324toc = timeit.default_timer()
325time_min = int(floor((toc-tic)/60))
326time_sec = int(mod((toc-tic), 60))
327print("")
328print("\t Elapsed time: %d min, %d sec" %(time_min, time_sec))
329print("")
330
331### -------------------------------------------------------------------- ###