Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f2d5a1f
simlin-compat: start on mdl conversion module
bpowers Jan 22, 2026
976651d
compat: implement MDL TokenNormalizer
bpowers Jan 22, 2026
92e98eb
compat: add MDL AST types for parser
bpowers Jan 22, 2026
1a5cbaa
compat: normalize all WITH LOOKUP variants to Token::WithLookup
bpowers Jan 23, 2026
7966fca
compat: address MDL AST review feedback
bpowers Jan 23, 2026
3671e38
compat: implement LALRPOP parser for MDL equations
bpowers Jan 23, 2026
6e8288d
compat: implement MDL to datamodel conversion
bpowers Jan 23, 2026
50d22b1
compat: refactor convert.rs into submodules
bpowers Jan 23, 2026
1e8aff3
compat: add comprehensive MDL equivalence tests
bpowers Jan 24, 2026
991167e
compat: support ACTIVE INITIAL in native MDL parser
bpowers Jan 24, 2026
9297168
compat: parse Vensim SUPPLEMENTARY flag as structured field
bpowers Jan 24, 2026
c451a6a
xmutil: parse Vensim SUPPLEMENTARY flag as structured field
bpowers Jan 24, 2026
e89d3b8
xmutil: use as_sectors mode for single-model output
bpowers Jan 24, 2026
b67c6e3
compat: parse MDL settings section for unit equivalences and integrat…
bpowers Jan 24, 2026
460f96d
compat: add RK2 integration method and improve MDL settings parsing
bpowers Jan 24, 2026
11c3357
compat: simplify MDL equivalence test normalization
bpowers Jan 24, 2026
7191845
compat: add principled unit simplification to native MDL parser
bpowers Jan 24, 2026
bcb3487
compat: handle range-only units as dimensionless in native MDL parser
bpowers Jan 24, 2026
08a75f8
compat: add MIT license and README for xmutil-derived MDL parser
bpowers Jan 24, 2026
89be777
compat: rename feature from "vensim" to "xmutil"
bpowers Jan 24, 2026
a85f7d6
compat: implement semantic model groups (sectors)
bpowers Jan 25, 2026
af4be45
compat: update MDL parser plan to reflect current implementation state
bpowers Jan 25, 2026
9b35c8b
compat: implement view/sketch parsing for native MDL parser
bpowers Jan 25, 2026
d2e165b
compat: fix view ghost/alias detection and filter Time variable
bpowers Jan 25, 2026
716e66e
compat: achieve view conversion parity with xmutil
bpowers Jan 25, 2026
ca7c254
compat: fix view conversion parity with xmutil round 3
bpowers Jan 26, 2026
91a5f02
compat: prefer TIME STEP units for time_units (xmutil parity)
bpowers Jan 26, 2026
c99441a
compat: accept escaped underscores in MDL symbols
bpowers Jan 26, 2026
6a6a915
compat: enable full view element comparison in MDL equivalence tests
bpowers Jan 26, 2026
e99de86
compat: expose native MDL import in non-xmutil builds
bpowers Jan 26, 2026
a8bc71f
app: use native MDL import, remove xmutil-js dependency
bpowers Jan 26, 2026
c1373af
compat: use default view_box for native MDL import
bpowers Jan 26, 2026
22b4164
engine2: remove hasVensimSupport and xmutil feature references
bpowers Jan 26, 2026
c9e329a
core: preserve link polarity and useLetteredPolarity in TS models
bpowers Jan 26, 2026
86af79d
engine: update ModelGroup members on variable rename
bpowers Jan 26, 2026
91b869e
engine: remove deleted variables from group members
bpowers Jan 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 48 additions & 0 deletions doc/simlin-project.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@
"items": {
"$ref": "#/$defs/LoopMetadata"
}
},
"groups": {
"type": "array",
"items": {
"$ref": "#/$defs/ModelGroup"
}
}
},
"required": [
Expand Down Expand Up @@ -498,6 +504,9 @@
"zoom": {
"type": "number",
"format": "double"
},
"useLetteredPolarity": {
"type": "boolean"
}
},
"required": [
Expand Down Expand Up @@ -798,6 +807,12 @@
"items": {
"$ref": "#/$defs/LinkPoint"
}
},
"polarity": {
"type": [
"string",
"null"
]
}
},
"required": [
Expand Down Expand Up @@ -954,6 +969,39 @@
"name"
]
},
"ModelGroup": {
"description": "Semantic/organizational group for categorizing model variables.\nThis is distinct from visual diagram groups (ViewElement::Group).",
"type": "object",
"properties": {
"name": {
"type": "string"
},
"doc": {
"type": [
"string",
"null"
]
},
"parent": {
"type": [
"string",
"null"
]
},
"members": {
"type": "array",
"items": {
"type": "string"
}
},
"runEnabled": {
"type": "boolean"
}
},
"required": [
"name"
]
},
"Dimension": {
"type": "object",
"properties": {
Expand Down
24 changes: 2 additions & 22 deletions src/app/NewProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { Project } from './Project';
import { User } from './User';
import { Project as ProjectDM } from '@system-dynamics/core/datamodel';
import { convertMdlToXmile } from '@system-dynamics/xmutil';
import { Project as Engine2Project } from '@system-dynamics/engine2';
import type { JsonProject } from '@system-dynamics/engine2';

Expand Down Expand Up @@ -143,27 +142,12 @@ export class NewProject extends React.Component<NewProjectProps, NewProjectState
}
const file = event.target.files[0];
const contents = await readFile(file);
let logs: string | undefined;

try {
let engine2Project: Engine2Project;

if (file.name.endsWith('.mdl')) {
// For Vensim MDL files, try direct import first if available
const hasVensim = await Engine2Project.hasVensimSupport();
if (hasVensim) {
engine2Project = await Engine2Project.openVensim(contents);
} else {
// Fall back to xmutil conversion when direct Vensim support is not available
const [xmileContents, conversionLogs] = await convertMdlToXmile(contents, true);
logs = conversionLogs;
if (xmileContents.length === 0) {
throw new Error('Vensim converter: ' + (logs || 'unknown error'));
}
engine2Project = await Engine2Project.open(xmileContents);
}
engine2Project = await Engine2Project.openVensim(contents);
} else {
Comment on lines 148 to 150

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate MDL import on Vensim support

This path now unconditionally calls Engine2Project.openVensim for .mdl files. The engine2 API still exposes Project.hasVensimSupport() and its tests explicitly handle the case where openVensim must throw when the WASM build lacks simlin_project_open_vensim. In that scenario, users will now hit a runtime exception and lose MDL import entirely (there’s no fallback anymore), whereas the previous code avoided that by checking support and falling back. Consider restoring the support check (or another fallback) so MDL import doesn’t regress on builds that omit Vensim support.

Useful? React with 👍 / 👎.

// XMILE/STMX files open directly
engine2Project = await Engine2Project.open(contents);
}

Expand All @@ -172,12 +156,8 @@ export class NewProject extends React.Component<NewProjectProps, NewProjectState
const activeProject = ProjectDM.fromJson(json);
const views = activeProject.models.get('main')?.views;
if (!views || views.isEmpty()) {
let errorMsg = `can't import model with no view at this time.`;
if (logs && logs.length !== 0) {
errorMsg = logs;
}
this.setState({
errorMsg,
errorMsg: `can't import model with no view at this time.`,
});
return;
}
Expand Down
1 change: 0 additions & 1 deletion src/app/config/rsbuild/shared.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ const sharedConfig = defineConfig({
'@system-dynamics/core': resolveApp('../core'),
'@system-dynamics/diagram': resolveApp('../diagram'),
'@system-dynamics/engine2': resolveApp('../engine2'),
'@system-dynamics/xmutil': resolveApp('../xmutil-js'),
},
},
},
Expand Down
3 changes: 1 addition & 2 deletions src/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
"dependencies": {
"@system-dynamics/core": "^1.0.0",
"@system-dynamics/diagram": "^1.0.0",
"@system-dynamics/engine2": "^2.0.0",
"@system-dynamics/xmutil": "^1.0.0"
"@system-dynamics/engine2": "^2.0.0"
},
"devDependencies": {
"@rsbuild/core": "^1.3.22",
Expand Down
3 changes: 1 addition & 2 deletions src/app/tsconfig.browser.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
"references": [
{ "path": "../core/tsconfig.browser.json" },
{ "path": "../diagram/tsconfig.browser.json" },
{ "path": "../engine2/tsconfig.browser.json" },
{ "path": "../xmutil-js/tsconfig.browser.json" }
{ "path": "../engine2/tsconfig.browser.json" }
]
}
72 changes: 61 additions & 11 deletions src/core/datamodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
type JsonFlowPoint,
type JsonLinkPoint,
type JsonLoopMetadata,
type JsonModelGroup,
type JsonSource,
} from '@system-dynamics/engine2';

