我们首先讨论整个核心API中使用的基本功能。然后我们讨论如何使用API处理CellML元数据和导入。我们讨论了API使用的对象模型和内存管理方案,并继续依次讨论每个扩展API。然后,我们讨论了用于测试API实现的套件,并在本节结束时与其他具有类似功能的软件进行了比较。
核心API
核心API的范围是对CellML模型内容的基本操作和访问。API中的信息检索工具与CellML文档中的XML元素的排列密切相关。核心API的IDL规范可以在CellML API源树的文件接口/CellML APISPEC.IDL中找到。
文档中的每个CellML元素都有一个对象。这些对象实现了一个特定于CellML元素类型的接口。这些元素的接口都继承(直接或间接)自CellMLElement接口。此接口提供了对所有元素都有用的功能。例如,它可以插入或删除相关元素的任何子元素,并针对元素设置由唯一键标识的临时用户数据注释。这些注释不构成内存中DOM表示的一部分,因此,例如,当模型序列化时,不会出现在生成的XML中。
具有强制名称属性的CellML元素的接口都继承自NamedCellMLElement接口。该接口提供一个name属性(可以提取或设置),并从CellMLElement接口继承。例如,CellMLComponent继承自NamedCellMLElement,因为CellML规范要求所有组件元素都具有name属性。
对于CellML规范允许的每种类型的子CellML-元素,父元素的接口都有一个只读属性,用于检索该类型的所有CellMLElements。返回的集合实现特定于预期元素类型的接口。例如,组件元素可以包含变量元素,因此CellMLComponent接口有一个名为variables的属性,类型为CellMLVariableSet。
这些特定类型的集合遵循与集合中元素对象的继承层次结构平行的继承层次。每个集合接口都有一个对应的迭代器接口,它允许按顺序获取每个对象。因为迭代器接口特定于要获取的对象,所以会返回所需的接口,从而无需调用QueryInterface(有关QueryInterface的信息,请参阅对象模型和内存管理部分)。然而,也可以使用不太特定的(接口继承层次结构中的祖先)集合接口来检索不太特定(祖先)的迭代器对象(例如,用于处理多种类型元素的泛型代码)。
从NamedCellMLElementIterator派生的迭代器还提供了按名称提取元素的接口。所有子代迭代器接口都提供更具体的按名称提取操作。
集合接口还提供了通过插入CellML元素来修改相关集合的工具。因为顺序对模型的含义并不重要,所以迭代和插入工具无法控制模型中元素的实际顺序。
CellML大量使用XML中的命名空间工具[22]. CellML提供了扩展元素,也就是说,不在常见的CellML、MathML或RDF名称空间中的元素。CellMLElement接口提供ExtensionElementList类型的属性extensionElement。ExtensionElementList接口允许检查和操作不在CellML命名空间中的DOM元素。
处理CellML元数据
此外,CellML模型通常包含元数据[23]以RDF/XML编码[24]. 有许多不同的方法来处理以RDF/XML编码的RDF数据。Model接口提供了一个名为getRDFRepresentation的操作,该操作使用类型URI来描述所请求的RDF表示的类型。这些表示提供了从RDFRepresentation接口派生的接口。API要求所有实现至少提供一个以字符串形式提供序列化RDF/XML文档的实现,以及一个以单个RDF/XML形式为数据提供DOM文档节点的实现。为了生成这些RDF/XML输出,实现需要将文档中找到的几个不同RDF片段合并到单个RDF/XML文档中。应用程序还可以修改RDF/XML并将其推回到模型中。
我们对CellML API的实现还提供了一个接口,允许访问和修改RDF三元组[25]在模型中找到。
处理CellML 1.1中的模型导入
CellML 1.1提供从其他模型导入模型的组件和物理单元[13]. 导入是通过添加导入元素来创建的,导入元素是指要通过URI导入的另一个模型。导入元素具有子元素,这些子元素描述导入模型中可以访问的组件和单元。CellML API提供了允许访问此类信息的工具。
支持CellML1.1的结果是,处理一个数学模型可能需要检查多个CellML文件。为了解决这个问题,API引入了以下两个概念:一个导入的模型在加载后被实例化。当数学模型所需的所有导入都已加载(包括导入模型导入的模型)时,该模型称为完全实例化。
该API有一个用于选择性实例化特定导入的操作,以及一个用于完全实例化模型的操作。对于实例化的导入,还可以访问模型元素及其导入的组件。
我们为模型中的组件集包括了三个单独的属性,以及三组相应的单元:
-
本地组件集-仅包含特定CellML文件中的组件(不包括导入的组件);
-
模型集-包含本地集中的所有组件,以及同一文件中的导入组件元素(即导入元素的组件元素子元素,描述导入的组件);和
-
完整集-包含模型中的所有组件,跨越构成模型的所有文件。当包含组件的模型未实例化时,导入组件元素由迭代器提供。当一个模型被实例化时,迭代器将返回导入模型中的组件,此外,还将检查这些模型,以确定进一步导入的模型,以便根据需要搜索组件。
三个相应的单元集遵循与组件集完全相同的语义,除了在单元上,而不是在组件上。
一些技术细节
API中定义的接口都使用OMGIDL的继承功能从称为IObject的基本接口派生。IObject是根据XPCOM对象模型中名称类似的接口建模的。IObject接口用于为与接口底层对象相关的基本公共设施提供接口,例如维护引用计数(如后文所述),并为每个对象提供唯一标识符。此唯一标识符对于确定两个接口引用是否描述同一对象以及构建需要比较对象的数据结构非常有用。API实现使用引用计数[26]以确定何时没有对特定对象的剩余引用。IObject接口具有递增和递减引用计数的操作。为了确保引用计数正确工作,在整个API设计过程中始终遵循一些简单的规则(API设计依赖于处理API的代码所遵循的相同规则)。提供接口引用的所有操作和属性也会增加基础对象的引用计数。例如,如果操作创建了一个新对象,但没有保留对该新对象的内部引用,则返回接口的引用计数应为1。其次,每当通过调用add_ref操作(或通过获取返回的接口)增加引用计数时,程序员必须确保最终通过调用release_ref运算来减少引用计数。API被设计为可以通过包装器访问,因此对象的实际存储甚至可能驻留在提供所使用接口的包装器的不同机器上。因此,出现了第三条规则:add_ref调用必须始终与完全相同的接口指针上的release_ref调用来匹配(与同一对象的不同接口指针相对,后者可能指向同一对象周围的不同包装器)。
值得注意的是,虽然IObject提供了引用计数功能,但许多编程语言都执行自动垃圾收集。当使用到这些语言的直接桥接时,包装器代码将代表用户自动调用add_ref和release_ref,从而避免了对显式内存管理的需要。例如,Java桥利用Java中的最终功能,结合Java本机接口提供的内存管理功能,因此Java用户无需显式修改引用计数。
除了引用计数方案外,IObject接口还提供QueryInterface操作。此操作用于询问对象是否支持特定接口,如果支持,则提供接口表示。如前所述,API通常通过包装器代码进行访问,因此API用户应始终在API接口上执行QueryInterface操作,而不是直接使用特定于语言的转换机制。
由API实现创建的对象纯粹存在于内存中(例如,是否创建了模型从头算,或从文件加载)。可以对内存中的模型进行修改。只有当应用程序使用API将CellML模型序列化回XML,然后将该XML写入磁盘,替换原始文件时,才会更新原始文件。同样,如果同一个模型被加载两次,那么内存中将有两个独立的模型实例。修改一个实例不会自动更改另一个实例。导入模型时,每个导入元素和导入模型的每个实例都有一个导入模型的单独实例。然而,核心CellML API中的所有元素、集合和迭代器都是“活动的”,从这个意义上说,通过API对模型的内存中实例进行任何更改都将立即影响来自API的响应,即使在进行更改之前检索了元素、集合或迭代器也是如此。例如,如果创建了一个迭代器,并且迭代了模型中除一个以外的所有单元元素,并且删除了剩余的单元元素,并从迭代器中检索到下一个元素,那么它将返回一个空值,表示没有剩余的单元元要迭代。
API利用OMG IDL中的异常机制来处理异常情况(例如,当API由于与CellML规范不一致的元素结构而无法执行请求的操作时)。我们的实现一致地使用了异常安全技术[27](如Resource Acquisition Is Initialization(RAII)模式),以确保在引发异常时不会发生内存泄漏。
独立于语言的基于IDL的接口不能解决接口最初是如何获得的“引导”问题,例如,创建新模型的接口。这个问题的解决方案依赖于语言。在每种语言中,我们都提供检索引导接口指针的功能。例如,在C++中,这是通过在头中定义的方法获得的。引导程序接口是在IDL中定义的,因此跨所有语言绑定进行了标准化。每个扩展API都有一个单独的引导接口。
我们的API实现不允许在同一个模型上同时进行两次写入(例如,修改操作或使用属性setter),也不允许同时进行读取和写入。在多个线程上访问同一模型的应用程序需要使用互斥锁保护对API的所有访问,或者更有效地使用读写锁来确保没有与写并发的活动。
扩展API
除了核心API之外,我们还生产了API,以提供超出核心API范围的服务。
核心API不依赖于扩展,因此单个API实现可以选择不支持所有扩展API。然而,所有扩展都依赖于核心API,一些扩展也依赖于其他扩展(见图1).
注释工具服务
核心CellML API为在内存中使用任意用户提供的对象注释CellML模型中的元素提供了基本工具。然而,用户数据注释很难用于某些应用程序,因为核心API要求用户数据与密钥相关联,当应用程序使用完密钥后,必须手动删除密钥。
Annotation Tools(AnnoTools)API提供了分配和发布一组注释的功能,而无需担心对同一代码的独立调用会干扰其他注释,也无需担心需要单独删除对象上留下的所有注释。
AnnoTools API的IDL规范可以在CellML API源树的文件接口/AnnoTools.IDL中找到。
AnnoTools实现为每个AnnotationSet生成一个唯一的前缀,并允许用户使用该前缀设置注释。他们保留所有添加的注释的内部列表,并在AnnotationSet被销毁时清除AnnotationSet中的所有注释。
AnnoTools API还包括更容易设置和检索字符串、整数和浮点注释的功能。
CellML变量关联服务
CellML变量关联服务(CeVAS)API有助于将互连的CellML变量作为相同的数学变量进行处理(尽管可能采用不同的单位)。这些变量可能来自不同的组件,其中一些可能是从不同的模型导入的。
CeVAS API的IDL规范可以在CellML API源树的文件接口/CeVAS.IDL中找到。
CellML 1.0和1.1要求与其他组件中的变量连接的变量具有公共或私有接口值“in”或“out”。应用公共接口还是私有接口取决于组件之间的封装关系。在CellML中,所有“In”接口都必须连接到“out”接口,封装始终是非循环的,有效的CellML-模型具有有限数量的变量元素。这意味着,在一个完整有效的模型中,在每个变量连接网络中总是有一个变量没有“in”接口。此变量称为源变量,CeVAS将其用作(直接或间接)与其连接的所有变量的代表。
该接口允许用户提供CellML模型接口,并预先计算连接的变量。可以迭代连接到特定变量的所有变量,并检索源变量。
这是使用有效的不相交集算法实现的,该算法允许集的逆Ackerman摊销时间合并[28]. 最初,模型中的每个变量都被视为一组大小为1的变量。该算法迭代处理模型中的所有连接,合并与两个连接变量中的每个变量关联的不相交集。因此,使用n个组件和米连接在中O(运行)(nα(米,n个)),其中α是逆Ackerman函数。注意,逆Ackerman函数增长非常缓慢;例如,α(2,2)=1,而α(1020, 1020) = 3.
CellML单元简化和扩展服务
CellML Units Simplification and Expansion Service(CUSES)API为处理CellML-模型中的物理单元提供了便利。
CUSES API的IDL规范可以在CellML API源树中的文件接口/CUSES.IDL中找到。
CellML有一组内置单元。这些单位是根据SI定义的[29]基本单位;安培、坎德拉、开尔文、千克、米、摩尔和秒。其他预定义单位是根据这些定义的。例如,焦耳定义为k克.米2.秒-1个此外,建模者可以定义自己的导出单位(例如,浓度的mmol/L)或新的基本单位。然而,在处理模型时,重要的是要知道连接变量之间的关系,以便在必要时执行适当的转换。例如,当一个以米为单位的变量与一个以毫米为单位的变数相连时,工具需要插入一个隐式转换因子,因此相同的变量在两个分量之间是兼容的。CUSES允许工具更简单地实现这一点。
首先将所有单元展开为以基本单元为单位的表达式。SI前缀转换为乘数。所有单位都转换为标准形式,由基本单位幂的乘积组成,每个基本单位最多出现一次,可能只有一个乘数和/或偏移量。基本单元及其相应的指数在基本单元实例的可枚举列表中向API用户公开。提供设施询问两个单元的尺寸是否相等。这对于确定连接是否有效很有用。还可以检索执行转换所需的偏移量和乘数。
CellML规范服务验证
针对CellML规范服务的验证(VACSS)API接受假定为CellML-文件的文件,并识别Cell ML是否有效,如果文件无效,则尝试建立问题列表。
VACSS API的IDL规范可以在CellML API源树的文件接口/VACSS.IDL中找到。
可以检测到的错误分为两种类型:
MathML语言表达式服务
对于许多应用程序来说,一个常见的任务是将CellML文档中嵌入的MathML片段转换为其他一些基于线性文本表示的文本片段,例如编程语言源代码。
MathML语言表达式服务(MaLaES)API提供了帮助完成此任务的功能。MaLaES使用CeVAS来标识与每个MathML ci元素对应的源变量(即通过标识符引用变量)。然后,它使用AnnoTools检索包含输出中用于该变量的符号的注释(用户可以设置)。MaLaES API的IDL规范可以在CellML API源树的文件接口/MaLaES.IDL中找到。
通常情况下,这些转换需要考虑单位,以确保MathML中的所有变量都包含任何必要的转换因子。因此,MaLaES允许将变量(由ci引用)转换为源变量的单位,也可以转换表达式的结果。为了允许转换为许多不同的语言,MaLaES使用了一种称为MAL(MathML to Language)的自定义格式的规范。MAL描述描述了MathML元素及其在基于输出文本的表示中的形式之间的映射(允许对MathML-表达式树进行预排序、顺序或顺序后遍历,参数之间使用任意分隔符),并描述了每个操作的优先级,在高优先级操作符中,使用什么字符串开始和结束低优先级操作符的分组,以及转换的格式。MAL被预编译成一个高效的内存中表示,然后可以用来生成输出。
CellML代码生成服务
另一个常见任务是将整个CellML模型转换为过程编程语言中的代码,从而能够求解该模型。CellML代码生成服务(CCGS)API简化了此任务。
CCGS API的IDL规范可以在CellML API源树中的文件interfaces/CCGS.IDL中找到。
CCGS专门用于存在单一自变量(在许多模型和时间中)且DAE系统指数最多为一的常见情况[30,1]. API的用户通过特定于语言的引导过程获取CodeGeneratorBootstrap接口指针,然后使用createCodeGenerator操作获取Code发电机接口。
在此CodeGenerator接口上,可以指定要生成的语言的各种不同属性。这意味着可以为各种过程编程语言生成代码(在某些情况下,需要一些后处理来折叠长行或执行类似的转换)。
由于CCGS依赖MaLaES将单个数学表达式转换为正确的基于文本的形式,因此用户还需要为感兴趣的语言提供MAL描述。
CCGS使用术语“计算目标”(由ComputationTarget接口指针表示)来表示在CellML模型中计算方程式所需的任何内容(包括那些具有常量值的公式,在这种情况下,计算只是对该值的赋值)。CellML模型中的变量与计算目标之间没有一对一的关系。例如,可能有一个名为x个,初始值为0,然后是一个等式,例如在这种情况下,x个和都是计算目标(t吨自变量也被视为一致性的计算目标)。请注意,当一个变量用于多个组件时,但变量连接在一起(使它们成为相同的数学变量),所有变量元素只有一个计算目标。
CCGS给每个计算目标一个度(0度表示它是原始变量,1度表示它为变量的一阶导数,2度表示它的二阶导数,依此类推)。所有具有相应的高阶计算目标的计算目标都被视为状态变量,而具有较低阶计算对象的计算目标被视为速率。同时具有高阶和低阶计算目标的计算目标处于速率和状态变量的独特位置。CCGS通过使用标准技术将具有高阶导数的常微分方程(ODE)系统转换为具有不高于一阶导数的等效ODE系统,为这种情况生成代码[31].
最初,CellML变量的值实际上是常量,因此会进行标识和标记。只使用常量即可计算的变量又被归类为常量(重复此过程,直到无法识别其他常量)。可能需要求解联立方程组才能确定n个其他未知常数来自n个等式(但如果可以将其拆分为较小的子系统,则这样做更有效)。这是在我们的实现中使用的一种启发式算法实现的,该算法保证当需要求解的最大不可分系统最多有三个方程和三个未知量时,可以找到最小的可能系统,并且在我们的测试中取得了良好的结果。
在确定方程或系统需要求解的顺序后,将为其生成代码。这是使用应用程序提供给CCGS的三种不同模式之一完成的。CCGS需要计算计算目标的位置年使用如下等式年=(f)(x个1,x个2, ...,x个
n个
),CCGS将使用分配模式直接分配到符号中年在其他情况下,方程式的形式可能是(f)(年,x个1,x个2,x个三, ...,x个
n个
) =克(年,x个1,x个2,x个三, ...,x个
n个
),在这种情况下,使用单变量求解模式进行计算年最后,对于方程组,使用多元求解模式。
任何不是常数、状态或速率的计算目标都被归类为“代数计算目标”。CCGS以与常量相同的方式,根据系统或方程用于计算速率和代数计算目标的顺序,使用常量、状态和自变量,绘制出一个有向非循环图。然而,这些计算被分成两个代码片段。第一个代码片段包含从状态、常数和自变量计算速率所需的所有计算,而第二个代码片段计算第一个代码片断中未计算的任何剩余代数计算目标。这种分离允许更有效地处理模型,因为在许多时间步长,积分器可能不想报告任何结果,因此无需评估计算下一个时间步长不需要的计算目标。
CCGS能够自动将索引分配到四个不同的数组中:
CodeGenerator对象允许在每个要设置的数组中分配第一个索引(例如,在C语言中为0[32]其中数组索引从0开始,在其他语言(如MATLAB)中从1开始)。此外,用户可以提供模式,例如STATES[%],以描述如何在输出编程语言中取消引用数组。调用方还可以提供自己的AnnoSet对象,并在需要时显式提供每个计算目标的名称。
总的来说,有四个不同的代码片段可用。首先,如前所述,初始化常数的片段。其次,计算速率的片段(以及计算这些速率所需的所有代数计算目标)。第三,剩余变量的片段。最后的代码片段包含需要生成的任何函数(使用提供给CCGS的模式),以便评估代码。然后可以从单变量和多变量求解器模式调用这些函数,也可以在MAL规范中调用,例如用于计算定积分的函数。
作为CCGS实施过程中的模型,它还将检查并报告某些错误情况,例如具有无关方程(报告为过约束)的模型,或方程太少而无法计算所有计算目标的模型(即欠约束模型)。例如,由于CCGS仅支持索引1或更低的DAE,因此如果模型是有效的索引2 DAE,它将报告该模型被错误约束。
CellML语言导出定义服务
定义用于MaLaES的新编程语言需要设置该语言的MAL描述,并通过API进行配置。然而,为了允许用户定义任意语言,可以方便地与其他用户交换此信息。CellML语言导出定义服务(CeLEDS)允许将语言的MAL描述嵌入到XML文件中。此外,它还提供了一个通用字典服务,以便将生成不同语言输出所需的信息提供给CeLEDS服务的使用者。CeLEDSAPI的IDL规范可以在CellMLAPI源树中的文件interfaces/CeLEDS.IDL中找到。
CeLEDSExporter服务基于CeLEDS提供的服务来支持完整代码生成(基于CCGS)。所有信息都是以标准化的XML格式以及MAL描述指定的,而不是在CodeGenerator接口上以编程方式设置属性。此外,CeLEDS包含有关程序超结构的信息,包括允许程序运行所需的不变代码片段(例如任何补充函数定义)。
这意味着为一种语言生成代码所需的所有信息都封装在一个XML文件中,该文件可以在运行时读取。用户可以很容易地修改这些定义,以便自定义代码生成的各个方面,并为转换到其他语言创建新的定义。
由于对如何指定转换进行了标准化,我们创建了一个小型的CeLEDS/CeLEDSExporter兼容转换定义库,包括C、MATLAB和Python的定义。此存储库位于CellML API源代码的CeLEDS/languages子目录中。我们还为FORTRAN77创建了一个定义,尽管它需要进一步测试才能被广泛使用。
CellML集成服务
CellML集成服务(CIS)API提供了一个接口,用于执行模型模拟,并在结果可用时接收异步通知。
CIS API的IDL规范可以在CellML API源代码树中的文件接口/CIS.IDL中找到。
CellML模型接口指针被赋予CIS,CIS随后创建CellMLCompiledModel对象。然后,应用程序指定要使用的算法和模拟参数(例如误差容限、最大步长和控制报告哪些点的参数)。应用程序还可以选择覆盖初始值而不重新编译模型。
IntegrationProgressObserver接口可以由应用程序实现,并在开始模拟之前提供给CellMLIntegrationRun接口。该接口接收有关计算的常数值的信息,以及每个时间步长的结果,以及集成成功还是失败的指示(在后一种情况下会显示错误消息)。
我们在内部实现的CellML API使用CCGS生成C代码。然后使用编译器编译C代码。例如,在我们的一个基于CellML API的应用程序中,我们捆绑了来自自由/开源GNU编译器集合的C编译器的精简版本[33](gcc)和我们的应用程序。然后将代码链接到共享对象并动态加载到CIS实现中,CIS实现使用单独的程序线程来模拟模型(使用SUNDIALS CVODE项目中的ODE解算器[34],或GNU科学库中的ODE解算器[35],取决于所请求的算法)。
测试套件
我们还开发了一个广泛的测试套件,用于验证API实现。对于核心API(包括DOM和MathML DOM)以及一些扩展API,test-suite中包含的程序使用API中的每个属性和操作,并检查如果实现行为正确,则预期为true的不变量实际上是否为true。此外,测试套件还包括一系列小程序,以及这些程序的一系列输入和预期输出。例如,程序CellML2C是一个小型的命令行驱动测试程序,它以CellML模型作为输入,并使用CCGS扩展API从中生成C代码。测试套件使用17个不同的模型调用CellML2 C(每个模型都经过精心设计,包含用于测试不同功能的特性)。每次提交后,我们的API实现都会在Linux、Mac OS X和Windows XP上根据此测试套件进行自动测试特别的在一系列其他平台上进行测试。API实现目前通过了上述所有测试。
未来,我们计划添加一些测试,以确认CellML集成服务实现提供的数字结果是正确的,与SBML测试套件类似http://sourceforge.net/projects/sbml/files/test-suite/2.0.0%20alpha/.
与libSBML的比较
据我们所知,CellMLAPI是第一个支持CellML模型处理的公开API。然而,还有其他类似的项目旨在处理不同编码的数学模型。
LibSBML在许多方面类似于CellML API,只是它处理SBML模型。由于CellML提供了比SBML更高级别的领域独立性,预计跨许多不同专业领域使用的工具将需要交换CellML模型。此外,一些工具(如通用建模环境)可能需要导入和导出SBML和CellML模型,在这种情况下,CellMLAPI实现和libSBML可以在同一程序中一起使用。除了语言支持方面的差异外,CellML API和libSBML之间还有一些其他的主要差异。
CellML API强调接口定义(即使用IDL描述的API本身)和接口实现的技术分离。使用CellML API,添加新的语言绑定需要开发代码来自动根据IDL描述生成包装器,而不是使用SWIG[36](这可能需要手动创建包装来整理细节)。因此,我们期望CellML API方法对API的更改和添加全新模块更加稳健。
此外,CellML API采用不同的方法处理MathML。CellML API需要实现来支持现有的API,即MathML DOM[20]而libSBML提供了一种更有限的基于抽象语法树(AST)的方法。然而,libSBML方法确实允许对纯文本进行翻译;此功能在OpenCell中可用(参见下文),我们计划在CellML API的未来版本中包含此功能。