diff --git a/examples/modelDescription.mjs b/examples/modelDescription.mjs new file mode 100644 index 0000000..b983bc7 --- /dev/null +++ b/examples/modelDescription.mjs @@ -0,0 +1,48 @@ +// This script examplifies how to get model description units and variables. +// +// Make sure to have installed dependencies and have the required environment variables +// available, as described in the Quick start example: +// +// https://github.com/modelon-community/impact-client-js#quick-start +// +// Then run the example with: node modelDescription.mjs + +import { Analysis, Client, ExperimentDefinition, Model } from '../dist/index.js' +import dotenv from 'dotenv' + +// Load the .env file variables, install with: npm install dotenv +dotenv.config({ path: '../.env' }) + +const client = Client.fromImpactApiKey({ + impactApiKey: process.env.MODELON_IMPACT_CLIENT_API_KEY, + jupyterHubToken: process.env.JUPYTERHUB_API_TOKEN, + serverAddress: process.env.MODELON_IMPACT_SERVER, +}) + +const WorkspaceName = 'test' + +const workspace = await client.createWorkspace({ + name: WorkspaceName, +}) + +const experimentDefinition = ExperimentDefinition.from({ + analysis: Analysis.from(Analysis.DefaultAnalysis), + model: Model.from({ className: 'Modelica.Blocks.Examples.PID_Controller' }), +}) + +const experiment = await workspace.executeExperimentUntilDone({ + caseIds: ['case_1'], + experimentDefinition, +}) + +const cases = await experiment.getCases() +const modelExecutable = await cases[0].getModelExecutable() +const modelDescription = await modelExecutable.getModelDescription() + +const variables = modelDescription.getVariables() +console.log(variables[0]) + +const units = modelDescription.getUnits() +console.log(units[0]) + +await client.deleteWorkspace(WorkspaceName) diff --git a/package-lock.json b/package-lock.json index 81c9ae4..d9ba6cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "axios": "^0.27.2", "axios-cookiejar-support": "^4.0.3", + "fast-xml-parser": "^4.2.7", "tough-cookie": "^4.0.0" }, "devDependencies": { @@ -3089,6 +3090,27 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-xml-parser": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.7.tgz", + "integrity": "sha512-J8r6BriSLO1uj2miOk1NW0YVm8AGOOu3Si2HQp/cSmo6EA4m3fcwu2WKjJ4RK9wMLBtg69y1kS8baDiQBR41Ig==", + "funding": [ + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + }, + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", @@ -8409,6 +8431,11 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, "node_modules/sucrase": { "version": "3.25.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.25.0.tgz", @@ -11427,6 +11454,14 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "fast-xml-parser": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.7.tgz", + "integrity": "sha512-J8r6BriSLO1uj2miOk1NW0YVm8AGOOu3Si2HQp/cSmo6EA4m3fcwu2WKjJ4RK9wMLBtg69y1kS8baDiQBR41Ig==", + "requires": { + "strnum": "^1.0.5" + } + }, "fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", @@ -15248,6 +15283,11 @@ "acorn": "^8.10.0" } }, + "strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, "sucrase": { "version": "3.25.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.25.0.tgz", diff --git a/package.json b/package.json index d8de035..870642f 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "dependencies": { "axios": "^0.27.2", "axios-cookiejar-support": "^4.0.3", + "fast-xml-parser": "^4.2.7", "tough-cookie": "^4.0.0" }, "devDependencies": { diff --git a/src/api.ts b/src/api.ts index a9c3e33..d0595e9 100644 --- a/src/api.ts +++ b/src/api.ts @@ -21,7 +21,9 @@ import { ExperimentItem, ExperimentTrajectories, ExperimentVariables, + FmuId, LocalProjectProtocol, + ModelExecutableInfo, ModelicaExperimentDefinition, ProjectId, WorkspaceProtocol, @@ -682,6 +684,26 @@ class Api { .catch((e) => reject(toApiError(e))) }) + getModelDescription = ({ + fmuId, + workspaceId, + }: { + fmuId: FmuId + workspaceId: WorkspaceId + }): Promise => + new Promise((resolve, reject) => { + this.ensureImpactToken() + .then(() => { + this.axios + .get( + `${this.baseUrl}${this.jhUserPath}impact/api/workspaces/${workspaceId}/model-executables/${fmuId}/model-description` + ) + .then((res) => resolve(res.data)) + .catch((e) => reject(toApiError(e))) + }) + .catch((e) => reject(toApiError(e))) + }) + getCustomFunctionOptions = ({ customFunction, workspaceId, @@ -737,6 +759,42 @@ class Api { this.configureAxios() } + getModelExecutableInfo = ({ + fmuId, + workspaceId, + }: { + fmuId: FmuId + workspaceId: WorkspaceId + }): Promise => + new Promise((resolve, reject) => { + this.ensureImpactToken() + .then(() => { + this.axios + .get( + `${this.baseUrl}${this.jhUserPath}impact/api/workspaces/${workspaceId}/model-executables/${fmuId}` + ) + .then((response) => resolve(response.data)) + .catch((e) => reject(toApiError(e))) + }) + .catch((e) => reject(toApiError(e))) + }) + + getModelExecutableInfos = ( + workspaceId: WorkspaceId + ): Promise => + new Promise((resolve, reject) => { + this.ensureImpactToken() + .then(() => { + this.axios + .get( + `${this.baseUrl}${this.jhUserPath}impact/api/workspaces/${workspaceId}/model-executables` + ) + .then((response) => resolve(response.data?.data?.items)) + .catch((e) => reject(toApiError(e))) + }) + .catch((e) => reject(toApiError(e))) + }) + delete = (path: string) => new Promise((resolve, reject) => { this.ensureImpactToken() diff --git a/src/case.ts b/src/case.ts index 3f2a6b6..7e305b4 100644 --- a/src/case.ts +++ b/src/case.ts @@ -3,13 +3,16 @@ import { CaseId, CaseTrajectories, ExperimentId, + FmuId, WorkspaceId, } from './types' import Api from './api' +import ModelExecutable from './model-executable' class Case { private api: Api private experimentId: ExperimentId + private fmuId?: FmuId id: CaseId runInfo: CaseRunInfo private workspaceId: WorkspaceId @@ -17,17 +20,20 @@ class Case { constructor({ api, experimentId, + fmuId, id, runInfo, workspaceId, }: { api: Api id: CaseId + fmuId?: FmuId experimentId: ExperimentId runInfo: CaseRunInfo workspaceId: WorkspaceId }) { this.api = api + this.fmuId = fmuId this.id = id this.runInfo = runInfo this.experimentId = experimentId @@ -48,6 +54,18 @@ class Case { workspaceId: this.workspaceId, }) + getModelExecutable = async () => { + if (!this.fmuId) { + return null + } + + return ModelExecutable.from({ + api: this.api, + fmuId: this.fmuId, + workspaceId: this.workspaceId, + }) + } + getTrajectories = (variableNames: string[]): Promise => this.api.getCaseTrajectories({ caseId: this.id, diff --git a/src/default-experiment.ts b/src/default-experiment.ts new file mode 100644 index 0000000..b85f298 --- /dev/null +++ b/src/default-experiment.ts @@ -0,0 +1,25 @@ +class DefaultExperiment { + startTime + stepSize + stopTime + tolerance + + constructor({ + startTime, + stepSize, + stopTime, + tolerance, + }: { + startTime?: number + stepSize?: number + stopTime?: number + tolerance?: number + }) { + this.startTime = startTime + this.stepSize = stepSize + this.stopTime = stopTime + this.tolerance = tolerance + } +} + +export default DefaultExperiment diff --git a/src/experiment.ts b/src/experiment.ts index f6af099..65a798d 100644 --- a/src/experiment.ts +++ b/src/experiment.ts @@ -52,6 +52,7 @@ class Experiment { new Case({ api: this.api, experimentId: this.id, + fmuId: caseResponse.input.fmu_id, id: caseResponse.id || i.toString(), runInfo: caseResponse.run_info, workspaceId: this.workspaceId, diff --git a/src/model-description.ts b/src/model-description.ts new file mode 100644 index 0000000..0978c5b --- /dev/null +++ b/src/model-description.ts @@ -0,0 +1,61 @@ +import DefaultExperiment from './default-experiment' +import { Unit, Variable } from './types' + +type ModelDescriptionData = { + DefaultExperiment: Record + ModelVariables: { + ScalarVariable: Variable[] + } + UnitDefinitions: { Unit: Unit[] } + modelName: string +} + +class ModelDescription { + private data: ModelDescriptionData + + constructor(data: ModelDescriptionData) { + this.data = data + } + + getDefaultExperiment() { + const defaultExperiment = this.data.DefaultExperiment + + const toFloatOrUndefined = (key: string, obj: Record) => + obj[key] ? parseFloat(obj[key]) : undefined + + const parameters: ConstructorParameters[0] = + {} + + const defaultExperimentKeys = [ + 'startTime', + 'stepSize', + 'stopTime', + 'tolerance', + ] as const + + type DefaultExperimentKey = typeof defaultExperimentKeys[number] + + defaultExperimentKeys.forEach((key) => { + parameters[key as DefaultExperimentKey] = toFloatOrUndefined( + key, + defaultExperiment + ) + }) + + return new DefaultExperiment(parameters) + } + + getModelName() { + return this.data.modelName + } + + getUnits() { + return this.data.UnitDefinitions.Unit + } + + getVariables(): Variable[] { + return this.data.ModelVariables.ScalarVariable + } +} + +export default ModelDescription diff --git a/src/model-executable.ts b/src/model-executable.ts new file mode 100644 index 0000000..00f32e7 --- /dev/null +++ b/src/model-executable.ts @@ -0,0 +1,111 @@ +import Analysis from './analysis' +import Api from './api' +import DefaultExperiment from './default-experiment' +import ExperimentDefinition from './experiment-definition' +import ModelDescription from './model-description' +import { XMLParser } from 'fast-xml-parser' +import Model from './model' +import { FmuId, ModelExecutableInfo, WorkspaceId } from './types' + +class ModelExecutable { + private api: Api + private defaultExperiment?: DefaultExperiment + private info?: ModelExecutableInfo + fmuId: FmuId + private modelDescription?: ModelDescription + private workspaceId: WorkspaceId + + private constructor({ + api, + defaultExperiment, + fmuId, + info, + modelDescription, + workspaceId, + }: { + api: Api + defaultExperiment?: DefaultExperiment + fmuId: FmuId + info?: ModelExecutableInfo + modelDescription?: ModelDescription + workspaceId: WorkspaceId + }) { + this.api = api + if (defaultExperiment) { + this.defaultExperiment = defaultExperiment + } + this.fmuId = fmuId + if (info) { + this.info = info + } + if (modelDescription) { + this.modelDescription = modelDescription + } + this.workspaceId = workspaceId + } + + async createExperimentDefinition(analysis: Analysis) { + if (!this.modelDescription) { + this.modelDescription = await this.downloadModelDescription() + } + return ExperimentDefinition.from({ + analysis, + model: Model.from({ + className: this.modelDescription.getModelName(), + }), + }) + } + + private async downloadModelDescription(): Promise { + const modelDescriptionXML = await this.api.getModelDescription({ + fmuId: this.fmuId, + workspaceId: this.workspaceId, + }) + + const options = { + attributeNamePrefix: '', + ignoreAttributes: false, + } + const parser = new XMLParser(options) + + const modelDescriptionJSON = parser.parse(modelDescriptionXML) + + return new ModelDescription(modelDescriptionJSON.fmiModelDescription) + } + + async getModelDescription() { + if (!this.modelDescription) { + this.modelDescription = await this.downloadModelDescription() + } + return this.modelDescription + } + + async getModelExecutableInfo() { + if (!this.info) { + this.info = await this.api.getModelExecutableInfo({ + fmuId: this.fmuId, + workspaceId: this.workspaceId, + }) + } + + return this.info + } + + static from({ + api, + fmuId, + workspaceId, + }: { + api: Api + fmuId: FmuId + workspaceId: WorkspaceId + }) { + return new ModelExecutable({ + api, + fmuId, + workspaceId, + }) + } +} + +export default ModelExecutable diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 1f1f511..f1f463d 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -59,6 +59,9 @@ export type LocalProjectProtocol = components['schemas']['LocalProjectProtocol'] export type FmuModel = components['schemas']['FmuModel'] export type ModelicaModel = components['schemas']['ModelicaModel'] +export type ModelExecutableInfo = + components['schemas']['ModelExecutableListV2'][0] + const modelTypes = ['fmuModel', 'modelicaModel'] as const export type ModelType = typeof modelTypes[number] @@ -68,5 +71,45 @@ export type CaseDefinition = components['schemas']['Extensions'][0] & { export type CaseId = string export type ExperimentId = string +export type FmuId = string export type ProjectId = string export type WorkspaceId = string + +export type BaseUnitAttribute = + | 'kg' + | 'm' + | 's' + | 'A' + | 'K' + | 'mol' + | 'cd' + | 'rad' + | 'factor' + | 'offset' + +export type BaseUnit = { [key in BaseUnitAttribute]: string } +export type UnitDefinition = { name: string; factor?: string; offset?: string } +export type Unit = { + BaseUnit: string | BaseUnit + DisplayUnit: UnitDefinition[] + name: string +} + +export type VariableValueType = + | 'Real' + | 'Integer' + | 'Boolean' + | 'String' + | 'Enumeration' + +export type Variable = { + [key in VariableValueType]: Record +} & { + name: string + valueReference: string + description: string + causality: string + variability: string + initial?: string + canHandleMultipleSetPerTimeInstant?: string +} diff --git a/src/workspace.ts b/src/workspace.ts index e3eef03..dff54a1 100644 --- a/src/workspace.ts +++ b/src/workspace.ts @@ -10,6 +10,7 @@ import { import Api from './api' import Experiment from './experiment' import ExperimentDefinition from './experiment-definition' +import ModelExecutable from './model-executable' import Project from './project' class Workspace { @@ -148,6 +149,24 @@ class Workspace { ) } + getModelExecutables = async (): Promise => { + const modelExecutableInfos = await this.api.getModelExecutableInfos( + this.id + ) + + if (!modelExecutableInfos) { + return [] + } + + return modelExecutableInfos.map((info) => { + return ModelExecutable.from({ + api: this.api, + fmuId: info.id, + workspaceId: this.id, + }) + }) + } + getProjects = async (): Promise => this.api.getWorkspaceProjects(this.id) } diff --git a/tests/integration/integration.test.ts b/tests/integration/integration.test.ts index 579b778..584a128 100644 --- a/tests/integration/integration.test.ts +++ b/tests/integration/integration.test.ts @@ -1,6 +1,7 @@ import * as dotenv from 'dotenv' import { Analysis, + ApiError, Client, ExperimentDefinition, InvalidApiKey, @@ -9,7 +10,7 @@ import { Workspace, } from '../../dist' import { ModelicaExperimentDefinition, ModelicaModel } from '../../src/types' -import { expect, test } from 'vitest' +import { beforeEach, expect, test } from 'vitest' import basicExperimentDefinition from './basicExperimentDefinition.json' dotenv.config() @@ -32,15 +33,15 @@ const getClient = (options?: { const TestWorkspaceName = 'integration-test-ws' +beforeEach(async () => { + const client = getClient() + await deleteTestWorkspace(client) +}) + const getTestWorkspace = async (client: Client) => { - let testWorkspace - try { - testWorkspace = await client.getWorkspace(TestWorkspaceName) - } catch (e) { - testWorkspace = await client.createWorkspace({ - name: TestWorkspaceName, - }) - } + const testWorkspace = await client.createWorkspace({ + name: TestWorkspaceName, + }) expect(testWorkspace.definition.name).toEqual(TestWorkspaceName) @@ -48,14 +49,15 @@ const getTestWorkspace = async (client: Client) => { } const deleteTestWorkspace = async (client: Client) => { - await client.deleteWorkspace(TestWorkspaceName) + const workspaces = await client.getWorkspaces() - const workspacesAfterDelete = await client.getWorkspaces() - expect( - workspacesAfterDelete.find( - (w: Workspace) => w.definition.name === TestWorkspaceName - ) - ).toEqual(undefined) + const deletePromises = await workspaces + .filter((ws) => ws.definition.name === TestWorkspaceName) + .map((ws) => { + client.deleteWorkspace(ws.id) + }) + + await Promise.all(deletePromises) } test('Try to use invalid impact API key', () => @@ -67,7 +69,7 @@ test('Try to use invalid impact API key', () => .then(() => { throw new Error('Test should have caught error') }) - .catch((e) => { + .catch((e: ApiError) => { // instanceof does not work for checking the type here, a ts-jest specific problem perhaps. // ApiError has errorCode. if ('errorCode' in e) { @@ -87,7 +89,7 @@ test('Try to use invalid jupyter hub token', () => .then(() => { throw new Error('Test should have caught error') }) - .catch((e) => { + .catch((e: ApiError) => { // instanceof does not work for checking the type here, a ts-jest specific problem perhaps. // ApiError has errorCode. if ('errorCode' in e) { @@ -116,7 +118,7 @@ test( const caseIds = experimentDefinition .getCaseDefinitions() - .map((def) => def.caseId) + .map((def: { caseId: string }) => def.caseId) try { const experiment = await testWorkspace.executeExperimentUntilDone({ @@ -207,11 +209,11 @@ test( const projects = await testWorkspace.getProjects() expect(projects.length).toEqual(1) - - await deleteTestWorkspace(client) } catch (e) { if (e instanceof Error) { console.log(e.toString()) + } else { + console.log(e) } throw new Error('Caught unexpected error while executing test') } @@ -249,11 +251,11 @@ test( timeoutMs: 60 * 1000, }) expect(typeof experiment).toBe('object') - - await deleteTestWorkspace(client) } catch (e) { if (e instanceof Error) { console.log(e.toString()) + } else { + console.log(e) } throw new Error('Caught unexpected error while executing test') } @@ -293,11 +295,11 @@ test( tries++ } expect(tries).toBeLessThan(MAX_TRIES) - - await deleteTestWorkspace(client) } catch (e) { if (e instanceof Error) { console.log(e.toString()) + } else { + console.log(e) } throw new Error('Caught unexpected error while executing test') } @@ -356,11 +358,64 @@ test( done = true } } + } catch (e) { + if (e instanceof Error) { + console.log(e.toString()) + } else { + console.log(e) + } + throw new Error('Caught unexpected error while executing test') + } + }, + TwentySeconds +) - await deleteTestWorkspace(client) +test( + 'Run simulation then examine ModelExecutable and ModelDescription', + async () => { + const experimentDefinition = + ExperimentDefinition.fromModelicaExperimentDefinition( + basicExperimentDefinition as unknown as ModelicaExperimentDefinition + ) + + const client = getClient() + const testWorkspace = await getTestWorkspace(client) + + try { + const experiment = await testWorkspace.executeExperimentUntilDone({ + caseIds: ['case_1', 'case_2'], + experimentDefinition, + timeoutMs: TwentySeconds, + }) + + const cases = await experiment.getCases() + const modelExecutable = await cases[0].getModelExecutable() + expect(modelExecutable).not.toBeNull() + if (modelExecutable) { + const caseModelExecutableInfo = + await modelExecutable.getModelExecutableInfo() + expect(caseModelExecutableInfo.input.class_name).toEqual( + 'Modelica.Blocks.Examples.PID_Controller' + ) + } + + const modelExecutables = await testWorkspace.getModelExecutables() + expect(modelExecutables.length).toEqual(1) + const modelDescription = + await modelExecutables[0].getModelDescription() + expect(modelDescription.getModelName()).toEqual( + 'Modelica.Blocks.Examples.PID_Controller' + ) + const modelExecutableInfo = + await modelExecutables[0].getModelExecutableInfo() + expect(modelExecutableInfo.input.class_name).toEqual( + 'Modelica.Blocks.Examples.PID_Controller' + ) } catch (e) { if (e instanceof Error) { console.log(e.toString()) + } else { + console.log(e) } throw new Error('Caught unexpected error while executing test') } diff --git a/tests/unit/default-experiment.test.ts b/tests/unit/default-experiment.test.ts new file mode 100644 index 0000000..55eef79 --- /dev/null +++ b/tests/unit/default-experiment.test.ts @@ -0,0 +1,18 @@ +import DefaultExperiment from '../../src/default-experiment' +import { expect, test } from 'vitest' + +test('Get parameters from default experiment', () => { + const defaultExperiment = new DefaultExperiment({ + startTime: 10, + stepSize: 11.1, + stopTime: 12.2, + tolerance: 500, + }) + + expect(defaultExperiment.startTime).toBe(10) + + expect(defaultExperiment.stepSize).toBe(11.1) + + expect(defaultExperiment.stopTime).toBe(12.2) + expect(defaultExperiment.tolerance).toBe(500) +}) diff --git a/tests/unit/model-executable.test.ts b/tests/unit/model-executable.test.ts new file mode 100644 index 0000000..5bd90b2 --- /dev/null +++ b/tests/unit/model-executable.test.ts @@ -0,0 +1,56 @@ +import ModelExecutable from '../../src/model-executable' +import fs from 'fs' +import { expect, test, vi } from 'vitest' +import Api from '../../src/api' +import { BaseUnit } from '../../src/types' + +const modelDescription = fs.readFileSync( + './tests/unit/modelDescription.xml', + 'utf-8' +) + +vi.mock('../../src/api', () => { + const Api = vi.fn() + + Api.prototype.getModelDescription = async () => modelDescription + + return { default: Api } +}) + +test('Create and examine a ModelExecutable instance', async () => { + // @ts-ignore + const api = new Api({ + impactApiKey: 'mock-api-key', + jupyterHubToken: 'mock-jh-token', + }) + + const modelExecutable = await ModelExecutable.from({ + api, + fmuId: 'some-fmu-id', + workspaceId: 'some-ws-id', + }) + + const md = await modelExecutable.getModelDescription() + + const defaultExperiment = await md.getDefaultExperiment() + + expect(defaultExperiment?.startTime).toBe(undefined) + expect(defaultExperiment?.stepSize).toBe(undefined) + expect(defaultExperiment?.stopTime).toBe(4.0) + expect(defaultExperiment?.tolerance).toBe(undefined) + + const variables = md.getVariables() + + expect(variables.length).toBe(182) + + expect(variables[0].Real.relativeQuantity).toEqual('false') + expect(variables[0].name).toEqual('PI.Dzero.k') + + const units = md.getUnits() + expect(units.length).toBe(10) + + expect(units[0].name).toEqual('rad') + expect(units[0].DisplayUnit.length).toEqual(2) + + expect((units[2].BaseUnit as BaseUnit).s).toEqual('1') +}) diff --git a/tests/unit/modelDescription.xml b/tests/unit/modelDescription.xml new file mode 100644 index 0000000..2f9974d --- /dev/null +++ b/tests/unit/modelDescription.xml @@ -0,0 +1,855 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +