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