STK组件在整个库中广泛使用“求值器模式”。STK组件中的任何计算几乎都使用了求值器模式。求值器通常表示时间的函数,即输入时间获得输出的特定类型的值。
1. 求值器(Evaluator
)的使用
通过定义对象创建求值器,定义对象时并不进行任何计算。先建模配置一个对象,然后获得求值器,最后使用该求值器进行与该对象相关的计算。
例如,AxesLinearRate
表示一组以线性速率旋转的坐标轴,我们可以构造一个此类型的实例,它以相对于J2000坐标轴的恒定速率旋转:
AxesLinearRate axes = new AxesLinearRate
{
ReferenceAxes = CentralBodiesFacet.GetFromContext().Earth.J2000Frame.Axes,
ReferenceEpoch = TimeConstants.J2000,
InitialRotation = UnitQuaternion.Identity,
InitialRotationalVelocity = 0.1,
RotationalAcceleration = 0.0,
SpinAxis = new UnitCartesian(1.0, 0.0, 0.0)
};
这个坐标轴在不同时间相对于J2000坐标轴具有不同的方向。为了求解在给定时间从J2000坐标轴到此坐标轴的旋转,我们首先需要获得一个求值器:
AxesEvaluator evaluator = axes.GetEvaluator();
然后我们可以通过求值器求解不同时间的值:
JulianDate dateToEvaluate = new JulianDate(new GregorianDate(2007, 11, 20, 12, 0, 0));
UnitQuaternion rotationFromJ2000 = evaluator.Evaluate(dateToEvaluate);
在上述例子中,通过调用定义对象的方法来创建求值器。如果是简单的对象,可使用GetEvaluator
方法即可。如果是复杂的对象(例如CentralBody
),则有多个返回不同求值器的方法。
在获取求值器后,对定义对象的更改不会影响求值器计算的结果。如果要在更改定义对象的属性后得到新结果,则需重新获取求值器,例如:
AxesLinearRate axes = new AxesLinearRate
{
ReferenceAxes = CentralBodiesFacet.GetFromContext().Earth.J2000Frame.Axes,
ReferenceEpoch = TimeConstants.J2000,
InitialRotation = UnitQuaternion.Identity,
InitialRotationalVelocity = 0.1,
RotationalAcceleration = 0.0,
SpinAxis = new UnitCartesian(1.0, 0.0, 0.0)
};
AxesEvaluator evaluator = axes.GetEvaluator();
// 求解指定时间的旋转.
JulianDate dateToEvaluate = new JulianDate(new GregorianDate(2007, 11, 20, 12, 0, 0));
UnitQuaternion rotationFromJ2000 = evaluator.Evaluate(dateToEvaluate);
// 旋转轴由X轴改为Y轴.
axes.SpinAxis = new UnitCartesian(0.0, 1.0, 0.0);
// 求解相同时间的旋转.
UnitQuaternion sameRotationFromJ2000 = evaluator.Evaluate(dateToEvaluate);
// rotationFromJ2000和sameRotationFromJ2000是相同的,
// 尽管对象的旋转轴已经改变,求值器不受对象改变的影响.
// 重新获取求值器并求解.
evaluator = axes.GetEvaluator();
UnitQuaternion differentRotationFromJ2000 = evaluator.Evaluate(dateToEvaluate);
// 此时rotationFromJ2000和differentRotationFromJ200不相同,反映了旋转轴的变化.
所有求值器都实现了IThreadAware
接口,这意味着求值器可在多线程中安全的使用。检查IsThreadSafe
属性以确定该求值器是否线程安全,如果求值器不是线程安全,则要使用CopyForAnotherThread.Copy<T>
为每个使用的线程复制求值器副本。
2. 求值组(EvaluatorGroup
)的使用
EvaluatorGroup
通过消除冗余计算,可以更有效地求解求值器。
一个求值器通常会使用一个或多个其它求值器来进行计算。例如,CentralBody.ObserveCartographicPoint
返回一个求值器,该求值器计算给定时间给定Point
相对于中心天体的经度、纬度和高度,为了做到这一点,它必须在中心天体的FixedFrame
中找到该点的位置,然后将Cartesian
位置转换为Cartographic
位置。这需要求解一个PointEvaluator
,如果Point
不是在FixedFrame
中,那么还需要计算在给定时间坐标系之间的转换。
通常,这些内嵌的求值器在多个上层求值器之间共享。例如,如果您在计算两颗SGP4外推器的卫星的Cartographic
位置,那么必须为两颗卫星完成从SGP4外推器的坐标系到地固坐标系的转换。如果两个求值器的求解时间相同,那么计算两次坐标系转换将是低效的。
EvaluatorGroup
提供了一种自动消除此类冗余计算的机制。在为每颗卫星获得求值器时,将相同的EvaluatorGroup
传递给CentralBody.ObserveCartographicPoint
方法,两个求值器的任何公共计算部分将只计算一次。
Point point1 = CreateSatellite1Point();
Point point2 = CreateSatellite2Point();
EarthCentralBody earth = CentralBodiesFacet.GetFromContext().Earth;
EvaluatorGroup group = new EvaluatorGroup();
MotionEvaluator<Cartographic> evaluator1 = earth.ObserveCartographicPoint(point1, group);
MotionEvaluator<Cartographic> evaluator2 = earth.ObserveCartographicPoint(point2, group);
// 在使用求值组后更新每个求值器的引用.
// 这将通过消除冗余计算来优化求值器.
evaluator1 = group.UpdateReference(evaluator1);
evaluator2 = group.UpdateReference(evaluator2);
// evaluator1和evaluator2将共享冗余计算.
仅在两个求值器求解同一时刻时的值才有用,如果是求解两个求值器在不同时刻的值,放到同一个
EvaluatorGroup
中不会带来任何好处,甚至可能会有很小的性能损失。
3. 实现自定义的求值器
以下讨论详细介绍了实现自定义求值器所涉及的各个部分。它讨论了用户应该实现的各种抽象和虚方法,它们做了什么以及需要注意哪些事项以确保良好的性能和线程安全性。提供代码样本,使用户可以获得典型求值器的完整方式。最后用户应该创建自定义实现所需的所有代码,这些实现可以将用户的算法无缝集成到其余的STK组件中。
3.1. 求值器构造
库中的大多数定义对象类(例如Point、AccessConstraint
等)都有一个抽象方法,负责为对象生成Evaluator
。除了作为相应Evaluator
的工厂方法之外,该方法还负责处理EvaluatorGroup
。EvaluatorGroup
用于在创建Evaluator
时消除冗余计算,并可通过确定哪些Evaluators
应缓存其先前值来优化性能。
在下面的示例中,GetEvaluator1
方法在EvaluatorGroup
上调用CreateEvaluator
。如果已在组中创建具有给定参数this
对象实例的Evaluator
,则返回预先存在的实例,并且永远不会调用CreateEvaluator1
实例方法。否则它调用CreateEvaluator1
用于创建Evaluator
的实例方法。请注意,“内部求值器”都是在CreateEvaluator
方法中构造的,这是为了避免在求值器已存在的情况下进行额外的调用(因为必要的内部求值器已经存在)。
public Evaluator<double> GetEvaluator1(EvaluatorGroup group, Point point, IList<Scalar> scalars)
{
return group.CreateEvaluator<Evaluator<double>, Point, IList<Scalar>, double, IList<int>>(
CreateEvaluator1, point, scalars, m_propertyOne, m_propertyList);
}
// 在此方法内创建所有的内部求值器,以便仅调用它们一次.
// 确保参数和相关类实例一起指定一个唯一键来标识组中的求值器.
private Evaluator<double> CreateEvaluator1(EvaluatorGroup group, Point point, IList<Scalar> scalars, double argumentOne, IList<int> argumentList)
{
PointEvaluator pointEvaluator = point.GetEvaluator(group);
List<MotionEvaluator<double>> scalarEvaluators = new List<MotionEvaluator<double>>(scalars.Count);
foreach (Scalar scalar in scalars)
{
scalarEvaluators.Add(scalar.GetEvaluator(group));
}
Ellipsoid immutableEarth = CentralBodiesFacet.GetFromContext().Earth.Shape;
// 确保复制所有列表以保证它们不能再求值器之外修改
argumentList = new List<int>(argumentList);
return new SampleEvaluator(group, pointEvaluator, scalarEvaluators, argumentOne, immutableEarth, argumentList);
}
在构造Evaluator
时,请确保Evaluator
中存储的所有成员都是其它Evaluator
类型、值类型或不可变引用类型。求值器永远不要使用可以在求值器外部进行更改的引用类型来构造,从而影响计算。只要在Evaluator
之外没有对List的引用,并且在求解期间以线程安全的方式处理List,不可变类型的List是安全的。
// 根据给定的内部求值器初始化一个新实例.
// 注意必须保证参数不会在求值器之后发生变化.
// 否则在求值组中使用时,此Evaluator的行为将不正确.
public SampleEvaluator(EvaluatorGroup group,
MotionEvaluator<Cartesian> innerEvaluatorOne,
IList<MotionEvaluator<double>> innerEvaluatorList,
double argumentOne,
Ellipsoid argumentTwo,
IList<int> argumentList)
: base(group)
{
m_innerEvaluatorOne = innerEvaluatorOne;
m_innerEvaluatorList = innerEvaluatorList;
m_argumentList = argumentList;
m_argumentOne = argumentOne;
m_immutableShape = argumentTwo;
}
private readonly double m_argumentOne;
private readonly Ellipsoid m_immutableShape;
private readonly IList<int> m_argumentList;
private MotionEvaluator<Cartesian> m_innerEvaluatorOne;
private readonly IList<MotionEvaluator<double>> m_innerEvaluatorList;
private bool m_isDisposed;
3.2. 报告线程安全
每个Evaluator
都实现了IThreadAware
接口。在生成新的计算线程时,这用于确保正确更新或复制Evaluator
及其数据以确保线程安全。如果IsThreadSafe
返回false
,则复制Evaluator
,通常使用CopyForAnotherThread
实例。如果IsThreadSafe
为true
,则在任何其它线程中使用现有实例。请注意,在IsThreadSafe
中应当先检查所有实现了IThreadAware
接口的对象的线程安全,然后再返回值。另外,当所有求值器构造完成后执行优化性能时,EvaluatorGroup
将使用UpdateEvaluatorReferences
方法。
在复制构造函数中,CopyContext
用于更新值。某些context
(例如CopyForAnotherThread
)将对自己实现ICloneWithContext
的所有成员执行“深层复制”。这个context
也会被用来优化求值器,通过使用这些实例的优化版本替换原实例的方式。因此,当用户将自定义数据结构用作Evaluator
中的数据,则它们至少应该实现ICloneWithContext
接口。如果有疑惑,最安全的方式是明确的调用Clone
方法,而不是简单的更新成员数据。如果用户自定义数据不能跨线程共享,只要求值器处理了数据复制,就没必要对用户自定义数据实现ICloneWithContext
接口了。
// 复制构造函数,它根据给定的上下文更新对象引用
// 在多线程中使用时,求值器的复制非常重要
private SampleEvaluator(SampleEvaluator existingInstance, CopyContext context)
: base(existingInstance, context)
{
m_argumentOne = existingInstance.m_argumentOne;
m_immutableShape = context.UpdateReference(existingInstance.m_immutableShape);
// 如果此求值器会修改这个List则复制它.
// 否则只需更新引用即可.
m_argumentList = context.UpdateReference(existingInstance.m_argumentList);
m_innerEvaluatorOne = existingInstance.m_innerEvaluatorOne;
m_innerEvaluatorList = existingInstance.m_innerEvaluatorList;
UpdateEvaluatorReferences(context);
}
// 此方法用于在优化求值组的依赖关系时,更新求值器之间的引用。
// 这样可以确保在给定的时间只执行一次每个唯一的计算。
public override void UpdateEvaluatorReferences(CopyContext context)
{
m_innerEvaluatorOne = context.UpdateReference(m_innerEvaluatorOne);
EvaluatorHelper.UpdateCollectionReferences(m_innerEvaluatorList, context);
}
// 克隆此求值器
public override object Clone(CopyContext context)
{
return new SampleEvaluator(this, context);
}
// 此属性指示此求值器是否包含任何非线程安全的操作。
// 如果存在任何文件IO、共享集合或者任何内部求值器不是线程安全的,
// 则需要先复制该求值器,然后再在另一个线程中使用。
public override bool IsThreadSafe
{
get
{
return m_innerEvaluatorOne.IsThreadSafe &&
EvaluatorHelper.AllEvaluatorsAreThreadSafe(m_innerEvaluatorList);
}
}
3.3. 数据有效时间
由于大多数求值器都是由内部求值器组成,了解求值器的有效时间非常重要。如果一个求值器包含一个有限数据(例如从一个StkEphemerisFile
创建的求值器)的内部求值器,那么上层求值器需要一种向用户报告这一点的方法。IAavilability
接口用来处理这个问题。
// 此方法检查此计算是否可以在给定时间生成有效值。
// 例如,如果某个求值器拥有一定时间跨度的星历数据,
// 则在没有数据的时间段内,求值器不可用。
public override bool IsAvailable(JulianDate date)
{
return EvaluatorHelper.AllEvaluatorsAreAvailable(date, m_innerEvaluatorOne) &&
EvaluatorHelper.AllEvaluatorsAreAvailable(date, m_innerEvaluatorList);
}
// 此方法生成此求值器可以生成有效值的时间间隔。
public override TimeIntervalCollection GetAvailabilityIntervals(TimeIntervalCollection consideredIntervals)
{
consideredIntervals = EvaluatorHelper.GetAvailabilityIntervals(consideredIntervals, m_innerEvaluatorOne);
consideredIntervals = EvaluatorHelper.GetAvailabilityIntervals(consideredIntervals, m_innerEvaluatorList);
return consideredIntervals;
}
3.4. 常量求值器
某些求值器总是返回固定值,通过检查IIsTimeVarying
接口可以消除额外调用来优化性能。将此值设置为false
时应小心,即使所有内部求值器都返回false
,如果当前的求值器跟时间相关,那么IsTimeVarying
也应该返回true
。求值器可以显式返回false
的唯一时机是它根本不使用时间参数,包括将时间提供给内部求值器。如果不确定,返回true
总是安全的,因为以性能为代价保证结果的正确。
// 此属性指示此求值器生成的值是否独立于时间,
// 在这种情况下,记录一个常量值而不是连续重新求解。
public override bool IsTimeVarying
{
// 如果"Evaluate" 方法在计算中不使用时间,除非将其传递给内部求值器,
// 那么只需返回:m_innerEvaluatorOne.IsTimeVarying
// 否则返回:true
get { return true; }
}
3.5. 求值器的释放
大多数求值器都包含Dispose
方法,以确保及时释放对系统资源的任何引用,确保释放任何本地资源,并对任何内部求值器调用Dispose
。
// 当不再使用此求值器时,它会调用所有内部求值器上的Dispose,
// 以确保及时是否资源
protected override void Dispose(bool disposing)
{
if (m_isDisposed || !disposing)
return;
m_isDisposed = true;
m_innerEvaluatorOne.Dispose();
EvaluatorHelper.Dispose(m_innerEvaluatorList);
}
3.6. 性能增强
为了使求值器能够以一种灵活的方式被创建,许多求值器被串在一起,组成计算的层次结构。其中大部分计算是共享的(例如地固系到惯性系的转换)。求值缓存允许这些复杂的计算保持高效,尽管它们很复杂。如果在两个或多个求值器之间共享一个求值器,则求值组将调用以下方法来创建缓存包装器,以替换所有使用它的其他求值器中的现有求值器。这是一个可选方法,因为大多数求值器在其基类中都包含一个默认的缓存包装器。但是,如果用户需要特定类型的求值器(例如SampleEvaluator
),而不是基类的实例,则用户需要实现正确类型的缓存包装器,否则它将抛出一个异常。通常这不是问题,因为定义的对象类型定义了返回基求值器实例的GetEvaluator
方法。
// 如果求值组确定有两个或两个以上的求值器使用这个求值器,
// 那么这个求值器的Evaluate方法的计算结果被缓存,只计算一次。
// 如果你不想有任何其它求值器来缓存这个求值器的结果,
// 那么简单的返回此实例,而不是缓存包装器
public override IEvaluator GetCachingWrapper()
{
return new CachingEvaluator<double>(this);
}
当然,如果用户希望确保不为特定的求值器发生缓存,则可以重载此方法以简单地返回现有实例。
3.7. 求解算法
实现求值器的核心是求解的计算算法。如果求值器的其余部分都是仔细实现,并且IsThreadSafe
返回正确的值,则不需要执行任何锁操作以确保线程安全。但是由于为增强性能的缓存的存在,给定时间的返回值永远不会变化,否则缓存将不会反映值的变化。
// 这是求值器进行计算的地方。
// 实现时需要注意以下几点:
// 输入相同时间时,结果应始终相同。
// 避免调用不可变的对象。
// 线程安全由用户处理,除非用户添加了任何特定的非线程安全操作,
// 在这种情况下,用户应该修改IsThreadSafe属性以返回false
public override double Evaluate(JulianDate date)
{
double maximum = 0;
for (int i = 0; i < m_innerEvaluatorList.Count; i++)
{
// MotionEvaluators运行你获得导数...
Motion<double> scalarResult = m_innerEvaluatorList[i].Evaluate(date, 1);
// 但是不能保证返回请求的阶数,因此请务必检查
if (scalarResult.Order > 0 && scalarResult.FirstDerivative > maximum)
{
maximum = scalarResult.Value;
}
}
Cartographic position = m_immutableShape.CartesianToCartographic(m_innerEvaluatorOne.Evaluate(date));
return position.Height * Math.Min(maximum, m_argumentOne);
}
3.8. 求值器主题的变化
有几个求值器在STK组件中提供稍微不同的功能。
MotionEvaluator<T>
添加了一种方法,可以选择更改过境和其它计算中使用的采样测量。它还将阶数参数带到其MotionEvaluator<T>.Evaluate
方法中,这是对要计算的导数阶数的可选项。虽然求值器可以不根据请求的阶数计算结果,但是指定一个小于有效阶数的值,可以让求值器不执行无关计算。 AccessConstraintEvaluator
是MotionEvaluator<T>
特定类型的实现的一个很好的例子。
// 可选的重写
public override JulianDate GetNextSampleSuggestion(JulianDate date)
{
return date.AddSeconds(m_stepsize);
}
// 可选的重写
public override double Evaluate(JulianDate date)
{
return Evaluate(date, 0).Value;
}
// 必须的重写
public override Motion<double> Evaluate(JulianDate date, int order)
{
Motion<Cartesian> state = m_innerEvaluator.Evaluate(date, order + 1);
double[] result = new double[state.Order - 1];
for (int i = 0; i < state.Order - 1; i++)
{
result[i] = state[i].Magnitude * m_argumentOne;
}
return new Motion<double>(result);
}
几何类型添加了一个附加属性,用于指定如何定义原始数据。例如PointEvaluator
添加了一个属性来指定在那些时间内定义该点的坐标系(ReferenceFrame
),因此可以在不必重新创建点的情况下,将点的定义从一个坐标系转换为另一个坐标系。
// 必须的重写
public override TimeIntervalCollection<ReferenceFrame> DefinedInIntervals
{
get { return CreateConstantDefinedIn(m_definedInFrame); }
}
同样,AxesEvaluator
具有类似的属性,该属性指定给定轴在不同时间的定义轴。请注意,从AxesEvaluator
返回的任何角速率都应在定义轴中表示。