Expand Down Expand Up @@ -859,13 +860,9 @@ const linkViewElementDefaults = {
arc: undefined as number | undefined,
isStraight: false,
multiPoint: undefined as List<Point> | undefined,
polarity: undefined as string | undefined,
};
export class LinkViewElement extends Record(linkViewElementDefaults) implements ViewElement {
// this isn't useless, as it ensures we specify the full object

constructor(props: typeof linkViewElementDefaults) {
super(props);
}
static fromJson(json: JsonLinkViewElement): LinkViewElement {
let arc: number | undefined = undefined;
let isStraight = false;
Expand All @@ -886,6 +883,7 @@ export class LinkViewElement extends Record(linkViewElementDefaults) implements
arc,
isStraight,
multiPoint,
polarity: json.polarity,
});
}
toJson(): JsonLinkViewElement {
Expand All @@ -900,6 +898,9 @@ export class LinkViewElement extends Record(linkViewElementDefaults) implements
} else if (this.multiPoint) {
result.multiPoints = this.multiPoint.map((p) => ({ x: p.x, y: p.y })).toArray();
}
if (this.polarity !== undefined) {
result.polarity = this.polarity;
}
return result;
}
get cx(): number {
Expand Down Expand Up @@ -1162,13 +1163,9 @@ const stockFlowViewDefaults = {
elements: List<ViewElement>(),
viewBox: Rect.default(),
zoom: -1,
useLetteredPolarity: false,
};
export class StockFlowView extends Record(stockFlowViewDefaults) {
// this isn't useless, as it ensures we specify the full object

constructor(props: typeof stockFlowViewDefaults) {
super(props);
}
static fromJson(json: JsonView, variables: Map<string, Variable>): StockFlowView {
let maxUid = -1;
let namedElements = Map<string, UID>();
Expand Down Expand Up @@ -1243,6 +1240,7 @@ export class StockFlowView extends Record(stockFlowViewDefaults) {
nextUid,
viewBox,
zoom: json.zoom ?? 1,
useLetteredPolarity: json.useLetteredPolarity ?? false,
});
}
toJson(): JsonView {
Expand Down Expand Up @@ -1282,6 +1280,10 @@ export class StockFlowView extends Record(stockFlowViewDefaults) {
result.zoom = this.zoom;
}

if (this.useLetteredPolarity) {
result.useLetteredPolarity = true;
}

return result;
}
}
Expand Down Expand Up @@ -1343,11 +1345,55 @@ export class LoopMetadata extends Record(loopMetadataDefaults) {
}
}

