Skip to content

Commit

Permalink
fix: allow for resetting a lookup at runtime back to its original data (
Browse files Browse the repository at this point in the history
#593)

Fixes #592
  • Loading branch information
chrispcampbell authored Dec 18, 2024
1 parent e2a1e71 commit cfec212
Show file tree
Hide file tree
Showing 20 changed files with 517 additions and 169 deletions.
98 changes: 76 additions & 22 deletions packages/cli/src/c/vensim.c
Original file line number Diff line number Diff line change
Expand Up @@ -56,25 +56,79 @@ double _ZIDZ(double a, double b) {
Lookup* __new_lookup(size_t size, bool copy, double* data) {
// Make a new Lookup with "size" number of pairs given in x, y order in a flattened list.
Lookup* lookup = malloc(sizeof(Lookup));
lookup->n = size;
lookup->inverted_data = NULL;
lookup->data_is_owned = copy;
lookup->original_size = size;
lookup->original_data_is_owned = copy;
if (copy) {
// Copy array into the lookup data.
lookup->data = malloc(sizeof(double) * 2 * size);
memcpy(lookup->data, data, size * 2 * sizeof(double));
// Copy the given lookup data into an internally managed buffer.
size_t data_length_in_bytes = size * 2 * sizeof(double);
lookup->original_data = malloc(data_length_in_bytes);
memcpy(lookup->original_data, data, data_length_in_bytes);
} else {
// Store a pointer to the lookup data (assumed to be static or owned elsewhere).
lookup->data = data;
lookup->original_data = data;
}
// Set the original data as "active".
lookup->active_data = lookup->original_data;
lookup->active_size = lookup->original_size;
// Set `dynamic_data` to NULL initially; it will be allocated on demand if lookup
// data is overridden at runtime using `__set_lookup`.
lookup->dynamic_data = NULL;
lookup->dynamic_data_length = 0;
lookup->dynamic_size = 0;
// Set `inverted_data` to NULL initially; it will be allocated on demand in case
// of `LOOKUP INVERT` calls.
lookup->inverted_data = NULL;
// Set the cached "last" values to the initial values.
lookup->last_input = DBL_MAX;
lookup->last_hit_index = 0;
return lookup;
}
void __set_lookup(Lookup* lookup, size_t size, double* data) {
// Set new data for the given `Lookup`. If `data` is NULL, the original data that was
// supplied to the `__new_lookup` call will be restored as the "active" data. Otherwise,
// `data` will be copied to an internal data buffer, which will be the "active" data.
// If `size` is greater than the size passed to previous calls, the internal data buffer
// will be grown as needed.
if (lookup == NULL) {
return;
}
if (data != NULL) {
// Allocate or grow the internal buffer as needed.
size_t data_length_in_elems = size * 2;
size_t data_length_in_bytes = data_length_in_elems * sizeof(double);
if (data_length_in_elems > lookup->dynamic_data_length) {
lookup->dynamic_data = malloc(data_length_in_bytes);
lookup->dynamic_data_length = data_length_in_elems;
}
// Copy the given lookup data into the internally managed buffer.
lookup->dynamic_size = size;
if (data_length_in_bytes > 0) {
memcpy(lookup->dynamic_data, data, data_length_in_bytes);
}
// Set the dynamic data as the "active" data.
lookup->active_data = lookup->dynamic_data;
lookup->active_size = lookup->dynamic_size;
} else {
// Restore the original data as the "active" data.
lookup->active_data = lookup->original_data;
lookup->active_size = lookup->original_size;
}
// Clear the cached inverted data, if needed.
if (lookup->inverted_data) {
free(lookup->inverted_data);
lookup->inverted_data = NULL;
}
// Reset the cached "last" values to the initial values.
lookup->last_input = DBL_MAX;
lookup->last_hit_index = 0;
}
void __delete_lookup(Lookup* lookup) {
if (lookup) {
if (lookup->data && lookup->data_is_owned) {
free(lookup->data);
if (lookup->original_data && lookup->original_data_is_owned) {
free(lookup->original_data);
}
if (lookup->dynamic_data) {
free(lookup->dynamic_data);
}
if (lookup->inverted_data) {
free(lookup->inverted_data);
Expand All @@ -84,8 +138,8 @@ void __delete_lookup(Lookup* lookup) {
}
void __print_lookup(Lookup* lookup) {
if (lookup) {
for (size_t i = 0; i < lookup->n; i++) {
printf("(%g, %g)\n", *(lookup->data + 2 * i), *(lookup->data + 2 * i + 1));
for (size_t i = 0; i < lookup->active_size; i++) {
printf("(%g, %g)\n", *(lookup->active_data + 2 * i), *(lookup->active_data + 2 * i + 1));
}
}
}
Expand All @@ -94,12 +148,12 @@ double __lookup(Lookup* lookup, double input, bool use_inverted_data, LookupMode
// Interpolate the y value from an array of (x,y) pairs.
// NOTE: The x values are assumed to be monotonically increasing.

if (lookup == NULL || lookup->n == 0) {
if (lookup == NULL || lookup->active_size == 0) {
return _NA_;
}

const double* data = use_inverted_data ? lookup->inverted_data : lookup->data;
const size_t max = (lookup->n) * 2;
const double* data = use_inverted_data ? lookup->inverted_data : lookup->active_data;
const size_t max = (lookup->active_size) * 2;

// Use the cached values for improved lookup performance, except in the case
// of `LOOKUP INVERT` (since it may not be accurate if calls flip back and forth
Expand Down Expand Up @@ -166,12 +220,12 @@ double __get_data_between_times(Lookup* lookup, double input, LookupMode mode) {
// Interpolate the y value from an array of (x,y) pairs.
// NOTE: The x values are assumed to be monotonically increasing.

if (lookup == NULL || lookup->n == 0) {
if (lookup == NULL || lookup->active_size == 0) {
return _NA_;
}

const double* data = lookup->data;
const size_t n = lookup->n;
const double* data = lookup->active_data;
const size_t n = lookup->active_size;
const size_t max = n * 2;

switch (mode) {
Expand Down Expand Up @@ -248,10 +302,10 @@ double __get_data_between_times(Lookup* lookup, double input, LookupMode mode) {
double _LOOKUP_INVERT(Lookup* lookup, double y) {
if (lookup->inverted_data == NULL) {
// Invert the matrix and cache it.
lookup->inverted_data = malloc(sizeof(double) * 2 * lookup->n);
double* pLookup = lookup->data;
lookup->inverted_data = malloc(sizeof(double) * 2 * lookup->active_size);
double* pLookup = lookup->active_data;
double* pInvert = lookup->inverted_data;
for (size_t i = 0; i < lookup->n; i++) {
for (size_t i = 0; i < lookup->active_size; i++) {
*pInvert++ = *(pLookup + 1);
*pInvert++ = *pLookup;
pLookup += 2;
Expand All @@ -261,12 +315,12 @@ double _LOOKUP_INVERT(Lookup* lookup, double y) {
}

double _GAME(Lookup* lookup, double default_value) {
if (lookup == NULL || lookup->n <= 0) {
if (lookup == NULL || lookup->active_size == 0) {
// The lookup is NULL or empty, so return the default value
return default_value;
}

double x0 = lookup->data[0];
double x0 = lookup->active_data[0];
if (_time < x0) {
// The current time is earlier than the first data point, so return the
// default value
Expand Down
35 changes: 32 additions & 3 deletions packages/cli/src/c/vensim.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,44 @@ typedef enum {
} LookupMode;

typedef struct {
double* data;
size_t n;
// The pointer to the active data buffer. This will be the same as either
// `original_data` or `dynamic_data`, depending on whether the lookup data
// is overridden at runtime using `__set_lookup`.
double* active_data;
// The size (i.e., number of pairs) of the active data.
size_t active_size;

// The pointer to the original data buffer.
double* original_data;
// The size (i.e., number of pairs) of the original data.
size_t original_size;
// Whether the `original_data` is owned by this `Lookup` instance. If `true`,
// the `original_data` buffer will be freed by `__delete_lookup`.
bool original_data_is_owned;

// The pointer to the dynamic data buffer. This will be NULL initially,
// and the buffer will be allocated (or grown) by `__set_lookup`.
double* dynamic_data;
// The size (i.e., number of pairs) of the dynamic data.
size_t dynamic_size;
// The number of elements in the dynamic data buffer. The buffer will grow
// as needed, so this can be greater than `2 * dynamic_size`.
size_t dynamic_data_length;

// The inverted version of the active data buffer. This is allocated on demand
// only in the case of `LOOKUP INVERT` function calls.
double* inverted_data;
bool data_is_owned;

// The input value for the last hit. This is cached for performance so that we
// can reduce the amount of linear searching in the common case where `LOOKUP`
// input values are monotonically increasing.
double last_input;
// The index for the last hit (see `last_input`).
size_t last_hit_index;
} Lookup;

Lookup* __new_lookup(size_t size, bool copy, double* data);
void __set_lookup(Lookup* lookup, size_t size, double* data);
void __delete_lookup(Lookup* lookup);
void __print_lookup(Lookup* lookup);

Expand Down
25 changes: 10 additions & 15 deletions packages/compile/src/generate/gen-code-c.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,19 @@ ${chunkedFunctions('evalLevels', Model.levelVars(), ' // Evaluate levels.')}`
let setLookupBody
if (spec.customLookups === true || Array.isArray(spec.customLookups)) {
setLookupBody = `\
Lookup** pLookup = NULL;
switch (varIndex) {
${setLookupImpl(Model.varIndexInfo(), spec.customLookups)}
default:
fprintf(stderr, "No lookup found for var index %zu in setLookup\\n", varIndex);
break;
}
if (pLookup != NULL) {
if (*pLookup == NULL) {
*pLookup = __new_lookup(numPoints, /*copy=*/true, points);
} else {
__set_lookup(*pLookup, numPoints, points);
}
}`
} else {
let msg = 'The setLookup function was not enabled for the generated model. '
Expand Down Expand Up @@ -200,19 +208,6 @@ void setInputsFromBuffer(double* inputData) {
${inputsFromBufferImpl()}
}
void replaceLookup(Lookup** lookup, double* points, size_t numPoints) {
if (lookup == NULL) {
return;
}
if (*lookup != NULL) {
__delete_lookup(*lookup);
*lookup = NULL;
}
if (points != NULL) {
*lookup = __new_lookup(numPoints, /*copy=*/true, points);
}
}
void setLookup(size_t varIndex, size_t* subIndices, double* points, size_t numPoints) {
${setLookupBody}
}
Expand Down Expand Up @@ -441,7 +436,7 @@ ${section(chunk)}
return inputVars.join('\n')
}
function setLookupImpl(varIndexInfo, customLookups) {
// Emit `replaceLookup` calls for all lookups and data variables that can be overridden
// Emit case statements for all lookups and data variables that can be overridden
// at runtime
let includeCase
if (Array.isArray(customLookups)) {
Expand All @@ -467,7 +462,7 @@ ${section(chunk)}
}
let c = ''
c += ` case ${info.varIndex}:\n`
c += ` replaceLookup(&${lookupVar}, points, numPoints);\n`
c += ` pLookup = &${lookupVar};\n`
c += ` break;`
return c
})
Expand Down
44 changes: 25 additions & 19 deletions packages/compile/src/generate/gen-code-c.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ bool data_initialized = false;
void initLookups0() {
__lookup1 = __new_lookup(6, /*copy=*/false, __lookup1_data_);
for (size_t i = 0; i < 2; i++) {
_d_game_inputs[i] = __new_lookup(0, /*copy=*/false, NULL);
}
}
void initLookups() {
Expand Down Expand Up @@ -264,37 +267,32 @@ void setInputsFromBuffer(double* inputData) {
_input = inputData[0];
}
void replaceLookup(Lookup** lookup, double* points, size_t numPoints) {
if (lookup == NULL) {
return;
}
if (*lookup != NULL) {
__delete_lookup(*lookup);
*lookup = NULL;
}
if (points != NULL) {
*lookup = __new_lookup(numPoints, /*copy=*/true, points);
}
}
void setLookup(size_t varIndex, size_t* subIndices, double* points, size_t numPoints) {
Lookup** pLookup = NULL;
switch (varIndex) {
case 6:
replaceLookup(&_d_game_inputs[subIndices[0]], points, numPoints);
pLookup = &_d_game_inputs[subIndices[0]];
break;
case 7:
replaceLookup(&_a_data[subIndices[0]], points, numPoints);
pLookup = &_a_data[subIndices[0]];
break;
case 8:
replaceLookup(&_b_data[subIndices[0]][subIndices[1]], points, numPoints);
pLookup = &_b_data[subIndices[0]][subIndices[1]];
break;
case 9:
replaceLookup(&_c_data, points, numPoints);
pLookup = &_c_data;
break;
default:
fprintf(stderr, "No lookup found for var index %zu in setLookup\\n", varIndex);
break;
}
if (pLookup != NULL) {
if (*pLookup == NULL) {
*pLookup = __new_lookup(numPoints, /*copy=*/true, points);
} else {
__set_lookup(*pLookup, numPoints, points);
}
}
}
const char* getHeader() {
Expand Down Expand Up @@ -418,17 +416,25 @@ void setLookup(size_t varIndex, size_t* subIndices, double* points, size_t numPo
})
expect(code).toMatch(`\
void setLookup(size_t varIndex, size_t* subIndices, double* points, size_t numPoints) {
Lookup** pLookup = NULL;
switch (varIndex) {
case 6:
replaceLookup(&_q_data, points, numPoints);
pLookup = &_q_data;
break;
case 7:
replaceLookup(&_y_data[subIndices[0]], points, numPoints);
pLookup = &_y_data[subIndices[0]];
break;
default:
fprintf(stderr, "No lookup found for var index %zu in setLookup\\n", varIndex);
break;
}
if (pLookup != NULL) {
if (*pLookup == NULL) {
*pLookup = __new_lookup(numPoints, /*copy=*/true, points);
} else {
__set_lookup(*pLookup, numPoints, points);
}
}
}`)
})

Expand Down
11 changes: 8 additions & 3 deletions packages/compile/src/generate/gen-code-js.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,15 @@ ${chunkedFunctions('evalLevels', true, Model.levelVars(), ' // Evaluate levels'
}
const varIndex = varSpec.varIndex;
const subs = varSpec.subscriptIndices;
let lookup;
switch (varIndex) {
${setLookupImpl(Model.varIndexInfo(), spec.customLookups)}
default:
throw new Error(\`No lookup found for var index \${varIndex} in setLookup\`);
}
if (lookup) {
const size = points ? points.length / 2 : 0;
lookup.setData(size, points);
}`
} else {
let msg = 'The setLookup function was not enabled for the generated model. '
Expand Down Expand Up @@ -307,7 +312,7 @@ ${customOutputSection(Model.varIndexInfo(), spec.customOutputs)}
return `\
/*export*/ function setInputs(valueAtIndex /*: (index: number) => number*/) {${inputsFromBufferImpl()}}
/*export*/ function setLookup(varSpec /*: VarSpec*/, points /*: Float64Array*/) {
/*export*/ function setLookup(varSpec /*: VarSpec*/, points /*: Float64Array | undefined*/) {
${setLookupBody}
}
Expand Down Expand Up @@ -517,7 +522,7 @@ ${section(chunk)}
return inputVars
}
function setLookupImpl(varIndexInfo, customLookups) {
// Emit `createLookup` calls for all lookups and data variables that can be overridden
// Emit case statements for all lookups and data variables that can be overridden
// at runtime
let overrideAllowed
if (Array.isArray(customLookups)) {
Expand All @@ -543,7 +548,7 @@ ${section(chunk)}
}
let c = ''
c += ` case ${info.varIndex}:\n`
c += ` ${lookupVar} = fns.createLookup(points.length / 2, points);\n`
c += ` lookup = ${lookupVar};\n`
c += ` break;`
return c
})
Expand Down
Loading

0 comments on commit cfec212

Please sign in to comment.