STK组件模式:求值器Evaluator

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的工厂方法之外,该方法还负责处理EvaluatorGroupEvaluatorGroup用于在创建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实例。如果IsThreadSafetrue,则在任何其它线程中使用现有实例。请注意,在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方法中,这是对要计算的导数阶数的可选项。虽然求值器可以不根据请求的阶数计算结果,但是指定一个小于有效阶数的值,可以让求值器不执行无关计算。 AccessConstraintEvaluatorMotionEvaluator<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返回的任何角速率都应在定义轴中表示。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容