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

fix: allow for resetting a lookup at runtime back to its original data #593

Merged
merged 7 commits into from
Dec 18, 2024
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