Skip to content

Commit 7ef746c

Browse files
authored
Merge pull request #257 from alexander-lam/master
Add Python plugin script examples
2 parents 1f20fb7 + 4fe483e commit 7ef746c

File tree

2 files changed

+373
-0
lines changed

2 files changed

+373
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import typing
2+
import numpy as np
3+
from agi.stk12.plugins.accessconstraintplugin import IAgAccessConstraintPlugin, AgEAccessConstraintObjectType, AgEAccessConstraintDependencyFlags, AgEAccessApparentPositionType
4+
from agi.stk12.plugins.utplugin import IAgUtPluginConfig, AgEUtLogMsgType, AgEUtFrame
5+
from agi.stk12.plugins.attrautomation import AgEAttrAddFlags
6+
from agi.stk12.plugins.stkplugin import AgStkPluginSite
7+
8+
# This plugin determines the lighting condition of the background of an access object.
9+
# The range constraint can be dynamically set if the background is Earth and lit by direct sun,
10+
# background is Earth and unlit (penumbra or umbra), or if the background is deep space.
11+
# Other CBs are not considered. The plugin returns 1 if the range constraint for the specified
12+
# background is satisifed, and 0 if not.
13+
14+
class CAgAccessConstraintPlugin(object):
15+
def __init__(self):
16+
self.scope = None
17+
self.site = None
18+
self.DayRange = 100000000
19+
self.NightRange = 100000000
20+
self.SpaceRange = 100000000
21+
self.a = 6378137
22+
self.b = 6378137
23+
self.c = 6356752.31424
24+
self.sunRadius = 695700000
25+
26+
@property
27+
def DisplayName(self) -> str:
28+
'''
29+
Triggered when the plugin is being registered. This is the name of the constraint used by STK.
30+
The DisplayName property may alternatively be defined as a member attribute (see the STK Python API Programmer's Guide).
31+
'''
32+
return 'COM_PY_RangeLightingConstraint'
33+
34+
def Register(self, result:"IAgAccessConstraintPluginResultRegister") -> None:
35+
'''
36+
Triggered after application start-up, in order to register the constraint for specific STK object pairs for which this constraint is applicable.
37+
'''
38+
result.BaseObjectType = AgEAccessConstraintObjectType.eSensor
39+
result.BaseDependency = AgEAccessConstraintDependencyFlags.eDependencyRelativePosVel + AgEAccessConstraintDependencyFlags.eDependencyPosVel + AgEAccessConstraintDependencyFlags.eDependencyRelSun
40+
result.Dimension = "Unitless"
41+
result.MinValue = 0.5
42+
result.MaxValue = 1.0
43+
result.TargetDependency = AgEAccessConstraintDependencyFlags.eDependencyRelativePosVel + AgEAccessConstraintDependencyFlags.eDependencyPosVel + AgEAccessConstraintDependencyFlags.eDependencyRelSun
44+
result.AddTarget(AgEAccessConstraintObjectType.eSatellite)
45+
result.AddTarget(AgEAccessConstraintObjectType.eAircraft)
46+
result.Register()
47+
48+
result.Message(AgEUtLogMsgType.eUtLogMsgInfo, f'{self.DisplayName}: Register(Sensor to Satellite/Aircraft)')
49+
50+
51+
def Init(self, site:"IAgUtPluginSite") -> bool:
52+
'''
53+
Triggered just before the first computational event trigger.
54+
'''
55+
self.site = AgStkPluginSite(site)
56+
return True
57+
58+
def PreCompute(self, result:"IAgAccessConstraintPluginResultPreCompute") -> bool:
59+
'''
60+
Triggered prior to the calls to the Evaluate method, to allow for any required initialization.
61+
'''
62+
return True
63+
64+
def Evaluate(self, result:"IAgAccessConstraintPluginResultEval", baseData:"IAgAccessConstraintPluginObjectData", targetData:"IAgAccessConstraintPluginObjectData") -> bool:
65+
'''
66+
Triggered when the plugin is evaluated for an access constraint value
67+
'''
68+
# Compute Earth intersection point
69+
posVec = np.array(baseData.Position_Array(AgEUtFrame.eUtFrameFixed))
70+
losVec = np.array(baseData.RelativePosition_Array(AgEAccessApparentPositionType.eLightPathApparentPosition, AgEUtFrame.eUtFrameFixed))
71+
scaleFactor = np.linalg.norm(posVec) / np.linalg.norm(losVec)
72+
losVecScaled = losVec * scaleFactor
73+
D = np.diag([1/self.a**2, 1/self.b**2, 1/self.c**2])
74+
coeffA = np.dot(np.dot(losVecScaled, D), losVecScaled)
75+
coeffB = np.dot(np.dot(posVec, D), losVecScaled)
76+
coeffC = np.dot(np.dot(posVec, D), posVec)-1
77+
discriminant = coeffB**2 - coeffA*coeffC
78+
range = targetData.Range(AgEAccessApparentPositionType.eLightPathApparentPosition)
79+
taus = [(-coeffB + np.sqrt(discriminant))/coeffA, (-coeffB - np.sqrt(discriminant))/coeffA]
80+
if discriminant < 0:
81+
# No intersection exists, we are looking at deep space
82+
result.Value = float((range < self.SpaceRange))
83+
else:
84+
# Intersection exists, determine lighting condition
85+
tau = min(n for n in taus if n>0)
86+
earthIntersect = posVec + losVecScaled * tau
87+
sens2SunVec = np.array(baseData.ApparentSunPosition_Array(AgEUtFrame.eUtFrameFixed))
88+
intersect2SunVec = sens2SunVec + posVec - earthIntersect
89+
intersect2SunVecHat = intersect2SunVec / np.linalg.norm(intersect2SunVec)
90+
intersectNormalHat = np.dot(D,earthIntersect)/np.linalg.norm(np.dot(D,earthIntersect))
91+
sunDistance = np.linalg.norm(intersect2SunVec)
92+
sunDiskAngle = self.sunRadius/sunDistance
93+
sunPosAngle = np.arctan2(np.dot(intersect2SunVecHat, intersectNormalHat), np.linalg.norm(np.cross(intersect2SunVecHat, intersectNormalHat)))
94+
if sunPosAngle > -sunDiskAngle:
95+
result.Value = float(range < self.DayRange)
96+
else:
97+
result.Value = float(range < self.NightRange)
98+
return True
99+
100+
def PostCompute(self, result:"IAgAccessConstraintPluginResultPostCompute") -> bool:
101+
'''
102+
Triggered after the calls to the Evaluate method, to allow for any required clean up.
103+
'''
104+
return True
105+
106+
def Free(self) -> None:
107+
'''
108+
Triggered just before the plugin is destroyed.
109+
'''
110+
del(self.scope)
111+
del(self.site)
112+
del(self.DayRange)
113+
del(self.NightRange)
114+
del(self.a)
115+
del(self.b)
116+
del(self.c)
117+
118+
def GetPluginConfig(self, pAttrBuilder:"IAgAttrBuilder") -> typing.Any:
119+
'''
120+
Get an attribute container of the configuration settings.
121+
'''
122+
if self.scope is None:
123+
self.scope = pAttrBuilder.NewScope()
124+
pAttrBuilder.AddQuantityMinDispatchProperty2(self.scope, "DayRange", "Range of sensor when background is lit Earth", "DayRange", "Distance", "km", "m", 0.0, AgEAttrAddFlags.eAddFlagNone)
125+
pAttrBuilder.AddQuantityMinDispatchProperty2(self.scope, "NightRange", "Range of sensor when background is unlit Earth", "NightRange", "Distance", "km", "m", 0.0, AgEAttrAddFlags.eAddFlagNone)
126+
pAttrBuilder.AddQuantityMinDispatchProperty2(self.scope, "SpaceRange", "Range of sensor when background is space", "SpaceRange", "Distance", "km", "m", 0.0, AgEAttrAddFlags.eAddFlagNone)
127+
return self.scope
128+
129+
def VerifyPluginConfig(self, pPluginCfgResult:"IAgUtPluginConfigVerifyResult") -> None:
130+
'''
131+
Verify the Plugin Config
132+
'''
133+
pass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
# This implements the Venus GRAM in STK under the GRAM common framework.
2+
# This plugin was most recently tested with GRAM 2.1.0.
3+
# This plugin requires the built GRAMpy module to interface with the GRAM suite.
4+
# A copy of the GRAM Suite can be obtained through the NASA Technology Transfer Program.
5+
6+
import typing, math
7+
from agi.stk12.plugins.utplugin import AgEUtLogMsgType
8+
from agi.stk12.plugins.attrautomation import AgEAttrAddFlags
9+
from agi.stk12.plugins.stkplugin import AgStkPluginSite
10+
from agi.stk12.vgt import AgECrdnSmartEpochState
11+
from GRAMpy import gram
12+
13+
class CAgAsDensityModelPlugin(object):
14+
def __init__(self):
15+
# Debug flags
16+
self.DebugMode = False
17+
self.PluginEnabled = True
18+
self.MessageInterval = 1000
19+
self.MsgCntr = -1
20+
self.IntegSteps = 0
21+
22+
# INPUTS
23+
self.CentralBodyName = 'Venus'
24+
self.LowestValidAltKm = 0
25+
self.SpicePath = r'\SPICE'
26+
self.InitialRandomSeed = 1001
27+
self.DensityPerturbationScale = 1.0
28+
self.MinimumRelativeStepSize = 0.0
29+
self.FastModeOn = 0
30+
self.DensityType = 'Mean'
31+
32+
# STK specific, do not change
33+
self.Scope = None
34+
self.Site = None
35+
self.Root = None
36+
self.Density = None
37+
self.DisplayName = 'VenusGRAM Suite Atmosphere Plugin'
38+
self.OutputLowAltMsg = True
39+
self.ComputingTemperature = True
40+
self.ComputingPressure = True
41+
42+
# GRAM specific, do not change
43+
self.venus = None
44+
self.position = None
45+
46+
def Init(self, Site:"IAgUtPluginSite") -> bool:
47+
self.Site = AgStkPluginSite(Site)
48+
self.Root = self.Site.StkRootObject
49+
50+
# Parse scenario epoch
51+
self.Root.UnitPreferences.SetCurrentUnit('DateFormat', 'YYYY:MM:DD') # YYYY:MM:DD:HH:MM:SS.sss Time
52+
smartEpoch = self.Root.CurrentScenario.AnalysisEpoch
53+
if smartEpoch.State == AgECrdnSmartEpochState.eCrdnSmartEpochStateExplicit:
54+
epoch = smartEpoch.TimeInstant
55+
else:
56+
epoch = smartEpoch.ReferenceEvent.FindOccurrence().Epoch
57+
epochSplit = epoch.split(':')
58+
self.Root.UnitPreferences.SetCurrentUnit('DateFormat', 'UTCG')
59+
60+
# Set up GRAM
61+
inputParameters = gram.VenusInputParameters()
62+
inputParameters.timeFrame = gram.PET
63+
inputParameters.spicePath = self.SpicePath
64+
inputParameters.initialRandomSeed = self.InitialRandomSeed
65+
inputParameters.densityPerturbationScale = self.DensityPerturbationScale
66+
inputParameters.minRelativeStepSize = self.MinimumRelativeStepSize
67+
inputParameters.fastModeOn = self.FastModeOn
68+
self.venus = gram.VenusAtmosphere()
69+
self.venus.setInputParameters(inputParameters)
70+
71+
# Configure GRAM start time and position
72+
self.position = gram.Position()
73+
ttime = gram.GramTime()
74+
ttime.setStartTime(int(epochSplit[0]), int(epochSplit[1]), int(epochSplit[2]), int(epochSplit[3]), int(epochSplit[4]), float(epochSplit[5]), gram.UTC, gram.PET)
75+
self.venus.setStartTime(ttime)
76+
77+
if self.DebugMode:
78+
self.Site.Message(AgEUtLogMsgType.eUtLogMsgDebug, "VenusGRAMPlugin:Init()")
79+
if self.PluginEnabled:
80+
self.Site.Message(AgEUtLogMsgType.eUtLogMsgInfo, "VenusGRAMPlugin:Init() Enabled")
81+
else:
82+
self.Site.Message(AgEUtLogMsgType.eUtLogMsgInfo, "VenusGRAMPlugin:Init() Disabled because Enabled flag is False")
83+
if not self.PluginEnabled:
84+
self.Site.Message(AgEUtLogMsgType.eUtLogMsgAlarm, "VenusGRAMPlugin:Init() Disabled because Enabled flag is False")
85+
return self.PluginEnabled
86+
87+
def Register(self, Result:"IAgAsDensityModelResultRegister"):
88+
if self.DebugMode:
89+
Result.Message(AgEUtLogMsgType.eUtLogMsgDebug, "VenusGRAMPlugin:Register()")
90+
91+
def Free(self) -> bool:
92+
self.Site = None
93+
return True
94+
95+
# PreCompute is a member of the optional IAgAsDensityModelPluginExtended interface
96+
def PreCompute(self, Result:"IAgAsDensityModelResult") -> bool:
97+
self.IntegSteps = 0
98+
self.MsgCntr = -1
99+
if self.PluginEnabled:
100+
self.Site.Message(AgEUtLogMsgType.eUtLogMsgDebug, "PreCompute() called.")
101+
102+
if self.DebugMode:
103+
self.TestResultInterface("PreCompute()", Result)
104+
105+
return True
106+
return False
107+
108+
# PreNextStep is a member of the optional IAgAsDensityModelPluginExtended interface
109+
def PreNextStep(self, Result:"IAgAsDensityModelResult") -> bool:
110+
if self.PluginEnabled:
111+
RptCnt = self.MessageInterval
112+
if self.DebugMode:
113+
if self.IntegSteps % RptCnt == 0:
114+
self.Site.Message(AgEUtLogMsgType.eUtLogMsgDebug, f'PreNextStep(): Integration Step: {self.IntegSteps}')
115+
if self.IntegSteps == 0:
116+
self.TestResultInterface("PreNextStep()", Result)
117+
118+
self.IntegSteps += 1
119+
120+
return True
121+
return False
122+
123+
# PostCompute is a member of the optional IAgAsDensityModelPluginExtended interface
124+
def PostCompute(self, Result:"IAgAsDensityModelResult") -> bool:
125+
if self.PluginEnabled:
126+
self.Site.Message(AgEUtLogMsgType.eUtLogMsgDebug, "PostCompute() called.")
127+
if self.DebugMode:
128+
self.TestResultInterface("PostCompute()", Result)
129+
130+
return True
131+
return False
132+
133+
def Evaluate(self, ResultEval:"IAgAsDensityModelResultEval") -> bool:
134+
self.MsgCntr += 1
135+
if self.PluginEnabled:
136+
self.PluginEnabled = self.SetDensity(ResultEval)
137+
if self.DebugMode and self.MsgCntr == 0:
138+
self.Site.Message(AgEUtLogMsgType.eUtLogMsgDebug, "Evaluate() called.")
139+
self.TestResultEvalInterface("Evaluate()", ResultEval)
140+
141+
return self.PluginEnabled
142+
143+
def CentralBody(self) -> str:
144+
return self.CentralBodyName
145+
146+
def ComputesTemperature(self) -> bool:
147+
return self.ComputingTemperature
148+
149+
def ComputesPressure(self) -> bool:
150+
return self.ComputingPressure
151+
152+
def UsesAugmentedSpaceWeather(self) -> bool:
153+
return False
154+
155+
def GetLowestValidAltitude(self) -> bool:
156+
return self.LowestValidAltKm
157+
158+
def OverrideAtmFluxLags(self, FluxLags: "IAgAsDensityModelPLuginAtmFluxLagsConfig") -> bool:
159+
return True
160+
161+
def SetDensity(self, ResultEval:"IAgAsDensityModelResultEval") -> bool:
162+
# Query STK for time and LLA
163+
stkTime = float(ResultEval.DateString('EpSec'))
164+
LLA = ResultEval.LatLonAlt_Array()
165+
LatitudeDeg = math.degrees(LLA[0])
166+
LongitudeDeg = math.degrees(LLA[1])
167+
AltitudeKm = LLA[2] / 1000
168+
169+
# Validate altitude
170+
if AltitudeKm < self.LowestValidAltKm:
171+
if self.OutputLowAltMsg:
172+
msg = f'setDensity: altitude {AltitudeKm:1.6f} is less than minimum valid altitude ( {self.LowestValidAltKm} km ). Keeping density constant below this height.'
173+
self.Site.Message(AgEUtLogMsgType.eUtLogMsgWarning, f'{msg}')
174+
self.OutputLowAltMsg = False
175+
AltitudeKm = self.LowestValidAltKm
176+
177+
if self.DebugMode:
178+
if self.MsgCntr % self.MessageInterval == 0:
179+
msg = f'setDensity: time = {stkTime:02.6f} EpSec'
180+
self.Site.Message(AgEUtLogMsgType.eUtLogMsgDebug, f'{msg}')
181+
msg = f'setDensity: lat = {LatitudeDeg:1.6f}, lon = {LongitudeDeg:1.6f}, alt = {AltitudeKm:1.6f}'
182+
self.Site.Message(AgEUtLogMsgType.eUtLogMsgDebug, f'{msg}')
183+
184+
# Set GRAM position
185+
self.position.elapsedTime = stkTime
186+
self.position.latitude = LatitudeDeg
187+
self.position.longitude = LongitudeDeg
188+
self.position.height = AltitudeKm
189+
self.venus.setPosition(self.position)
190+
191+
# Update GRAM atmosphere
192+
self.venus.update()
193+
atmos = self.venus.getAtmosphereState()
194+
if self.DensityType == 'Mean':
195+
self.Density = atmos.density
196+
elif self.DensityType == 'Low':
197+
self.Density = atmos.lowDensity
198+
elif self.DensityType == 'High':
199+
self.Density = atmos.highDensity
200+
else:
201+
self.Density = atmos.perturbedDensity
202+
203+
# Push result back to STK
204+
ResultEval.SetDensity(self.Density)
205+
ResultEval.SetPressure(atmos.pressure)
206+
ResultEval.SetTemperature(atmos.temperature)
207+
208+
if self.DebugMode:
209+
if self.MsgCntr % self.MessageInterval == 0:
210+
msg = f'setDensity: density {self.Density}'
211+
self.Site.Message(AgEUtLogMsgType.eUtLogMsgDebug, f'{msg}')
212+
return True
213+
214+
def TestResultInterface(self, Name:"str", Result:"IAgAsDensityModelResult"):
215+
if not self.DebugMode:
216+
return
217+
return
218+
219+
def TestResultEvalInterface(self, Name:"str", ResultEval:"IAgAsDensityModelResultEval"):
220+
if not self.DebugMode:
221+
return
222+
return
223+
224+
def GetPluginConfig(self, pAttrBuilder:"IAgAttrBuilder") -> typing.Any:
225+
if self.Scope is None:
226+
self.Scope = pAttrBuilder.NewScope()
227+
pAttrBuilder.AddStringDispatchProperty( self.Scope, "CentralBodyName", "CentralBodyName", "CentralBodyName", AgEAttrAddFlags.eAddFlagReadOnly)
228+
pAttrBuilder.AddStringDispatchProperty( self.Scope, "SpicePath", "SpicePath", "SpicePath", AgEAttrAddFlags.eAddFlagNone )
229+
pAttrBuilder.AddIntMinMaxDispatchProperty( self.Scope, "InitialRandomSeed", "InitialRandomSeed", "InitialRandomSeed", 1, 29999, AgEAttrAddFlags.eAddFlagNone )
230+
pAttrBuilder.AddDoubleMinMaxDispatchProperty( self.Scope, "DensityPerturbationScale", "DensityPerturbationScale", "DensityPerturbationScale", 0.0, 2.0, AgEAttrAddFlags.eAddFlagNone )
231+
pAttrBuilder.AddDoubleMinMaxDispatchProperty( self.Scope, "MinimumRelativeStepSize", "MinimumRelativeStepSize", "MinimumRelativeStepSize", 0.0, 1.0, AgEAttrAddFlags.eAddFlagNone )
232+
pAttrBuilder.AddBoolDispatchProperty( self.Scope, "FastModeOn", "FastModeOn", "FastModeOn", AgEAttrAddFlags.eAddFlagNone )
233+
pAttrBuilder.AddChoicesDispatchProperty( self.Scope, "DensityType", "Density Type", "DensityType", ["Low", "Mean", "High", "Perturbed"] )
234+
return self.Scope
235+
236+
def VerifyPluginConfig(self, pPluginCfgResult:"IAgUtPluginConfigVerifyResult") -> bool:
237+
pPluginCfgResult.Result = True
238+
pPluginCfgResult.Message = "Ok"
239+
return True
240+

0 commit comments

Comments
 (0)