Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow for creating a LookupDef without manually initializing a ModelListing #502

Merged
merged 12 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 0 additions & 1 deletion examples/house-game/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"dependencies": {
"@sdeverywhere/build": "^0.3.4",
"@sdeverywhere/cli": "^0.7.23",
"@sdeverywhere/plugin-config": "^0.2.4",
"@sdeverywhere/plugin-vite": "^0.1.8",
"@sdeverywhere/plugin-worker": "^0.2.3"
}
Expand Down
17 changes: 4 additions & 13 deletions examples/house-game/packages/app/src/model/app-model.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
import { Readable, Writable, get, writable } from 'svelte/store'

import type { ModelRunner, Point } from '@sdeverywhere/runtime'
import { ModelListing, Outputs, createLookupDef } from '@sdeverywhere/runtime'
import { Outputs, createLookupDef } from '@sdeverywhere/runtime'

import { spawnAsyncModelRunner } from '@sdeverywhere/runtime-async'

import modelListingJson from './generated/listing.json?raw'
import modelWorkerJs from './generated/worker.js?raw'
import { AppState, inputValuesForState, stateForIndex } from './app-state'

/**
* Create an `AppModel` instance.
*/
export async function createAppModel(): Promise<AppModel> {
// Load the model listing
const listing = new ModelListing(modelListingJson)

// Initialize the generated model asynchronously. We inline the worker code in the
// rolled-up bundle, so that we don't have to fetch a separate `worker.js` file.
const runner = await spawnAsyncModelRunner({ source: modelWorkerJs })
const outputs = runner.createOutputs()

// Create the `AppModel` instance
return new AppModel(listing, runner, outputs)
return new AppModel(runner, outputs)
}

/**
Expand All @@ -45,11 +41,7 @@ export class AppModel {

public onDataUpdated: (outputs: Outputs, maxTime: number) => void

constructor(
private readonly listing: ModelListing,
private readonly runner: ModelRunner,
private readonly outputs: Outputs
) {
constructor(private readonly runner: ModelRunner, private readonly outputs: Outputs) {
this.writableBusy = writable(false)
this.busy = this.writableBusy

Expand Down Expand Up @@ -128,8 +120,7 @@ export class AppModel {
}
]
}
const gameDataVarSpec = this.listing.varSpecs.get('_planning_data')
const gameLookup = createLookupDef(gameDataVarSpec, this.gameLookupPoints)
const gameLookup = createLookupDef({ varName: 'planning data' }, this.gameLookupPoints)
const lookups = [gameLookup]

// Set the "busy" flag (to put the UI into a non-editable state)
Expand Down
4 changes: 0 additions & 4 deletions examples/house-game/sde.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@ export async function config() {
}
},

// Copy the generated model listing to the app so that it can be loaded
// at runtime
outListingFile: generatedFilePath('listing.json'),

plugins: [
// Generate a `worker.js` file that runs the generated model in a worker
workerPlugin({
Expand Down
10 changes: 10 additions & 0 deletions packages/compile/src/generate/gen-code-js.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ let codeGenerator = (parsedModel, opts) => {
code += emitInitLevelsCode()
code += emitEvalCode()
code += emitIOCode()
code += emitModelListing()
code += emitDefaultFunction()
return code
}
Expand Down Expand Up @@ -513,6 +514,14 @@ ${section(chunk)}
//
// Module exports
//
function emitModelListing() {
const minimalListingJson = JSON.stringify(Model.jsonList().minimal, null, 2)
const minimalListingJs = minimalListingJson.replace(/"(\w+)"\s*:/g, '$1:').replaceAll('"', "'")
return `\
/*export*/ const modelListing = ${minimalListingJs}

