#
# ISC License
#
# Copyright (c) 2025, Autonomous Vehicle Systems Lab, University of Colorado at Boulder
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
r"""
.. raw:: html
<iframe width="560" height="315" src="https://www.youtube.com/embed/oV2lPwB1J2g?si=Sw0z6B1D6RXydbTw" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
Overview
--------
This scenario demonstrates how to set up and modify QuadMaps within Vizard, as both rectangular/polar
defined regions and camera field-of-view regions.
The script is found in the folder ``basilisk/examples`` and executed by using::
python3 scenarioQuadMaps.py
.. important:: This scenario requires BSK to be built with ``vizInterface`` enabled.
Creating QuadMaps
-----------------
As shown in this scenario, QuadMaps can be created using the helper functions in :ref:`quadMapSupport`.
This is already imported within :ref:`vizSupport`, and can be accessed using ``vizSupport.qms``.
QuadMaps can also be built by hand.
.. note:: The ordering of the ``vertices`` field is important for QuadMaps to be rendered correctly.
Each corner must be specified in the fixed-frame of its parent body, and the resulting vector
is [x1, y1, z1, x2, ..., y4, z4]. If another quadrilateral is to be added to the mesh, its
vertices append directly after the first set. Thus, the ``vertices`` field should always contain
a multiple of 12 elements (k quads, each with 4 corners, each with a 3D position).
This scenario demonstrates setting up QuadMaps using latitude/longitude/altitude, as shown by both Colorado (bounded by
[37°N, 41°N] latitude and [-109°02'48"E, -102°02'48"E] longitude) and the Arctic Region (bounded by [66.5°N, 90°N]
latitude and [-180°E, 180°E] longitude). These are computed using ``vizSupport.qms.computeRectMesh()``.
The satellite is capturing images of Earth's surface every 20 minutes, and the camera field-of-view is projected onto
the surface as a red QuadMap. Due to the eccentricity of the orbit, the spacing and scale of these FOV boxes change
over time. There are two separate functions which handle the camera FOV: ``vizSupport.qms.computeCamFOVBox()`` returns
the intersection points between the reference ellipsoid and the camera FOV, while ``vizSupport.qms.subdivideFOVBox()``
interpolates these corners according to produce a square grid that can more easily wrap a convex surface. It is
important to note that, in this scenario, the FOV box is only drawn if all 4 corners find a valid intersection with the
reference ellipsoid. Otherwise, the non-intersection case becomes more complex to handle and is currently the
responsibility of the user to handle.
A custom QuadMap is also created on one of the solar cells of the satellite, which is defined by hand in body-fixed coordinates.
.. image:: /_images/static/scenarioQuadMaps_overview.png
:align: center
Once half of the simulation time passes, various settings are changed: Arctic Region toggles its ``isHidden`` flag,
Solar Cell switches its label to "NOLABEL", and Colorado changes its label/color.
"""
#
# Basilisk Scenario Script and Integrated Test
#
# Purpose: Integrated test of the Vizard QuadMap surface highlight visual element.
# Author: Jack Fox
# Creation Date: Apr. 15, 2025
#
import os
import numpy as np
# To play with any scenario scripts as tutorials, you should make a copy of them into a custom folder
# outside the Basilisk directory.
#
# To copy them, first find the location of the Basilisk installation.
# After installing, you can find the installed location of Basilisk by opening a python interpreter and
# running the commands:
from Basilisk import __path__
bskPath = __path__[0]
fileName = os.path.basename(os.path.splitext(__file__)[0])
fileNamePath = os.path.abspath(__file__)
# import simulation related support
from Basilisk.simulation import spacecraft
# general support file with common unit test functions
# import general simulation support files
from Basilisk.utilities import (SimulationBaseClass, macros, orbitalMotion,
simIncludeGravBody, unitTestSupport, vizSupport)
from Basilisk.architecture import messaging
[docs]
def run(show_plots):
"""
The scenarios can be run with the followings setups parameters:
Args:
show_plots (bool): Determines if the script should display plots. This script has no plots to show.
"""
# Create simulation variable names
simTaskName = "simTask"
simProcessName = "simProcess"
# Create a sim module as an empty container
scSim = SimulationBaseClass.SimBaseClass()
# Create the simulation process
dynProcess = scSim.CreateNewProcess(simProcessName)
# Create the dynamics task and specify the integration update time
simulationTimeStep = macros.sec2nano(10.)
dynProcess.addTask(scSim.CreateNewTask(simTaskName, simulationTimeStep))
# Set up the simulation tasks/objects
# Initialize spacecraft object and set properties
scObject = spacecraft.Spacecraft()
scObject.ModelTag = "bsk-Sat"
# Add spacecraft object to the simulation process
scSim.AddModelToTask(simTaskName, scObject)
# Set up Gravity Body
gravFactory = simIncludeGravBody.gravBodyFactory()
planet = gravFactory.createEarth()
planet.isCentralBody = True # ensure this is the central gravitational body
mu = planet.mu
planet.radiusRatio = 0.9966
# Finally, the gravitational body must be connected to the spacecraft object. This is done with
gravFactory.addBodiesTo(scObject)
# Setup spice library for Earth ephemeris
timeInitString = "2000 November 26, 09:30:00.0 TDB"
spiceObject = gravFactory.createSpiceInterface(time=timeInitString, epochInMsg=True)
spiceObject.zeroBase = 'Earth'
scSim.AddModelToTask(simTaskName, spiceObject)
#
# Set up orbit and simulation time
#
# Set up the orbit using classical orbit elements
oe = orbitalMotion.ClassicElements()
rMEO = 11260. * 1000 # meters
# Elliptic MEO case
oe.a = rMEO
oe.e = 0.25
oe.i = 28. * macros.D2R
oe.Omega = 10.5 * macros.D2R
oe.omega = 20.5 * macros.D2R
oe.f = 10. * macros.D2R
rN, vN = orbitalMotion.elem2rv(mu, oe)
# To set the spacecraft initial conditions, the following initial position and velocity variables are set:
scObject.hub.r_CN_NInit = rN # m - r_BN_N
scObject.hub.v_CN_NInit = vN # m/s - v_BN_N
simulationTime = macros.hour2nano(6)
# Set up data logging before the simulation is initialized
numDataPoints = 100
samplingTime = unitTestSupport.samplingTime(simulationTime, simulationTimeStep, numDataPoints)
# create a logging task object of the spacecraft output message at the desired down sampling ratio
dataRec = scObject.scStateOutMsg.recorder(samplingTime)
scSim.AddModelToTask(simTaskName, dataRec)
if vizSupport.vizFound:
viz = vizSupport.enableUnityVisualization(scSim, simTaskName, scObject,
# saveFile=__file__
)
# Del viz.quadMaps[:] at the start of the sim
viz.quadMaps.clear()
# Create Standard Camera
cam = vizSupport.createStandardCamera(viz,
setMode=0,
bodyTarget="earth",
fieldOfView=np.deg2rad(10)
)
# In more complex scenarios with pointing modules, a CameraConfigMsg can be used
# for more precise camera-pointing. See scenarioVizPoint.py for this camera setup demo.
# cam = messaging.CameraConfigMsgPayload()
# cam.cameraID = 1
# cam.sigma_CB = [-0.333333, 0.333333, -0.333333]
# cam.cameraPos_B = [5., 0., 0.]
# cam.fieldOfView = 75.*macros.D2R
# cam.resolution = [2048, 2048]
# cam.skybox = ""
# viz.addCamMsgToModule(messaging.CameraConfigMsg().write(cam))
# Create initial QuadMaps
vizSupport.addQuadMap(viz,
ID=1,
parentBodyName="earth",
vertices=vizSupport.qms.computeRectMesh(planet,
[66.5, 90],
[-180, 180],
8),
color=[0, 0, 255, 100],
label="Arctic Region"
)
vizSupport.addQuadMap(viz,
ID=2,
parentBodyName="earth",
vertices=vizSupport.qms.computeRectMesh(planet,
[37, 41],
[-102.0467, -109.0467],
5),
color=[0, 255, 0, 100],
label="Colorado"
)
vizSupport.addQuadMap(viz,
ID=3,
parentBodyName="bsk-Sat",
vertices=[-2.551, 0.341, 1.01,
-2.804, 0.341, 1.01,
-2.804, 0.111, 1.01,
-2.551, 0.111, 1.01],
color=[255, 128, 0, 100],
label="Solar Cell")
viz.settings.mainCameraTarget = "earth"
# Number of interpolations for camera FOV when generating QuadMaps (helps wrap convex surface)
fieldOfViewSubdivs = 3
# ==================== Run simulation loop ==================== #
# initialize Simulation: This function runs the self_init()
# and reset() routines on each module.
scSim.InitializeSimulation()
camQM_ID = 10
incrementalStopTime = 0
imgTimeStep = macros.min2nano(20)
while incrementalStopTime < simulationTime:
if vizSupport.vizFound:
# Add QuadMap showing camera FOV
FOVBox = vizSupport.qms.computeCamFOVBox(planet, spiceObject, scObject, cam)
# Only draw if all 4 corners intersect Earth!
if len(FOVBox) == 12:
# Subdivide region to wrap onto ellipsoid
if fieldOfViewSubdivs > 1:
FOVBox = vizSupport.qms.subdivideFOVBox(planet, FOVBox, fieldOfViewSubdivs)
vizSupport.addQuadMap(viz,
ID=camQM_ID,
parentBodyName="earth",
vertices=FOVBox,
color=[255, 0, 0, 60]
)
camQM_ID += 1
if incrementalStopTime == simulationTime/2:
vizSupport.addQuadMap(viz,
ID=1,
parentBodyName="earth",
vertices=vizSupport.qms.computeRectMesh(planet,
[66.5, 90],
[-180, 180],
8),
color=[0, 0, 255, 100],
isHidden=True
)
vizSupport.addQuadMap(viz,
ID=2,
parentBodyName="earth",
vertices=vizSupport.qms.computeRectMesh(planet,
[37, 41],
[-102.0467, -109.0467],
5),
color=[255, 255, 0, 100],
label="CO (new color!)"
)
vizSupport.addQuadMap(viz,
ID=3,
parentBodyName="bsk-Sat",
vertices=[-2.551, 0.341, 1.01,
-2.804, 0.341, 1.01,
-2.804, 0.111, 1.01,
-2.551, 0.111, 1.01],
color=[255, 128, 0, 100],
label="NOLABEL")
incrementalStopTime += imgTimeStep
scSim.ConfigureStopTime(incrementalStopTime)
scSim.ExecuteSimulation()
# Empty out QuadMap container so they are only sent once
vizSupport.quadMapList = []
# Unload Spice kernel
gravFactory.unloadSpiceKernels()
return {} # no figures to return
if __name__ == "__main__":
run(False)