"""
Functions for parsing the subscript from a .mdl file using PySD.
"""
import warnings
import re
import textwrap
import string
import json
import numpy as np
from openpyxl.workbook.defined_name import DefinedName
from .utils.excels import Excels
from .utils.subscripts import Subscripts
class ExternalVariable(object):
def __init__(self, var_name, dims, cell, description, units, file, sheet):
self.var_name = var_name.strip()
self.base_name = self._clean_identifier(self.var_name)
if self.base_name != self.var_name:
warnings.warn(
f"The name of the variable '{self.var_name}' has special "
+ f"characters. '{self.base_name}' will be used for "
+ "cellrange names.")
self.description = description.strip()
self.units = units.strip()
self.dims = [dim.strip() for dim in dims]
self.file = file
self.sheet = sheet
self.dims_dict = {}
self.cell = cell
self.ref_row, self.ref_col = self._split_excel_cell(cell)
self.subscripts_warns = set()
def add_dimension(self, dim_name, read_along, sep=1):
"""
Add dimension to an object.
Parameters
----------
dim_name: str
Name of the subscript range to add, it must be an existing
subscript range or subrange.
read_along: str ('col' or 'row' or 'sheet' or 'file')
Dimension to read along the subscript range.
sep: int (for 'col' or 'row') or list (for 'sheet' or 'file')
The separator between different elements of the subscript range.
For read_along='row' or 'read_along='col' it must be the number
of rows or columns between different elements of the subscript
range. For read_along='sheet' or 'read_along='file' it must
be the list of sheets or files where the subscript range.
Returns
-------
None
"""
if dim_name.strip() not in Subscripts.get_ranges():
raise ValueError(
f"\n'{dim_name}' is not in the list of subscript ranges:\n\t"
+ str(Subscripts.get_ranges()))
elif read_along not in ['col', 'row', 'sheet', 'file']:
raise ValueError(
"\nread_along must be 'row', 'col', 'sheet' or 'file'."
)
self.dims_dict[dim_name.strip()] = (read_along, sep)
def add_series(self, name, cell, read_along, length):
"""
Add series element x or time for LOOKUPS or DATA type objects.
Parameters
----------
name: str
Name of the series cellrange.
cell: str
Reference cell.
read_along: str ('col' or 'row')
Dimension to read along the series.
length: int
The length of the series.
Returns
-------
None
"""
cname = self._clean_identifier(name)
if name.strip() != cname:
warnings.warn(
f"The name of the interpolation dimension '{name.strip()}'"
f" has special characters. '{cname}' will be used for "
+ "cellrange names.")
self.series = {
'name': cname,
'cell': cell,
'read_along': read_along,
'length': length}
ref_row, ref_col = self._split_excel_cell(cell)
if read_along == 'row':
rows = [ref_row, ref_row + length - 1]
cols = [ref_col, ref_col]
elif read_along == 'col':
rows = [ref_row, ref_row]
cols = [ref_col, ref_col + length - 1]
else:
raise ValueError(
"\nread_along must be 'row' or 'col'."
)
# add series cellrange name without specifiying the sheet
self.series['cellrange'] = '$%s$%s:$%s$%s' % (
self._num_to_col(cols[0]), rows[0] + 1,
self._num_to_col(cols[1]), rows[1] + 1)
def _update_series_cellranges(self, sheets, files):
"""
Add sheets to the cell ranges of the series
Parameters
----------
sheets: set
The set of the sheets to add cellranges.
files: set
The set of the files to add cellranges.
Returns
-------
None
"""
self.series['cellrange'] = [
sheet + '!' + self.series['cellrange']
for sheet in sheets for file in files]
self.series['file'] = [
file
for sheet in sheets for file in files]
self.series['sheet'] = [
sheet
for sheet in sheets for file in files]
self.series['name'] =\
[self.series['name']] * len(self.series['cellrange'])
def _build_boxes(self, visited):
"""
Using the information of the dims_dict, builds the cellrange
boxes.
Parameters
----------
visited: list
List of the visited read_along elements with sep=1. It is
used for specify the dimension of the series in DATA and LOOKUPS.
Returns
-------
visited: list
List of the visited read_along elements with sep=1,
excluding 'file' and 'sheet'. Is needed for constants to
add '*' for transpositions.
"""
for dim in self.dims:
read_along, step = self.dims_dict[dim]
if step == 1:
# append only subscript range name
self._add_info([dim],
read_along,
len(Subscripts.get(dim))-1)
visited.append(read_along)
elif isinstance(step, int):
# append list of subscripts in subscript range
self._add_info(Subscripts.get(dim),
read_along,
range(0, step*len(Subscripts.get(dim)), step))
# steps: [0, step, 2*step, ..., (n_subs-1)*step]
else:
# read along file of sheet
# append list of subscripts in subscript range
self._add_info(Subscripts.get(dim),
read_along,
step)
visited.append(read_along)
# raise warnings only once per dimension
for swarn in self.subscripts_warns:
warnings.warn(swarn)
for dim in ['col', 'row']:
if visited.count(dim) > 1:
raise ValueError(
f"\nTwo or more dimensions are defined along {dim}"
" with step 1.")
for dim in ['file', 'sheet']:
if visited.count(dim) == 0:
# no dimension defined along sheet or file
self.elements[dim] =\
[getattr(self, dim)] * len(self.elements[dim])
elif visited.count(dim) == 1:
# 1 dimension defined along sheet or file, remove it from
# dim for transpositions in CONSTANTS
visited.remove(dim)
else:
# 2 or more dimensions defined along sheet or file
raise ValueError(
f"\nTwo or more dimensions are defined along {dim}.")
# convert cols to alpha
self.elements['col'] = [[self._num_to_col(col) for col in element]
for element in self.elements['col']]
# convert rows to excel numbering
self.elements['row'] = [[row+1 for row in element]
for element in self.elements['row']]
# writting information
self.elements['cellrange'] = [
'%s!$%s$%s:$%s$%s' % (sheet, cols[0], rows[0], cols[1], rows[1])
for sheet, cols, rows in zip(self.elements['sheet'],
self.elements['col'],
self.elements['row'])
]
return visited
def _add_info(self, subs, read_along, steps=None):
"""
Combine several list with elements of a given list
Parameters
----------
subs: list
List of current subscripts.
read_along: str ('col' or 'row' or 'sheet' or 'file')
Dimension to read along the subscript range.
steps: int, ndarray or None (optional)
Default is None.
Returns
-------
list_out: list of lists
"""
# add the subs
list_out = []
for element1 in self.elements['subs']:
for element2 in subs:
list_out.append(element1 + [element2])
self.elements['subs'] = list_out
# dimension gives the table shape
if isinstance(steps, int):
list_out = []
for coord in self.elements[read_along]:
list_out.append(coord + np.array([0, steps]))
self.elements[read_along] = list_out
return
# duplicate the rows, cols, file and sheet values if not given
coords_to_duplicate = [
along for along in ['col', 'row', 'file', 'sheet']
if along != read_along]
for along in coords_to_duplicate:
list_out = []
for coord in self.elements[along]:
for step in steps:
list_out.append(coord)
self.elements[along] = list_out
list_out = []
names_out = []
if read_along in ['col', 'row']:
# udpate cols or rows to read
for coord in self.elements[read_along]:
for step in steps:
list_out.append(coord + step)
self.elements[read_along] = list_out
for current_name in self.elements['cellname']:
for sub in subs:
subc = self._clean_identifier(sub)
if subc != sub.strip():
self.subscripts_warns.add(
f"The name of the subscript '{sub.strip()}'"
+ f" has special characters. '{subc}' will be"
+ " used for cellrange names.")
names_out.append(current_name + '_' + subc)
self.elements['cellname'] = names_out
else:
# update file or sheet to read
for name in self.elements['cellname']:
for step in steps:
list_out.append(step)
names_out.append(name)
self.elements[read_along] = list_out
self.elements['cellname'] = names_out
def _write_cellranges(self, names, files, sheets, cellranges):
"""
Loop for writting several cellranges in excel file.
Parameters
----------
names: list
List of names of cellranges.
files: list
List of files to write each cellrange in.
sheets: list
List of sheets to write each cellrange in.
cellranges: list
List of cellranges to write.
Returns
-------
written_names: list
The list of the written cell range names.
"""
written_names = []
for name, file, sheet, cellrange in zip(names,
files,
sheets,
cellranges):
self._write_cellrange(name, file, sheet, cellrange, self.force)
written_names.append(name)
return written_names
@staticmethod
def _write_cellrange(name, file, sheet, cellrange, force):
"""
Writes cellranges using openpyxl
Parameters
----------
name: str
The name of the cellrange.
file: str
The name of the file to write the cellrange in.
sheet: str
The name of the sheet to write the cellrange in.
cellrange: str
The cellrange to write.
Returns
-------
None
"""
wb = Excels.read(file)
for sheetId, sheet1 in enumerate(wb.sheetnames):
if sheet1.lower() == sheet.lower():
local_cellranges = wb[sheet1].defined_names
break
if name in local_cellranges:
if local_cellranges.get(name).attr_text == cellrange:
# cellrange already defined with same name and coordinates
return
elif force:
del local_cellranges[name]
else:
raise ValueError(
f"\nTrying to write a cellrange with name '{name}' at "
+ f"'{cellrange}'. However, '{name}' already exist in "
+ f"'{local_cellranges.get(name).attr_text}'\n"
+ "Use force=True to overwrite it.")
new_range = DefinedName(
name, attr_text=cellrange, localSheetId=sheetId)
local_cellranges.add(new_range)
@staticmethod
def _col_to_num(col):
"""
Transforms the column name to int.
Parameters
----------
col: str
Column name.
Returns
-------
int
Column number starting from 0.
"""
if len(col) == 1:
return ord(col.upper()) - ord('A')
elif len(col) == 2:
left = ord(col[0].upper()) - ord('A') + 1
right = ord(col[1].upper()) - ord('A')
return left * (ord('Z')-ord('A')+1) + right
else:
left = ord(col[0].upper()) - ord('A') + 1
center = ord(col[1].upper()) - ord('A') + 1
right = ord(col[2].upper()) - ord('A')
return left * ((ord('Z')-ord('A')+1)**2)\
+ center * (ord('Z')-ord('A')+1)\
+ right
@staticmethod
def _num_to_col(num):
"""
Transforms the column number to name.
Parameters
----------
num: int
Column number starting from 0.
Returns
-------
str
Column name.
"""
chars = []
num += 1
def divmod_excel(n):
a, b = divmod(n, 26)
if b == 0:
return a - 1, b + 26
return a, b
while num > 0:
num, d = divmod_excel(num)
chars.append(string.ascii_uppercase[d-1])
return ''.join(reversed(chars))
@classmethod
def _split_excel_cell(cls, cell):
"""
Splits a cell value given in a string.
Returns None for non-valid cell formats.
Parameters
----------
cell: str
Cell like string, such as "A1", "b16", "AC19"...
If it is not a cell like string will return None.
Returns
-------
row number, column number: int, int
If the cell input is valid. Both numbers are given in Python
enumeration, i.e., first row and first column are 0.
"""
split = re.findall(r'\d+|\D+', cell)
try:
# check that we only have two values [column, row]
assert len(split) == 2
# check that the column name has no special characters
assert not re.compile('[^a-zA-Z]+').search(split[0])
# check that row number is not 0
assert int(split[1]) != 0
# the column name has as maximum 3 letters
assert len(split[0]) <= 3
return int(split[1])-1, cls._col_to_num(split[0])
except AssertionError:
return
@staticmethod
def _clean_identifier(string):
"""
Remove invalid characters and spaces from a string.
Parameters
----------
string: str
Original string.
Returns
-------
str
Clean string.
"""
return re.sub('[^A-Za-z0-9]+', '_', string).strip('_')
[docs]
class Lookups(ExternalVariable):
"""
Class for creating GET DIRECT/XLS LOOKUPS equations and cellranges.
Parameters
----------
var_name: str
The name of the variable in Vensim code and basestring for
cellrange names.
dims: list
List of the dimensions of the variable in the same order that
will be created the variable in the Vensim code.
cell: str
Reference cell of the data. First cellw with numeric values
(upper-left corner).
description: str (optional)
Description to include in the Vensim equations. By default no
description will be included.
units: str (optional)
Units to include in the Vensim equations. By default no
units will be included.
file: str (optional)
File where the data is. This argument is mandatory unless a
subscript range is defined across several files. Default is None.
sheet: str (optional)
Sheet where the data is. This argument is mandatory unless a
subscript range is defined across several sheets. Default is None.
"""
def __init__(self, var_name, dims, cell, description='', units='',
file=None, sheet=None, **kwargs):
super().__init__(var_name, dims, cell, description, units, file, sheet)
[docs]
def add_x(self, name, cell, read_along, length):
"""
Add x for LOOKUPS type objects.
Parameters
----------
name: str
Name of the x series cellrange.
cell: str
Reference cell.
read_along: str ('col' or 'row')
Dimension to read along the x series.
length: int
The length of the x series.
Returns
-------
None
"""
super().add_series(name, cell, read_along, length)
[docs]
def execute(self, force=False, loading='DIRECT'):
"""
Get vensim equations and write cell range names in the Excel file.
Parameters
----------
force: bool (optional)
If True and trying and tryting to write a cell range name
that already exist in other positions it will overwrite it
(not recommended). If False it will return and error when
trying to write the new cellrange name. Default is False.
loading: str (optional)
Vensing GET loading type it can be 'DIRECT' or 'XLS'.
Default is 'DIRECT'.
Returns
-------
vensim_eqs: str
The string of Vensim equations to copy in the model .mdl file.
"""
# force removal of conflicting cellrange names
self.force = force
vensim_eqs = self.get_vensim(loading)
# write series cellranges
super()._write_cellranges(
self.series['name'], self.series['file'],
self.series['sheet'], self.series['cellrange'])
# write data cellranges
super()._write_cellranges(
self.elements['cellname'], self.elements['file'],
self.elements['sheet'], self.elements['cellrange'])
# save changes and close Excel files
Excels.save_and_close()
return vensim_eqs
def get_vensim(self, force=False, loading='DIRECT'):
"""
Get vensim equations and write cell range names in the Excel file.
Parameters
----------
force: bool (optional)
If True and trying and tryting to write a cell range name
that already exist in other positions it will overwrite it
(not recommended). If False it will return and error when
trying to write the new cellrange name. Default is False.
loading: str (optional)
Vensing GET loading type it can be 'DIRECT' or 'XLS'.
Default is 'DIRECT'.
Returns
-------
vensim_eqs: str
The string of Vensim equations to copy in the model .mdl file.
"""
# force removal of conflicting cellrange names
self.force = force
self.elements = {
'row': [np.array([self.ref_row, self.ref_row], dtype=int)],
'col': [np.array([self.ref_col, self.ref_col], dtype=int)],
'subs': [[]],
'sheet': [[]],
'file': [[]],
'cellname': [self.base_name]
}
self.elements[self.series['read_along']][0][1] +=\
self.series['length'] - 1
super()._build_boxes([self.series['read_along']])
super()._update_series_cellranges(set(self.elements['sheet']),
set(self.elements['file']))
# generate Vensim equations
vensim_eqs = ""
for subs, file, sheet, cellname in zip(self.elements['subs'],
self.elements['file'],
self.elements['sheet'],
self.elements['cellname']):
if subs:
subs_write = f"[{', '.join(map(str, subs))}]"
else:
subs_write = ""
vensim_eq = """
%s=
\tGET_%s_LOOKUPS('%s', '%s', '%s', '%s') ~~|""" % (
self.var_name+subs_write, loading, file, sheet,
self.series['name'][0], cellname)
vensim_eqs += vensim_eq
vensim_eqs = textwrap.dedent(vensim_eqs)
vensim_eqs = vensim_eqs[:-4] + f'\n\t~\t{self.units}'\
+ f'\n\t~\t{self.description}\n\t|'
return vensim_eqs
[docs]
class Data(ExternalVariable):
"""
Class for creating GET DIRECT/XLS DATA equations and cellranges.
Parameters
----------
var_name: str
The name of the variable in Vensim code and basestring for
cellrange names.
dims: list
List of the dimensions of the variable in the same order that
will be created the variable in the Vensim code.
cell: str
Reference cell of the data. First cellw with numeric values
(upper-left corner).
description: str (optional)
Description to include in the Vensim equations. By default no
description will be included.
units: str (optional)
Units to include in the Vensim equations. By default no
units will be included.
file: str (optional)
File where the data is. This argument is mandatory unless a
subscript range is defined across several files. Default is None.
sheet: str (optional)
Sheet where the data is. This argument is mandatory unless a
subscript range is defined across several sheets. Default is None.
interp: str or None (optional)
Keyword of the interpolation method to use with DATA. It can be
any keyword accepted by Vensim 'interpolate', 'look forward',
'keep backward' or 'raw'. If None, no keyword will be added,
Vensim will use the default interpolation method ('interpolate').
Default is None.
"""
def __init__(self, var_name, dims, cell, description='', units='',
file=None, sheet=None, interp=None, **kwargs):
super().__init__(var_name, dims, cell, description, units, file, sheet)
if interp:
# conver interp to vensim notation
self.interp = interp.strip().upper().replace('_', ' ')
if self.interp not in ['INTERPOLATE', 'RAW',
'HOLD BACKWARD', 'LOOK FORWARD']:
raise ValueError(
"\ninterp must be 'interpolate', 'raw', "
"'hold backward' or 'look forward'")
else:
self.interp = None
[docs]
def add_time(self, name, cell, read_along, length):
"""
Add time for DATA type objects.
Parameters
----------
name: str
Name of the time series cellrange.
cell: str
Reference cell.
read_along: str ('col' or 'row')
Dimension to read along the time series.
length: int
The length of the time series.
Returns
-------
None
"""
super().add_series(name, cell, read_along, length)
[docs]
def execute(self, force=False, loading='DIRECT'):
"""
Get vensim equations and write cell range names in the Excel file.
Parameters
----------
force: bool (optional)
If True and trying and tryting to write a cell range name
that already exist in other positions it will overwrite it
(not recommended). If False it will return and error when
trying to write the new cellrange name. Default is False.
loading: str (optional)
Vensing GET loading type it can be 'DIRECT' or 'XLS'.
Default is 'DIRECT'.
Returns
-------
vensim_eqs: str
The string of Vensim equations to copy in the model .mdl file.
"""
# force removal of conflicting cellrange names
self.force = force
vensim_eqs = self.get_vensim(loading)
# write series cellranges
super()._write_cellranges(
self.series['name'], self.series['file'],
self.series['sheet'], self.series['cellrange'])
# write data cellranges
super()._write_cellranges(
self.elements['cellname'], self.elements['file'],
self.elements['sheet'], self.elements['cellrange'])
# save changes and close Excel files
Excels.save_and_close()
return vensim_eqs
def get_vensim(self, force=False, loading='DIRECT'):
"""
Get vensim equations and write cell range names in the Excel file.
Parameters
----------
force: bool (optional)
If True and trying and tryting to write a cell range name
that already exist in other positions it will overwrite it
(not recommended). If False it will return and error when
trying to write the new cellrange name. Default is False.
loading: str (optional)
Vensing GET loading type it can be 'DIRECT' or 'XLS'.
Default is 'DIRECT'.
Returns
-------
vensim_eqs: str
The string of Vensim equations to copy in the model .mdl file.
"""
# force removal of conflicting cellrange names
self.force = force
self.elements = {
'row': [np.array([self.ref_row, self.ref_row], dtype=int)],
'col': [np.array([self.ref_col, self.ref_col], dtype=int)],
'subs': [[]],
'sheet': [[]],
'file': [[]],
'cellname': [self.base_name]
}
self.elements[self.series['read_along']][0][1] +=\
self.series['length'] - 1
super()._build_boxes([self.series['read_along']])
super()._update_series_cellranges(set(self.elements['sheet']),
set(self.elements['file']))
# generate Vensim equations
vensim_eqs = ""
for subs, file, sheet, cellname in zip(self.elements['subs'],
self.elements['file'],
self.elements['sheet'],
self.elements['cellname']):
if subs:
subs_write = f"[{', '.join(map(str, subs))}]"
else:
subs_write = ""
if self.interp:
# add keyword for interpolation method
interp_write = f":{self.interp}:"
else:
interp_write = ""
vensim_eq = """
%s:=
\tGET_%s_DATA('%s', '%s', '%s', '%s') ~~|""" % (
self.var_name+subs_write+interp_write,
loading, file, sheet, self.series['name'][0], cellname)
vensim_eqs += vensim_eq
vensim_eqs = textwrap.dedent(vensim_eqs)
vensim_eqs = vensim_eqs[:-4] + f'\n\t~\t{self.units}'\
+ f'\n\t~\t{self.description}\n\t|'
return vensim_eqs
[docs]
class Constants(ExternalVariable):
"""
Class for creating GET DIRECT/XLS CONSTANTS equations and cellranges.
Parameters
----------
var_name: str
The name of the variable in Vensim code and basestring for
cellrange names.
dims: list
List of the dimensions of the variable in the same order that
will be created the variable in the Vensim code.
cell: str
Reference cell of the data. First cellw with numeric values
(upper-left corner).
description: str (optional)
Description to include in the Vensim equations. By default no
description will be included.
units: str (optional)
Units to include in the Vensim equations. By default no
units will be included.
file: str (optional)
File where the data is. This argument is mandatory unless a
subscript range is defined across several files. Default is None.
sheet: str (optional)
Sheet where the data is. This argument is mandatory unless a
subscript range is defined across several sheets. Default is None.
"""
def __init__(self, var_name, dims, cell, description='', units='',
file=None, sheet=None, **kwargs):
super().__init__(var_name, dims, cell, description, units, file, sheet)
self.transpose = False
[docs]
def execute(self, force=False, loading='DIRECT'):
"""
Get vensim equations and write cell range names in the Excel file.
Parameters
----------
force: bool (optional)
If True and trying and tryting to write a cell range name
that already exist in other positions it will overwrite it
(not recommended). If False it will return and error when
trying to write the new cellrange name. Default is False.
loading: str (optional)
Vensing GET loading type it can be 'DIRECT' or 'XLS'.
Default is 'DIRECT'.
Returns
-------
vensim_eqs: str
The string of Vensim equations to copy in the model .mdl file.
"""
# force removal of conflicting cellrange names
self.force = force
vensim_eqs = self.get_vensim(loading)
# write data cellranges
super()._write_cellranges(
self.elements['cellname'], self.elements['file'],
self.elements['sheet'], self.elements['cellrange'])
# save changes and close Excel files
Excels.save_and_close()
return vensim_eqs
def get_vensim(self, loading='DIRECT'):
"""
Get vensim equations.
Parameters
----------
loading: str (optional)
Vensing GET loading type it can be 'DIRECT' or 'XLS'.
Default is 'DIRECT'.
Returns
-------
vensim_eqs: str
The string of Vensim equations to copy in the model .mdl file.
"""
self.elements = {
'row': [np.array([self.ref_row, self.ref_row], dtype=int)],
'col': [np.array([self.ref_col, self.ref_col], dtype=int)],
'subs': [[]],
'sheet': [[]],
'file': [[]],
'cellname': [self.base_name]
}
visited = self._build_boxes([])
# transpose with *
if visited in [["row"], ["col", "row"]]:
self.transpose = True
# generate Vensim equations
vensim_eqs = ""
for subs, file, sheet, cellname in zip(self.elements['subs'],
self.elements['file'],
self.elements['sheet'],
self.elements['cellname']):
if subs:
subs_write = f"[{', '.join(map(str, subs))}]"
else:
subs_write = ""
if self.transpose:
cellname += '*'
vensim_eq = """
%s=
\tGET_%s_CONSTANTS('%s', '%s', '%s') ~~|""" % (
self.var_name+subs_write, loading, file, sheet, cellname)
vensim_eqs += vensim_eq
vensim_eqs = textwrap.dedent(vensim_eqs)
vensim_eqs = vensim_eqs[:-4] + f'\n\t~\t{self.units}'\
+ f'\n\t~\t{self.description}\n\t|'
return vensim_eqs
[docs]
def load_from_json(json_file):
"""
Run the features using a JSON file.
Prameters
---------
json_file: str
Name of the JSON file with the needed information.
Returns
-------
str
The equations to copy in the Vensim model file.
"""
with open(json_file) as file:
vars_dict = json.load(file)
return execute(vars_dict)
def execute(vars_dict):
"""
Run the features using a dictionary.
Prameters
---------
vars_dict
Python dictionary with the needed information.
Returns
-------
str
The equations to copy in the Vensim model file.
"""
eqs = []
for var, info in vars_dict.items():
if info['type'].lower() == 'constants':
# create object
obj = Constants(var, **info)
elif info['type'].lower() == 'lookups':
# create object
obj = Lookups(var, **info)
# add x series
obj.add_x(**info['x'])
elif info['type'].lower() == 'data':
# create object
obj = Data(var, **info)
# add time series
obj.add_time(**info['time'])
else:
raise ValueError(
f"\n Invalid type of variable '{info['type']}' for '{var}'."
+ " It must be 'constants', 'lookups' or 'data'.")
# add dimensions
for dimension, along in info['dimensions'].items():
obj.add_dimension(dimension, *along)
if 'force' in info:
force = info['force']
else:
force = False
if 'loading' in info:
loading = info['loading']
else:
loading = 'DIRECT'
eqs.append(obj.execute(force=force, loading=loading))
return '\n'.join(eqs)