`
}
function emitDefaultFunction() {
// TODO: For now, the default function returns an object that has the shape of the
// `JsModel` interface. It is an async function for future proofing and so that it
Expand All @@ -530,6 +539,7 @@ export default async function () {
kind: 'js',
outputVarIds,
outputVarNames,
modelListing,

getInitialTime,
getFinalTime,
Expand Down
96 changes: 96 additions & 0 deletions packages/compile/src/generate/gen-code-js.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,11 +533,107 @@ function evalAux0() {
}
}

/*export*/ const modelListing = {
dimensions: [
{
id: '_dima',
subIds: [
'_a1',
'_a2'
]
},
{
id: '_dimb',
subIds: [
'_b1',
'_b2'
]
}
],
variables: [
{
id: '_final_time',
index: 1
},
{
id: '_initial_time',
index: 2
},
{
id: '_saveper',
index: 3
},
{
id: '_time_step',
index: 4
},
{
id: '_input',
index: 5
},
{
id: '_a_data',
dimIds: [
'_dima'
],
index: 6
},
{
id: '_b_data',
dimIds: [
'_dima',
'_dimb'
],
index: 7
},
{
id: '_c_data',
index: 8
},
{
id: '_a',
dimIds: [
'_dima'
],
index: 9
},
{
id: '_b',
dimIds: [
'_dima',
'_dimb'
],
index: 10
},
{
id: '_c',
index: 11
},
{
id: '_x',
index: 12
},
{
id: '_w',
index: 13
},
{
id: '_y',
index: 14
},
{
id: '_z',
index: 15
}
]
}

export default async function () {
return {
kind: 'js',
outputVarIds,
outputVarNames,
modelListing,

getInitialTime,
getFinalTime,
Expand Down
71 changes: 60 additions & 11 deletions packages/compile/src/model/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import Variable from './variable.js'
let variables = []
let inputVars = []
let constantExprs = new Map()
let cachedVarIndexInfo
let cachedJsonList

// Also keep variables in a map (with `varName` as key) for faster lookup
const variablesByName = new Map()
Expand All @@ -44,6 +46,8 @@ function resetModelState() {
variablesByName.clear()
constantExprs.clear()
nonAtoANames = Object.create(null)
cachedVarIndexInfo = undefined
cachedJsonList = undefined
}

/**
Expand Down Expand Up @@ -1118,37 +1122,82 @@ function varIndexInfo() {
// varType
// varIndex
// subscriptCount
return Array.from(varIndexInfoMap().values())
if (cachedVarIndexInfo) {
return cachedVarIndexInfo
}
cachedVarIndexInfo = Array.from(varIndexInfoMap().values())
return cachedVarIndexInfo
}

function jsonList() {
// Return a stringified JSON object containing variable and subscript information
// for the model.
// Return an object containing variable and subscript information for the model
// that will be used to write the JSON model listing files.
if (cachedJsonList) {
return cachedJsonList
}

// Get the set of available subscripts
const allDims = [...allDimensions()]
const sortedDims = allDims.sort((a, b) => a.name.localeCompare(b.name))
const sortedFullDims = allDims.sort((a, b) => a.name.localeCompare(b.name))

// Extract a subset of the available info for each variable and put them in eval order
const sortedVars = filteredListedVars()
const sortedFullVars = filteredListedVars()

// Assign a 1-based index for each variable that has data that can be accessed.
// This matches the index number used in `storeOutput` and `setLookup` in the
// generated C/JS code
const infoMap = varIndexInfoMap()
for (const v of sortedVars) {
for (const v of sortedFullVars) {
const varInfo = infoMap.get(v.varName)
if (varInfo) {
v.varIndex = varInfo.varIndex
}
}

// Convert to JSON
const obj = {
dimensions: sortedDims,
variables: sortedVars
// Derive minimal versions of the full arrays; these only contain the minimal
// subset of fields that are needed by the `ModelListing` class from the
// runtime package. The property names in the minimal objects are slightly
// different than the full ones to better match the latest naming used in the
// compile and runtime packages.
const sortedMinimalDims = sortedFullDims.map(d => {
return {
id: d.name,
subIds: d.value
}
})

// Note that `sortedFullVars` may contain duplicates in the case of separated
// variables, but for the minimal listing we only want to have one entry per
// index (i.e., one entry for each base variable ID), so we filter out the
// duplicates here.
const baseIds = new Set()
const sortedMinimalVars = []
for (const v of sortedFullVars) {
const baseId = v.varName
if (!baseIds.has(baseId)) {
baseIds.add(baseId)

const varInfo = {}
varInfo.id = baseId
if (v.families) {
varInfo.dimIds = v.families
}
varInfo.index = v.varIndex
sortedMinimalVars.push(varInfo)
}
}

cachedJsonList = {
full: {
dimensions: sortedFullDims,
variables: sortedFullVars
},
minimal: {
dimensions: sortedMinimalDims,
variables: sortedMinimalVars
}
}
return JSON.stringify(obj, null, 2)
return cachedJsonList
}

export default {
Expand Down
Loading