modelflowib 2.73__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modelBLfunk.py +180 -0
- model_Excel.py +332 -0
- model_cvx.py +139 -0
- model_dynare.py +173 -0
- model_financial_stability.py +88 -0
- model_latex.py +497 -0
- model_latex_class.py +808 -0
- model_parquet_mixin.py +424 -0
- modelclass.py +9828 -0
- modelconstruct.py +1496 -0
- modelconstruct_estimation.py +2872 -0
- modeldash.py +265 -0
- modeldashboot.py +202 -0
- modeldashsidebar.py +456 -0
- modeldekom.py +651 -0
- modeldiff.py +561 -0
- modeldisplay.py +550 -0
- modelestimation.py +1776 -0
- modelestimator_new.py +2613 -0
- modelflowib-2.73.dist-info/METADATA +156 -0
- modelflowib-2.73.dist-info/RECORD +44 -0
- modelflowib-2.73.dist-info/WHEEL +5 -0
- modelflowib-2.73.dist-info/licenses/license.md +10 -0
- modelflowib-2.73.dist-info/top_level.txt +39 -0
- modelgrab.py +318 -0
- modelgrabgdx.py +584 -0
- modelgrabwf2.py +1107 -0
- modelhelp.py +543 -0
- modelhtml.py +606 -0
- modelinvert.py +250 -0
- modeljupyter.py +824 -0
- modeljupytermagic.py +813 -0
- modelmacrograb.py +98 -0
- modelmanipulation.py +1461 -0
- modelmf.py +349 -0
- modelnet.py +114 -0
- modelnewton.py +2178 -0
- modelnormalize.py +430 -0
- modelpattern.py +428 -0
- modelreport.py +2187 -0
- modeluserfunk.py +97 -0
- modelvis.py +1038 -0
- modelwidget.py +718 -0
- modelwidget_input.py +1933 -0
modelgrabgdx.py
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
"""Read GAMS GDX files and build ModelFlow model instances.
|
|
2
|
+
|
|
3
|
+
Provides utilities for loading GDX files via gams.transfer, extracting
|
|
4
|
+
free variables indexed by time into wide-format DataFrames, and
|
|
5
|
+
bundling one or more scenarios into a single ModelFlow model object.
|
|
6
|
+
|
|
7
|
+
Key components
|
|
8
|
+
--------------
|
|
9
|
+
GdxDataset
|
|
10
|
+
Dataclass that loads a single GDX file and produces a wide
|
|
11
|
+
timeseries DataFrame of all free variables with a time dimension.
|
|
12
|
+
|
|
13
|
+
model_grab_gdx
|
|
14
|
+
Convenience function that accepts one or more GDX file paths,
|
|
15
|
+
builds GdxDataset instances, finds the common variable set, and
|
|
16
|
+
returns a populated ModelFlow model with all scenarios stored
|
|
17
|
+
in keep_solutions.
|
|
18
|
+
|
|
19
|
+
display_bytype_tables
|
|
20
|
+
Jupyter helper that prints every symbol in a GDX file grouped
|
|
21
|
+
by type (Set, Parameter, Variable, Equation).
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
requires GAMS instalation and
|
|
25
|
+
pip install "gamsapi[transfer]"
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from IPython.display import display, Markdown
|
|
29
|
+
import pandas as pd
|
|
30
|
+
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
from modelclass import model
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
from gams import transfer as gt
|
|
40
|
+
HAS_GAMS = True
|
|
41
|
+
except ImportError:
|
|
42
|
+
HAS_GAMS = False
|
|
43
|
+
|
|
44
|
+
def _require_gams(gams_dir):
|
|
45
|
+
if not HAS_GAMS:
|
|
46
|
+
raise ImportError(
|
|
47
|
+
"The 'gams.transfer' package is required but not installed. "
|
|
48
|
+
"Install it with: pip install gams[transfer]"
|
|
49
|
+
)
|
|
50
|
+
if not Path(gams_dir).is_dir():
|
|
51
|
+
raise FileNotFoundError(
|
|
52
|
+
f"GAMS system directory not found: '{gams_dir}'. "
|
|
53
|
+
"Install GAMS from https://www.gams.com or pass the correct gams_dir."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def clean_columns(df: pd.DataFrame) -> pd.DataFrame:
|
|
57
|
+
out = df.copy()
|
|
58
|
+
out.columns = (
|
|
59
|
+
out.columns.astype(str)
|
|
60
|
+
.str.replace(r"[()]", "", regex=True) # remove ( )
|
|
61
|
+
.str.replace(r"\s+", "_", regex=True) # blanks/whitespace -> _
|
|
62
|
+
.str.replace(r"[^A-Za-z0-9_]", "_", regex=True) # other chars -> _
|
|
63
|
+
)
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
def get_gdx_t(filename="baseline.gdx", gams_dir=r"C:\GAMS\47"):
|
|
67
|
+
|
|
68
|
+
_require_gams(gams_dir)
|
|
69
|
+
print(f'\nStart reading {filename}')
|
|
70
|
+
container = gt.Container(filename, system_directory=gams_dir)
|
|
71
|
+
# print(f'Finished reading {filename}')
|
|
72
|
+
|
|
73
|
+
dfs = {}
|
|
74
|
+
bytype = {}
|
|
75
|
+
for name, sym in container:
|
|
76
|
+
tname = type(sym).__name__
|
|
77
|
+
if tname in ("Alias", "UniverseAlias"):
|
|
78
|
+
continue
|
|
79
|
+
bytype.setdefault(tname, []).append(sym) # sym, not the tuple
|
|
80
|
+
dfs[name] = sym.records
|
|
81
|
+
# print(f'Finished tranferring {filename}')
|
|
82
|
+
|
|
83
|
+
return bytype, dfs
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def display_content(filename="baseline.gdx", gams_dir=r"C:\GAMS\47"):
|
|
89
|
+
"""Display one Markdown table per full_typename in Jupyter."""
|
|
90
|
+
|
|
91
|
+
bytype,_ = get_gdx_t(filename=filename, gams_dir=gams_dir)
|
|
92
|
+
for t in sorted(bytype.keys()):
|
|
93
|
+
df = symbols_to_df_t(bytype[t])
|
|
94
|
+
display(Markdown(f"## {t} ({len(df)})"))
|
|
95
|
+
display(Markdown(df.to_markdown(index=False)))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def bytype_description_to_dict_t(bytype):
|
|
103
|
+
outdict = {
|
|
104
|
+
s.name.upper(): s.description
|
|
105
|
+
for symbollist in bytype.values()
|
|
106
|
+
for s in sorted(symbollist, key=lambda x: x.name)
|
|
107
|
+
}
|
|
108
|
+
return outdict
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def symbols_to_df_t(symbols):
|
|
112
|
+
"""Convert a list of gams.transfer symbols to a tidy DataFrame."""
|
|
113
|
+
rows = []
|
|
114
|
+
for s in symbols:
|
|
115
|
+
rows.append({
|
|
116
|
+
"name": s.name,
|
|
117
|
+
"dims": s.domain_names,
|
|
118
|
+
"description": s.description,
|
|
119
|
+
})
|
|
120
|
+
return pd.DataFrame(rows).sort_values("name").reset_index(drop=True)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
import numpy as np
|
|
125
|
+
|
|
126
|
+
def free_to_timeseries_t(bytype, dfs, t_name="t", value_col="level", sep="__"):
|
|
127
|
+
var_symbols = bytype.get("Variable", [])
|
|
128
|
+
|
|
129
|
+
selected = [
|
|
130
|
+
s.name for s in var_symbols
|
|
131
|
+
if (s.type == 0 or str(s.type).lower() == "free")
|
|
132
|
+
and s.domain_names
|
|
133
|
+
and s.domain_names[-1] == t_name
|
|
134
|
+
and s.name in dfs
|
|
135
|
+
and dfs[s.name] is not None
|
|
136
|
+
]
|
|
137
|
+
if not selected:
|
|
138
|
+
return pd.DataFrame()
|
|
139
|
+
|
|
140
|
+
value_cols_set = {"level", "marginal", "lower", "upper", "scale", "value"}
|
|
141
|
+
|
|
142
|
+
chunks = []
|
|
143
|
+
for v in selected:
|
|
144
|
+
df = dfs[v]
|
|
145
|
+
if t_name not in df.columns or len(df) == 0:
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
col = value_col if value_col in df.columns else next(
|
|
149
|
+
(c for c in df.columns if c.lower() == value_col.lower()), None)
|
|
150
|
+
if col is None:
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
dims = [c for c in df.columns if c.lower() not in value_cols_set]
|
|
154
|
+
other_dims = [c for c in dims if c != t_name]
|
|
155
|
+
|
|
156
|
+
t_vals = df[t_name].values
|
|
157
|
+
v_vals = df[col].values
|
|
158
|
+
|
|
159
|
+
if other_dims:
|
|
160
|
+
parts = [df[d].values.astype(str) for d in other_dims]
|
|
161
|
+
if len(parts) == 1:
|
|
162
|
+
colkeys = np.char.add(v + sep, parts[0])
|
|
163
|
+
else:
|
|
164
|
+
combined = parts[0]
|
|
165
|
+
for p in parts[1:]:
|
|
166
|
+
combined = np.char.add(np.char.add(combined, sep), p)
|
|
167
|
+
colkeys = np.char.add(v + sep, combined)
|
|
168
|
+
else:
|
|
169
|
+
colkeys = np.full(len(df), v, dtype=object)
|
|
170
|
+
|
|
171
|
+
chunks.append((t_vals, colkeys, v_vals))
|
|
172
|
+
|
|
173
|
+
if not chunks:
|
|
174
|
+
return pd.DataFrame()
|
|
175
|
+
|
|
176
|
+
all_t = np.concatenate([c[0] for c in chunks])
|
|
177
|
+
all_k = np.concatenate([c[1] for c in chunks])
|
|
178
|
+
all_v = np.concatenate([c[2] for c in chunks])
|
|
179
|
+
|
|
180
|
+
out = (
|
|
181
|
+
pd.DataFrame({t_name: all_t, "_k": all_k, "_v": all_v})
|
|
182
|
+
.pivot(index=t_name, columns="_k", values="_v")
|
|
183
|
+
)
|
|
184
|
+
out.index = out.index.astype(int)
|
|
185
|
+
out.columns = out.columns.str.upper()
|
|
186
|
+
out.sort_index(inplace=True)
|
|
187
|
+
out.index.name = t_name
|
|
188
|
+
return out
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dataclass
|
|
192
|
+
class GdxDataset:
|
|
193
|
+
filename: str | Path
|
|
194
|
+
gams_dir: str = r"C:\GAMS\47"
|
|
195
|
+
name: str = field(init=False)
|
|
196
|
+
df: pd.DataFrame = field(init=False)
|
|
197
|
+
var_descriptions: dict[str, str] = field(init=False)
|
|
198
|
+
|
|
199
|
+
def __post_init__(self) -> None:
|
|
200
|
+
self.name = Path(self.filename).stem
|
|
201
|
+
self.bytype, dfs = get_gdx_t(self.filename, self.gams_dir)
|
|
202
|
+
self.df = free_to_timeseries_t(self.bytype, dfs, t_name="t").pipe(clean_columns)
|
|
203
|
+
temp = bytype_description_to_dict_t(self.bytype)
|
|
204
|
+
self.var_descriptions = {
|
|
205
|
+
v: (f"{temp.get(base, base)} [{suffix}]" if sep else temp.get(base, base))
|
|
206
|
+
for v in self.df.columns
|
|
207
|
+
for base, sep, suffix in [v.partition("__")]
|
|
208
|
+
}
|
|
209
|
+
self.info
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def info(self):
|
|
214
|
+
print('\n')
|
|
215
|
+
print(f'From : {self.filename}')
|
|
216
|
+
print(f'Name : {self.name}')
|
|
217
|
+
print(f'Number of variables: {self.df.shape[1]}')
|
|
218
|
+
if len(self.df):
|
|
219
|
+
print(f'Number of periods : {self.df.shape[0]} {self.df.index[0]} to {self.df.index[-1]}')
|
|
220
|
+
else:
|
|
221
|
+
print('Number of periods : 0')
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def model_grab_gdx(gdx_files, gams_dir=r"C:\GAMS\47", max_workers=None):
|
|
225
|
+
"""Build a model from one or more GDX files.
|
|
226
|
+
|
|
227
|
+
Parameters
|
|
228
|
+
----------
|
|
229
|
+
gdx_files : str, Path, or list of str/Path
|
|
230
|
+
One or more paths to .gdx files.
|
|
231
|
+
gams_dir : str
|
|
232
|
+
GAMS system directory.
|
|
233
|
+
max_workers : int or None
|
|
234
|
+
Number of threads for parallel GDX loading.
|
|
235
|
+
None = min(len(gdx_files), 8) (auto).
|
|
236
|
+
1 = sequential (old behaviour, useful for debugging).
|
|
237
|
+
"""
|
|
238
|
+
if isinstance(gdx_files, (str, Path)):
|
|
239
|
+
gdx_files = [gdx_files]
|
|
240
|
+
|
|
241
|
+
n_files = len(gdx_files)
|
|
242
|
+
|
|
243
|
+
if n_files == 1 or max_workers == 1:
|
|
244
|
+
# sequential – no thread overhead
|
|
245
|
+
GdxDataset_list = [GdxDataset(f, gams_dir=gams_dir) for f in gdx_files]
|
|
246
|
+
else:
|
|
247
|
+
# parallel – GDX reading releases the GIL (C library),
|
|
248
|
+
# and numpy/pandas pivot work also largely releases it.
|
|
249
|
+
workers = max_workers or min(n_files, 8)
|
|
250
|
+
|
|
251
|
+
# We need to preserve the original file order, so submit
|
|
252
|
+
# with an index and sort afterwards.
|
|
253
|
+
results = [None] * n_files
|
|
254
|
+
with ThreadPoolExecutor(max_workers=workers) as pool:
|
|
255
|
+
futures = {
|
|
256
|
+
pool.submit(GdxDataset, f, gams_dir): i
|
|
257
|
+
for i, f in enumerate(gdx_files)
|
|
258
|
+
}
|
|
259
|
+
for fut in as_completed(futures):
|
|
260
|
+
idx = futures[fut]
|
|
261
|
+
results[idx] = fut.result() # propagates exceptions
|
|
262
|
+
GdxDataset_list = results
|
|
263
|
+
|
|
264
|
+
col_sets = [set(g.df.columns) for g in GdxDataset_list]
|
|
265
|
+
common_cols = sorted(set.intersection(*col_sets))
|
|
266
|
+
common_var_descriptions = {v: GdxDataset_list[0].var_descriptions.get(v, v) for v in common_cols}
|
|
267
|
+
|
|
268
|
+
fmodel = '\n'.join(f'{v}= 42' for v in common_cols)
|
|
269
|
+
mmodel = model(fmodel)
|
|
270
|
+
modelvar_set = set(mmodel.allvar.keys())
|
|
271
|
+
missing = [v for v in common_cols if v not in modelvar_set]
|
|
272
|
+
if missing:
|
|
273
|
+
print(f'Problems with these variable names: \n{missing}')
|
|
274
|
+
|
|
275
|
+
mmodel.basedf = GdxDataset_list[0].df.loc[:, common_cols]
|
|
276
|
+
mmodel.lastdf = GdxDataset_list[0].df.loc[:, common_cols]
|
|
277
|
+
mmodel.keep_solutions = {g.name: g.df.loc[:, common_cols] for g in GdxDataset_list}
|
|
278
|
+
mmodel.smpl()
|
|
279
|
+
mmodel.oldkwargs = {}
|
|
280
|
+
mmodel.var_description = common_var_descriptions
|
|
281
|
+
return mmodel
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# =====================================================================
|
|
285
|
+
# Static GDX support - set-indexed models WITHOUT a time dimension
|
|
286
|
+
# (e.g. GAMS CGE models such as JRC DEMETRA).
|
|
287
|
+
#
|
|
288
|
+
# Nothing above this line has been changed: the legacy time-indexed API
|
|
289
|
+
# (get_gdx_t, free_to_timeseries_t, GdxDataset, model_grab_gdx) works
|
|
290
|
+
# exactly as before. The functions below are additions:
|
|
291
|
+
#
|
|
292
|
+
# clean_name - sanitise ONE name with the clean_columns rules
|
|
293
|
+
# static_to_frame - wide DataFrame of ALL variable levels and
|
|
294
|
+
# parameter values, broadcast constant over a
|
|
295
|
+
# year range (no t dimension required)
|
|
296
|
+
# sets_to_lists - ModelFlow LIST statements from GDX sets
|
|
297
|
+
# (subsets -> 0/1 sublists, 2-dim sets ->
|
|
298
|
+
# per-element 0/1 sublists, parameters ->
|
|
299
|
+
# nonzero-pattern sublists, aliases)
|
|
300
|
+
# pair_list - a "pair list" (parallel sublists) for a
|
|
301
|
+
# sparse n-dim set or nonzero parameter,
|
|
302
|
+
# for do PAIRS $ ... {K1} {K2} ... enddo
|
|
303
|
+
# GdxStaticDataset - convenience wrapper bundling the above
|
|
304
|
+
# =====================================================================
|
|
305
|
+
|
|
306
|
+
import re as _re
|
|
307
|
+
|
|
308
|
+
_VALUE_COLS = {"level", "marginal", "lower", "upper", "scale",
|
|
309
|
+
"value", "element_text"}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def clean_name(name) -> str:
|
|
313
|
+
"""Sanitise one symbol or set-element name.
|
|
314
|
+
|
|
315
|
+
Same rules as clean_columns (keep the two in sync!):
|
|
316
|
+
remove ( ), whitespace -> _, any other non-alphanumeric -> _,
|
|
317
|
+
upper case. E.g. GAMS SAM account 'i-s' -> 'I_S'.
|
|
318
|
+
"""
|
|
319
|
+
out = _re.sub(r"[()]", "", str(name))
|
|
320
|
+
out = _re.sub(r"\s+", "_", out)
|
|
321
|
+
out = _re.sub(r"[^A-Za-z0-9_]", "_", out)
|
|
322
|
+
return out.upper()
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _dims_of(df):
|
|
326
|
+
"""Domain columns of a gams.transfer records DataFrame."""
|
|
327
|
+
return [c for c in df.columns if c.lower() not in _VALUE_COLS]
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _records_tuples(dfs, name):
|
|
331
|
+
"""The records of a set OR parameter as a set of cleaned tuples.
|
|
332
|
+
|
|
333
|
+
For parameters GDX only stores nonzero records, so membership of a
|
|
334
|
+
tuple == 'parameter is nonzero there' - i.e. a GAMS $-condition.
|
|
335
|
+
"""
|
|
336
|
+
df = dfs.get(name)
|
|
337
|
+
if df is None or len(df) == 0:
|
|
338
|
+
return set()
|
|
339
|
+
dims = _dims_of(df)
|
|
340
|
+
if not dims:
|
|
341
|
+
return set()
|
|
342
|
+
return {tuple(clean_name(v) for v in row)
|
|
343
|
+
for row in df[dims].astype(str).values}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _set_elements(dfs, name):
|
|
347
|
+
"""Ordered, cleaned elements of a 1-dim set."""
|
|
348
|
+
df = dfs.get(name)
|
|
349
|
+
if df is None or len(df) == 0:
|
|
350
|
+
return []
|
|
351
|
+
dims = _dims_of(df)
|
|
352
|
+
return [clean_name(v) for v in df[dims[0]].astype(str)]
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def static_to_frame(bytype, dfs, years, sep="__",
|
|
356
|
+
include_variables=True, include_parameters=True,
|
|
357
|
+
skip=()):
|
|
358
|
+
"""Wide DataFrame of all variable levels and parameter values.
|
|
359
|
+
|
|
360
|
+
Unlike free_to_timeseries_t, no trailing time dimension is
|
|
361
|
+
required: every symbol is flattened to NAME__ELEM1__ELEM2 (same
|
|
362
|
+
sep and cleaning as the legacy path) and broadcast as a constant
|
|
363
|
+
column over `years`. Intended for static GAMS models whose GDX
|
|
364
|
+
dump (execute_unload) holds base-year levels.
|
|
365
|
+
|
|
366
|
+
Parameters
|
|
367
|
+
----------
|
|
368
|
+
bytype, dfs : output of get_gdx_t
|
|
369
|
+
years : iterable of period labels for the index, e.g. range(2020, 2041)
|
|
370
|
+
skip : symbol names (GAMS spelling) to leave out
|
|
371
|
+
"""
|
|
372
|
+
wanted = []
|
|
373
|
+
if include_variables:
|
|
374
|
+
wanted += [(s, "level") for s in bytype.get("Variable", [])]
|
|
375
|
+
if include_parameters:
|
|
376
|
+
wanted += [(s, "value") for s in bytype.get("Parameter", [])]
|
|
377
|
+
|
|
378
|
+
skipset = {str(s).upper() for s in skip}
|
|
379
|
+
data = {}
|
|
380
|
+
for sym, valcol in wanted:
|
|
381
|
+
if sym.name.upper() in skipset:
|
|
382
|
+
continue
|
|
383
|
+
df = dfs.get(sym.name)
|
|
384
|
+
if df is None or len(df) == 0:
|
|
385
|
+
continue
|
|
386
|
+
col = next((c for c in df.columns if c.lower() == valcol), None)
|
|
387
|
+
if col is None:
|
|
388
|
+
continue
|
|
389
|
+
name = clean_name(sym.name)
|
|
390
|
+
dims = _dims_of(df)
|
|
391
|
+
if dims:
|
|
392
|
+
keys = df[dims[0]].astype(str).map(clean_name)
|
|
393
|
+
for d in dims[1:]:
|
|
394
|
+
keys = keys + sep + df[d].astype(str).map(clean_name)
|
|
395
|
+
for k, v in zip(keys, df[col].values):
|
|
396
|
+
data[f"{name}{sep}{k}"] = v
|
|
397
|
+
else: # scalar
|
|
398
|
+
data[name] = df[col].values[0]
|
|
399
|
+
|
|
400
|
+
out = pd.DataFrame(data, index=pd.Index(list(years), name="year"))
|
|
401
|
+
return out
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def sets_to_lists(bytype, dfs, base_sets, aliases=None,
|
|
405
|
+
condition_symbols=None, sep="_"):
|
|
406
|
+
"""ModelFlow LIST statements generated from the sets of a GDX file.
|
|
407
|
+
|
|
408
|
+
base_sets : GAMS names of the sets that become LISTs. Each yields
|
|
409
|
+
|
|
410
|
+
LIST <B> = <B> : e1 , e2 , ... /
|
|
411
|
+
<SUB> : 1 , 0 , ... / (1-dim conditions)
|
|
412
|
+
<SET2D>_<elem> : 0 , 1 , ... $ (2-dim conditions)
|
|
413
|
+
|
|
414
|
+
condition_symbols : names of sets and/or PARAMETERS to turn into
|
|
415
|
+
sublists (parameters use their nonzero pattern - the GAMS
|
|
416
|
+
$-condition 'par(i) <> 0'). Default: every set in the GDX.
|
|
417
|
+
- 1-dim symbols whose elements all belong to a base set become
|
|
418
|
+
a 0/1 sublist on that base list (possibly on several base
|
|
419
|
+
lists when bases overlap - harmless).
|
|
420
|
+
- 2-dim symbols become per-element 0/1 sublists on BOTH base
|
|
421
|
+
lists: name <SYM>_<element-of-the-other-dimension>.
|
|
422
|
+
- higher dimensions are skipped here: use pair_list for those.
|
|
423
|
+
|
|
424
|
+
aliases : dict alias_name -> base_name (GAMS ALIAS). The alias is
|
|
425
|
+
a FULL copy of the base list block with the key sublist renamed,
|
|
426
|
+
so conditions keep working on the alias (sum(CP CCESN, ...)).
|
|
427
|
+
|
|
428
|
+
Returns one string with all LIST statements.
|
|
429
|
+
"""
|
|
430
|
+
all_sets = {s.name: s for s in bytype.get("Set", [])}
|
|
431
|
+
all_params = {s.name: s for s in bytype.get("Parameter", [])}
|
|
432
|
+
if condition_symbols is None:
|
|
433
|
+
condition_symbols = list(all_sets)
|
|
434
|
+
|
|
435
|
+
base_elems = {b: _set_elements(dfs, b) for b in base_sets}
|
|
436
|
+
|
|
437
|
+
def sublines_for(b):
|
|
438
|
+
"""The condition sublists belonging to base list b."""
|
|
439
|
+
elems = base_elems[b]
|
|
440
|
+
eset = set(elems)
|
|
441
|
+
lines = []
|
|
442
|
+
for name in condition_symbols:
|
|
443
|
+
sym = all_sets.get(name) or all_params.get(name)
|
|
444
|
+
if sym is None or name in base_sets:
|
|
445
|
+
continue
|
|
446
|
+
members = _records_tuples(dfs, name)
|
|
447
|
+
if not members:
|
|
448
|
+
continue
|
|
449
|
+
nd = len(next(iter(members)))
|
|
450
|
+
cname = clean_name(name)
|
|
451
|
+
if nd == 1:
|
|
452
|
+
mem1 = {t[0] for t in members}
|
|
453
|
+
if mem1 <= eset:
|
|
454
|
+
lines.append(f"{cname} : " + " , ".join(
|
|
455
|
+
"1" if e in mem1 else "0" for e in elems))
|
|
456
|
+
elif nd == 2:
|
|
457
|
+
el1 = {t[0] for t in members}
|
|
458
|
+
el2 = {t[1] for t in members}
|
|
459
|
+
if el2 <= eset: # dim 2 lives on this list
|
|
460
|
+
for e1 in sorted(el1):
|
|
461
|
+
row = {t[1] for t in members if t[0] == e1}
|
|
462
|
+
lines.append(f"{cname}{sep}{e1} : " + " , ".join(
|
|
463
|
+
"1" if e in row else "0" for e in elems))
|
|
464
|
+
if el1 <= eset: # dim 1 lives on this list
|
|
465
|
+
for e2 in sorted(el2):
|
|
466
|
+
row = {t[0] for t in members if t[1] == e2}
|
|
467
|
+
lines.append(f"{cname}{sep}{e2} : " + " , ".join(
|
|
468
|
+
"1" if e in row else "0" for e in elems))
|
|
469
|
+
return lines
|
|
470
|
+
|
|
471
|
+
blocks = {}
|
|
472
|
+
out = []
|
|
473
|
+
for b in base_sets:
|
|
474
|
+
elems = base_elems[b]
|
|
475
|
+
if not elems:
|
|
476
|
+
print(f"sets_to_lists: base set '{b}' empty or missing - skipped")
|
|
477
|
+
continue
|
|
478
|
+
head = f"{clean_name(b)} : " + " , ".join(elems)
|
|
479
|
+
subs = sublines_for(b)
|
|
480
|
+
blocks[b] = (head, subs)
|
|
481
|
+
body = " /\n ".join([head] + subs)
|
|
482
|
+
out.append(f"LIST {clean_name(b)} = {body} $")
|
|
483
|
+
|
|
484
|
+
for alias, b in (aliases or {}).items():
|
|
485
|
+
if b not in blocks:
|
|
486
|
+
print(f"sets_to_lists: alias '{alias}' - base '{b}' missing - skipped")
|
|
487
|
+
continue
|
|
488
|
+
head, subs = blocks[b]
|
|
489
|
+
newhead = f"{clean_name(alias)} : " + head.split(":", 1)[1]
|
|
490
|
+
body = " /\n ".join([newhead] + subs)
|
|
491
|
+
out.append(f"LIST {clean_name(alias)} = {body} $")
|
|
492
|
+
|
|
493
|
+
return "\n\n".join(out)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def pair_list(dfs, name, listname=None, keynames=None):
|
|
497
|
+
"""A 'pair list' for a sparse n-dim set or nonzero parameter.
|
|
498
|
+
|
|
499
|
+
One LIST whose parallel sublists hold the tuple components, for
|
|
500
|
+
looping over exactly the active tuples (GAMS sparse domains):
|
|
501
|
+
|
|
502
|
+
LIST FDPAIRS = FF : LAND , LAB , ... /
|
|
503
|
+
A : AMAIZ , AMAIZ , ... $
|
|
504
|
+
do FDPAIRS $ frml <> X__{FF}__{A} = ... $ enddo $
|
|
505
|
+
|
|
506
|
+
Parameters
|
|
507
|
+
----------
|
|
508
|
+
name : set or parameter name in the GDX (nonzero records)
|
|
509
|
+
listname : LIST name, default = cleaned `name`
|
|
510
|
+
keynames : names of the sublists (the {index} keys); default D1, D2, ...
|
|
511
|
+
"""
|
|
512
|
+
members = sorted(_records_tuples(dfs, name))
|
|
513
|
+
if not members:
|
|
514
|
+
return f"! pair_list: '{name}' has no records\n"
|
|
515
|
+
nd = len(members[0])
|
|
516
|
+
keys = [clean_name(k) for k in (keynames or [f"D{i+1}" for i in range(nd)])]
|
|
517
|
+
lname = clean_name(listname or name)
|
|
518
|
+
lines = [f"{k} : " + " , ".join(m[i] for m in members)
|
|
519
|
+
for i, k in enumerate(keys)]
|
|
520
|
+
body = " /\n ".join(lines)
|
|
521
|
+
return f"LIST {lname} = {body} $"
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
@dataclass
|
|
525
|
+
class GdxStaticDataset:
|
|
526
|
+
"""Static (no time dimension) GDX -> ModelFlow building blocks.
|
|
527
|
+
|
|
528
|
+
Wraps get_gdx_t + static_to_frame; sets_to_lists / pair_list are
|
|
529
|
+
exposed as methods. The legacy GdxDataset is untouched - use that
|
|
530
|
+
for time-indexed scenario GDX files.
|
|
531
|
+
|
|
532
|
+
Example
|
|
533
|
+
-------
|
|
534
|
+
>>> g = GdxStaticDataset('10_gdx/demetra_ET_all.gdx',
|
|
535
|
+
... years=range(2020, 2041))
|
|
536
|
+
>>> print(g.lists(['c', 'a', 'h', 'w'], aliases={'CP': 'c'}))
|
|
537
|
+
>>> basedf = g.df # constant wide frame over the years
|
|
538
|
+
"""
|
|
539
|
+
filename: str | Path
|
|
540
|
+
years: object = tuple(range(2021, 2051))
|
|
541
|
+
gams_dir: str = r"C:\GAMS\47"
|
|
542
|
+
name: str = field(init=False)
|
|
543
|
+
df: pd.DataFrame = field(init=False)
|
|
544
|
+
var_descriptions: dict[str, str] = field(init=False)
|
|
545
|
+
|
|
546
|
+
def __post_init__(self) -> None:
|
|
547
|
+
self.name = Path(self.filename).stem
|
|
548
|
+
self.bytype, self.dfs = get_gdx_t(self.filename, self.gams_dir)
|
|
549
|
+
self.df = static_to_frame(self.bytype, self.dfs, self.years)
|
|
550
|
+
temp = bytype_description_to_dict_t(self.bytype)
|
|
551
|
+
self.var_descriptions = {
|
|
552
|
+
v: (f"{temp.get(base, base)} [{suffix}]" if sep_ else temp.get(base, base))
|
|
553
|
+
for v in self.df.columns
|
|
554
|
+
for base, sep_, suffix in [v.partition("__")]
|
|
555
|
+
}
|
|
556
|
+
self.info
|
|
557
|
+
|
|
558
|
+
def lists(self, base_sets, aliases=None, condition_symbols=None):
|
|
559
|
+
return sets_to_lists(self.bytype, self.dfs, base_sets,
|
|
560
|
+
aliases=aliases,
|
|
561
|
+
condition_symbols=condition_symbols)
|
|
562
|
+
|
|
563
|
+
def pairs(self, name, listname=None, keynames=None):
|
|
564
|
+
return pair_list(self.dfs, name, listname=listname,
|
|
565
|
+
keynames=keynames)
|
|
566
|
+
|
|
567
|
+
@property
|
|
568
|
+
def info(self):
|
|
569
|
+
print('\n')
|
|
570
|
+
print(f'From : {self.filename}')
|
|
571
|
+
print(f'Name : {self.name} (static)')
|
|
572
|
+
print(f'Number of columns : {self.df.shape[1]}')
|
|
573
|
+
print(f'Broadcast over : {self.df.index[0]} to {self.df.index[-1]}')
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
if __name__ == '__main__':
|
|
577
|
+
|
|
578
|
+
#%%
|
|
579
|
+
gdx_files = [
|
|
580
|
+
"non_energy_technology/baseline.gdx",
|
|
581
|
+
"non_energy_technology/shock_carbon_tax.gdx",
|
|
582
|
+
"non_energy_technology/shock_carbon_tax_steps.gdx",
|
|
583
|
+
]
|
|
584
|
+
mmodel = model_grab_gdx(gdx_files)
|