const modelGroupDefaults = {
name: '',
doc: undefined as string | undefined,
parent: undefined as string | undefined,
members: List<string>(),
runEnabled: false,
};

/**
* Semantic/organizational group for categorizing model variables.
* This is distinct from visual diagram groups (GroupViewElement).
*/
export class ModelGroup extends Record(modelGroupDefaults) {
constructor(props: typeof modelGroupDefaults) {
super(props);
}
static fromJson(json: JsonModelGroup): ModelGroup {
return new ModelGroup({
name: json.name,
doc: json.doc,
parent: json.parent,
members: List(json.members ?? []),
runEnabled: json.runEnabled ?? false,
});
}
toJson(): JsonModelGroup {
const result: JsonModelGroup = {
name: this.name,
members: this.members.toArray(),
};
if (this.doc) {
result.doc = this.doc;
}
if (this.parent) {
result.parent = this.parent;
}
if (this.runEnabled) {
result.runEnabled = this.runEnabled;
}
return result;
}
}

const modelDefaults = {
name: '',
variables: Map<string, Variable>(),
views: List<StockFlowView>(),
loopMetadata: List<LoopMetadata>(),
groups: List<ModelGroup>(),
};
export class Model extends Record(modelDefaults) {
// this isn't useless, as it ensures we specify the full object
Expand All @@ -1370,6 +1416,7 @@ export class Model extends Record(modelDefaults) {
variables,
views: List((json.views ?? []).map((view: JsonView) => StockFlowView.fromJson(view, variables))),
loopMetadata: List((json.loopMetadata ?? []).map((lm: JsonLoopMetadata) => LoopMetadata.fromJson(lm))),
groups: List((json.groups ?? []).map((g: JsonModelGroup) => ModelGroup.fromJson(g))),
});
}
toJson(): JsonModel {
Expand Down Expand Up @@ -1406,6 +1453,9 @@ export class Model extends Record(modelDefaults) {
if (this.loopMetadata.size > 0) {
result.loopMetadata = this.loopMetadata.map((lm: LoopMetadata) => lm.toJson()).toArray();
}
if (this.groups.size > 0) {
result.groups = this.groups.map((g: ModelGroup) => g.toJson()).toArray();
}

return result;
}
Expand Down Expand Up @@ -1439,7 +1489,7 @@ export class Dt extends Record(dtDefaults) {
}
}

export type SimMethod = 'euler' | 'rk4';
export type SimMethod = 'euler' | 'rk2' | 'rk4';

const simSpecsDefaults = {
start: 0,
Expand Down
Loading
Loading