diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fe9334..c165574 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.2.0] - 2022-07-02 +### Added +* Added the possibility to read GAMS variables and convert them into pandas DataFrames + ## [2.1.1] - 2020-08-20 ### Added * Add index names from symbol domain if defined when converting to a pandas object. diff --git a/gdx2py/__init__.py b/gdx2py/__init__.py index 4a9a787..c959cc4 100644 --- a/gdx2py/__init__.py +++ b/gdx2py/__init__.py @@ -2,4 +2,4 @@ from .version import __version__ from .gdxfile import GdxFile -from .gams import GAMSSet, GAMSScalar, GAMSParameter +from .gams import GAMSSet, GAMSScalar, GAMSParameter, GAMSVariable diff --git a/gdx2py/gams.py b/gdx2py/gams.py index f31e793..b76ecaa 100644 --- a/gdx2py/gams.py +++ b/gdx2py/gams.py @@ -250,3 +250,79 @@ def to_pandas(self): return df + +class GAMSVariable(_GAMSNDimSymbol): + """Class for GAMS Variables + + Attributes: + domain (list): List of set names that make the domain of the symbol + expl_text (str): Symbol explanatory text + + Methods: + keys: Return a view to the symbol's keys + values: Return a view to the symbol's values + """ + + def __init__( + self, + data: Mapping[tuple, float], + domain: Sequence[str] = None, + expl_text: str = '', + ): + """Constructor for GAMSParameter + + Args: + data: Dictionay of keys and values + domain (optional): List of domain set names, use `None` for the universal set + expl_text (optional): Explanatory text + + Raises: + ValueError + """ + + # Check arguments + try: + super().__init__(data.keys(), domain, expl_text) + except AttributeError: + raise ValueError("Data must be a mapping") + self._type = 'Parameter' + self._data = data + + def keys(self): + return self._data.keys() + + def values(self): + try: + return self._data.values() + except TypeError: # Support self.data being a pandas.Series + return self._data.values + + def __getitem__(self, key): + return self._data[key] + + def __iter__(self): + self._iterator = iter(self._data.items()) + return self + + def __next__(self): + key, val = next(self._iterator) + return key, float(val) + + def to_pandas(self): + try: + import pandas as pd + except ImportError: + raise ImportError( + "This feature needs pandas package. " + "Please use pip or conda to install pandas" + ) + df = pd.DataFrame(iter(self.values()), self.keys()) + df.name = self.expl_text + + if self.domain: + if isinstance(df.index, pd.MultiIndex): + df.index.names = self.domain + else: + df.index.name = self.domain[0] + + return df \ No newline at end of file diff --git a/gdx2py/gdxfile.py b/gdx2py/gdxfile.py index 768951c..9ba7c2a 100644 --- a/gdx2py/gdxfile.py +++ b/gdx2py/gdxfile.py @@ -4,7 +4,6 @@ __author__ = "Erkka Rinne" - import sys import os.path import math @@ -12,10 +11,11 @@ from warnings import warn from collections.abc import Mapping, Sequence, KeysView, ValuesView -from gdxcc import GMS_VAL_LEVEL, GMS_DT_SET, GMS_DT_PAR +from gdxcc import GMS_VAL_LEVEL, GMS_VAL_MARGINAL, GMS_VAL_LOWER, GMS_VAL_UPPER, \ + GMS_DT_SET, GMS_DT_PAR, GMS_DT_VAR import gdxcc -from .gams import _GAMSSymbol, GAMSSet, GAMSScalar, GAMSParameter +from .gams import _GAMSSymbol, GAMSSet, GAMSScalar, GAMSParameter, GAMSVariable # String representations of API constants GMS_DTYPES = { @@ -23,9 +23,9 @@ 'Parameter': GMS_DT_PAR, 'Scalar': GMS_DT_PAR, # Not used at the moments - #'Variable': GMS_DT_VAR, - #'Equation': GMS_DT_EQU, - #'Alias': GMS_DT_ALIAS: + 'Variable': GMS_DT_VAR, + # 'Equation': GMS_DT_EQU, + # 'Alias': GMS_DT_ALIAS: } # Define data types @@ -47,6 +47,13 @@ gdxcc.GMS_SV_NAINT: math.nan, } +VARIABLE_DIMENSIONS = { + GMS_VAL_LEVEL: "level", + GMS_VAL_MARGINAL: "marginal", + GMS_VAL_LOWER: "lower bound", + GMS_VAL_UPPER: "upper bound" +} + GMS_USERINFO_SET_PARAMETER = 0 # UserInfo value for sets and parameters @@ -346,6 +353,19 @@ def _read_symbol(self, symno: int): ) # Squeze out dimension of 1-dim keys val = value_arr[GMS_VAL_LEVEL] values[i] = SPECIAL_VALUES.get(val, val) + elif symtype == GMS_DT_VAR: + for i in range(recs): + _ret, key_arr, value_arr, _afdim = gdxcc.gdxDataReadStr(self._h) + keys[i] = ( + key_arr[0] if dim == 1 else tuple(key_arr) + ) # Squeze out dimension of 1-dim keys + + value = {} + for elem in VARIABLE_DIMENSIONS: + val = value_arr[elem] + value[VARIABLE_DIMENSIONS[elem]] = SPECIAL_VALUES.get(val, val) + + values[i] = value # Done reading gdxcc.gdxDataReadDone(self._h) @@ -360,6 +380,10 @@ def _read_symbol(self, symno: int): return GAMSParameter( dict(zip(keys, values)), domain=domain, expl_text=expl_text ) + elif symtype == GMS_DT_VAR: + return GAMSVariable( + dict(zip(keys, values)), domain=domain, expl_text=expl_text + ) def _write_symbol(self, symname: str, symbol: _GAMSSymbol): """Write a Pandas series to a GAMS Set symbol @@ -369,8 +393,12 @@ def _write_symbol(self, symname: str, symbol: _GAMSSymbol): Raises: RuntimeError + NotImplementedError """ + if isinstance(symbol, GAMSVariable): + raise NotImplementedError + # Get number of dimensions dim = symbol.dimension diff --git a/gdx2py/version.py b/gdx2py/version.py index 55fa725..04188a1 100644 --- a/gdx2py/version.py +++ b/gdx2py/version.py @@ -1 +1 @@ -__version__ = '2.1.1' +__version__ = '2.2.0'