-
Notifications
You must be signed in to change notification settings - Fork 22
Expand file tree
/
Copy pathgdx.py
More file actions
1701 lines (1508 loc) · 61.9 KB
/
Copy pathgdx.py
File metadata and controls
1701 lines (1508 loc) · 61.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Engine functionality for reading and writing GDX files.
The GdxFile and GdxSymbol classes are full-featured interfaces
for going between the GDX format and pandas DataFrames,
including translation between GDX and numpy special values.
"""
from __future__ import annotations
import copy
import logging
import os
import weakref
from collections import OrderedDict, defaultdict
from collections.abc import MutableSequence, Sequence
from enum import Enum
import numpy as np
import pandas as pd
from gdxpds._engine import Engine, make_engine, resolve_engine
# Re-exported from gdxpds.special for backward compatibility.
from gdxpds.special import (
NUMPY_SPECIAL_VALUES, # noqa: F401
convert_gdx_to_np_svs, # noqa: F401
convert_np_to_gdx_svs, # noqa: F401
gdx_isnan, # noqa: F401
gdx_val_equal, # noqa: F401
is_np_eps, # noqa: F401
is_np_sv, # noqa: F401
)
from gdxpds.tools import Error, NeedsGamsDir
logger = logging.getLogger(__name__)
def _stable_topological_sort(names, parents_of):
"""
Stable topological sort.
Parameters
----------
names : sequence of str
The items to sort, in their original order. Original position is
used to break ties between ready items.
parents_of : dict of str to iterable of str
For each name, the names it depends on (must precede it).
Self-references and parent names not in ``names`` are ignored.
Returns
-------
(list of str or None, dict of str to set of str, or None)
``(ordered, cycle)``.
- ``cycle`` is a ``{name: set_of_unresolved_parents}`` dict
describing the names that couldn't be ordered when the input
contains a cycle, else ``None``. Iterating it (e.g.
``sorted(cycle)``) yields the involved names.
- ``ordered`` is the topologically sorted sequence when a reorder
was actually needed. It is ``None`` when the input was already
in dependency order (no reorder needed) or when a cycle was
detected (callers should consult ``cycle`` and react).
"""
name_to_pos = {n: i for i, n in enumerate(names)}
remaining = {
n: {p for p in parents_of.get(n, ()) if p in name_to_pos and p != n} for n in names
}
ordered = []
while remaining:
ready = sorted(
(n for n in remaining if not remaining[n]),
key=lambda n: name_to_pos[n],
)
if not ready:
return None, remaining
for name in ready:
ordered.append(name)
del remaining[name]
for deps in remaining.values():
deps.discard(name)
if ordered == list(names):
return None, None
return ordered, None
def replace_df_column(df: pd.DataFrame, colname: str, new_col) -> None:
"""
Utility function that replaces df[colname] with new_col. Special
care is taken for the case when df has multiple columns named '*',
since this causes pandas to crash.
Parameters
----------
df : pandas.DataFrame
edited in place by this function
colname : str
name of column in df whose data is to be replaced
new_col : vector, list, pandas.Series
new column data for df[colname]
"""
cols = df.columns
tmpcols = [col if col != "*" else "aaa" for col in cols]
df.columns = tmpcols
df[colname] = new_col
df.columns = cols
return
class GdxError(Error):
def __init__(self, H, msg):
"""
Pulls information from gdxcc about the last encountered error and appends
it to msg.
Parameters
----------
H : pointer or None
SWIG binding pointer to a GDX object
msg : str
gdxpds error message
Attributes
----------
msg : str
msg that is passed in with a gdxErrorStr appended
"""
if H:
# Imported lazily: GdxError is only raised on the gdxcc path, where a
# binding is necessarily present. Keeping it out of module scope lets
# `import gdxpds` succeed with no binding installed.
try:
from gams.core import gdx as gdxcc
except ImportError:
import gdxcc
msg += ". " + gdxcc.gdxErrorStr(H, gdxcc.gdxGetLastError(H))[1] + "."
super().__init__(msg)
class TransferError(Error):
"""Raised when a ``gams.transfer`` read or write operation fails.
The gams.transfer counterpart to :class:`GdxError`. There is no GDX handle or
last-error registry behind gams.transfer, so the underlying exception is
carried by chaining (``raise TransferError(...) from e``) rather than a
handle-derived message. Subclass of :class:`Error`, so ``except Error`` still
catches it.
"""
class DomainError(Error):
"""
Raised for any invalid input to the dim/domain layer of a symbol:
cycle in parent-child references, unknown parent name, wrong-length
list against a fixed-dimension symbol, wrong outer type for ``dims``
or ``domain``, or a malformed element (non-string in ``dims``; plain
string passed to :py:attr:`GdxSymbol.domain` instead of a
:py:class:`GdxSymbol` reference). Subclass of :class:`Error` so
callers may continue catching the broader category if they wish.
"""
class SymbolNotFoundError(Error):
"""Raised when a requested symbol name is not present in a :class:`GdxFile`.
Subclass of :class:`Error`, so ``except Error`` still catches it.
"""
class GdxFile(MutableSequence, NeedsGamsDir):
def __init__(
self,
gams_dir: str | os.PathLike[str] | None = None,
lazy_load: bool = True,
engine: str | Engine | None = None,
) -> None:
"""
Initializes a GdxFile object by connecting to GAMS and creating a pointer.
Raises a :class:`gdxpds.tools.GamsLoadError` if GAMS cannot be located or
loaded, or if the GDX object cannot be created.
Parameters
----------
gams_dir : None or str
lazy_load : bool
If True, :py:class:`GdxSymbol` data are not automatically loaded when the
symbols are initially :py:meth:`read`. Individual data tables can only be
accessed later after the corresponding calls to :py:meth:`GdxSymbol.load`.
If False, all data are automatically loaded and the full GDX file is
available in memory after the call to :py:meth:`read`.
engine : None or str or :py:class:`gdxpds.Engine`
Which I/O engine to use. ``None`` (default) resolves via the
``GDXPDS_ENGINE`` env var, then the default engine: ``gams.transfer``
when usable, otherwise ``gdxcc``. Pass ``"gdxcc"`` / ``Engine.GDXCC``
to pin the gdxcc engine.
"""
self.lazy_load = lazy_load
self._version = None
self._producer = None
self._filename = None
self._symbols = OrderedDict()
# Set before anything that can raise, so cleanup() is safe if create fails.
self._finalizer = None
self._engine_impl = None
self._engine_kind = None
NeedsGamsDir.__init__(self, gams_dir=gams_dir)
# Build the I/O engine. For the gdxcc engine this binds the GDX library
# and creates the handle, which the engine owns and frees in close().
# `self.gams_dir` (resolved above) is threaded through to engine
# selection so the gams.transfer probe runs against the caller's
# actual install rather than the cached default-discovered one.
self._engine_kind = resolve_engine(engine, gams_dir=self.gams_dir)
self._engine_impl = make_engine(self._engine_kind, self.gams_dir, self.gams_dir_source)
# Free the engine's native resources exactly once, at the first of:
# cleanup(), garbage collection, or interpreter exit. The callback is the
# engine's own close() -- a bound method of the engine, not self -- so
# it never keeps this GdxFile alive (which would defeat GC-time
# finalization) and stays valid at interpreter shutdown (close() uses
# callables bound when the handle was created, not module-global lookups).
self._finalizer = weakref.finalize(self, self._engine_impl.close)
self.universal_set = GdxSymbol("*", GamsDataType.Set, dims=1, file=None, index=0)
self.universal_set._file = self
return
def cleanup(self) -> None:
if self._finalizer is not None:
self._finalizer() # runs engine.close() at most once
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.cleanup()
def clone(self) -> GdxFile:
"""
Returns a new GdxFile containing clones of the GdxSymbols in this
GdxFile. The clone will not be associated with a filename. The clone's
GdxSymbols will not have indexes. The clone will be ready to write to
a new file.
Returns
-------
:py:class:`GdxFile`
"""
result = GdxFile(gams_dir=self.gams_dir, lazy_load=False, engine=self._engine_kind)
for symbol in self:
result.append(symbol.clone())
result[-1]._file = result
return result
@property
def empty(self):
"""
Returns True if this GdxFile object does not contain any symbols.
Returns
-------
bool
"""
return len(self) == 0
@property
def filename(self):
"""
Filename this :py:class:`GdxFile` is associated with, if any
Returns
-------
None or str
"""
return self._filename
@property
def version(self):
"""
GDX file version
"""
return self._version
@property
def producer(self):
"""
What program wrote the GDX file
"""
return self._producer
@property
def num_elements(self):
"""
Total number of records present in this file, summed over all symbols.
Returns
-------
int
"""
return sum([symbol.num_records for symbol in self])
def read(self, filename: str | os.PathLike[str]) -> None:
"""
Opens gdx file at filename and reads meta-data. If not self.lazy_load,
also loads all symbols.
Throws an Error if not self.empty.
Throws a GdxError if any calls to gdxcc fail.
Parameters
----------
filename : pathlib.Path or str
"""
if not self.empty:
raise Error("GdxFile.read can only be used if the GdxFile is .empty")
# The engine reads file + symbol metadata and builds the GdxSymbol
# collection (records are not loaded here).
self._engine_impl.open_read(self, filename)
# read all symbols if not lazy_load
if not self.lazy_load:
self.load_all()
return
def load_all(self) -> None:
"""
Eagerly load every symbol's records into its :py:attr:`GdxSymbol.dataframe`.
Already-loaded symbols are skipped.
"""
self._engine_impl.load_file(self)
def load_symbols(self, names: Sequence[str]) -> None:
"""
Eagerly load the records of the named symbols (a subset of the file).
Resolves each name to its :py:class:`GdxSymbol` and loads via the engine
(gams.transfer issues a single targeted read; gdxcc loops per symbol).
Raises :class:`SymbolNotFoundError` for an unknown name; already-loaded
symbols are skipped.
"""
symbols = []
for name in names:
if name not in self:
raise SymbolNotFoundError(f"No symbol named {name!r} in {self.filename!r}.")
symbols.append(self[name])
self._engine_impl.load_symbols(self, symbols)
def reorder_for_strict_domains(self):
"""
Reorder ``self._symbols`` in place so every symbol follows the symbols it depends
on: each strict (``GdxSymbol``-ref) ``domain`` parent, and the parent Set of each
:py:attr:`GamsDataType.Alias`. Stable topological sort: symbols that don't reference
each other keep their current relative order. No-ops on cycles (logs a warning and
leaves the original order untouched).
Strict-domain and alias writes require the parent's ``gdxDataWriteDone`` (or, for an
alias, the parent's registration) to have completed before the dependent symbol is
written. Calling this before :py:meth:`write` is the easy way to satisfy that
constraint when symbols were appended in an unordered way.
"""
names = list(self._symbols.keys())
parents_of = {}
for s in self._symbols.values():
ps = set()
if s.domain is not None:
for d in s.domain:
if d is None or d is s:
continue
ps.add(d.name)
# An alias must be written after the Set it aliases.
if s.alias_of_name is not None and s.alias_of_name != s.name:
ps.add(s.alias_of_name)
parents_of[s.name] = ps
ordered, cycle = _stable_topological_sort(names, parents_of)
if cycle is not None:
logger.warning(
"reorder_for_strict_domains: cyclic domain references "
"detected; leaving symbol order untouched. Cycle "
"involves: %s",
sorted(cycle),
)
return
if ordered is None:
return
self._symbols = OrderedDict((name, self._symbols[name]) for name in ordered)
def write(self, filename: str | os.PathLike[str]) -> None:
"""
Writes this :py:class:`GdxFile` to filename
Parameters
----------
filename : pathlib.Path or str
"""
self._engine_impl.write_file(self, filename)
def __repr__(self):
return f"GdxFile(self,gams_dir={repr(self.gams_dir)},lazy_load={repr(self.lazy_load)})"
def __str__(self):
s = f"GdxFile containing {len(self)} symbols and {self.num_elements} elements."
sep = " Symbols:\n "
for symbol in self:
s += sep + str(symbol)
sep = "\n "
return s
def __getitem__(self, key):
"""
Supports list-like indexing and symbol-based indexing
Parameters
----------
key : int or str
If int, the index into the list of symbols. If str, the name of the symbol to
be accessed.
Returns
-------
:py:class:`GdxSymbol`
"""
return self._symbols[self._name_key(key)]
def __setitem__(self, key, value):
"""
Supports overwriting or adding a :py:class:`GdxSymbol` via a list-like interface
Parameters
----------
key : int
Must be an index into the list of symbols, within range(len(self)+1)
value : :py:class:`GdxSymbol`
"""
self._check_insert_setitem(key, value)
value._file = self
if key < len(self):
self._symbols[self._name_key(key)] = value
self._fixup_name_keys()
return
assert key == len(self)
self._symbols[value.name] = value
return
def __delitem__(self, key):
"""
Deletes a symbol from this :py:class:`GdxFile`'s collection
Parameters
----------
key : int or str
If int, the index into the list of symbols. If str, the name of the symbol to
be accessed.
"""
del self._symbols[self._name_key(key)]
return
def __len__(self):
"""
Number of :py:class:`GdxSymbol`s in this :py:class:`GdxFile`
"""
return len(self._symbols)
def insert(self, key: int, value: GdxSymbol) -> None:
"""
Inserts value at position key
Parameters
----------
key : int
Must be an index into the list of symbols, within range(len(self)+1)
value : :py:class:`GdxSymbol`
"""
self._check_insert_setitem(key, value)
value._file = self
if key == len(self) and value.name not in self._symbols:
# We can safely append the symbol. This is fast (O(log(n)) complexity)
self._symbols[value.name] = value
else:
# Need to insert inside the sequence. This is slow (O(n) complexity)
data = [(symbol.name, symbol) for symbol in self]
data.insert(key, (value.name, value))
self._symbols = OrderedDict(data)
return
def __contains__(self, key):
"""
Returns True if __getitem__ works with key.
"""
try:
self.__getitem__(key)
return True
except Exception:
return False
def keys(self) -> list[str]:
"""
List of symbol names obtained by iterating through this :py:class:`GdxFile`
Returns
-------
list of str
"""
return [symbol.name for symbol in self]
def _name_key(self, key):
name_key = key
if isinstance(key, int):
name_key = list(self._symbols.keys())[key]
return name_key
def _check_insert_setitem(self, key, value):
if not isinstance(value, GdxSymbol):
raise Error(f"GdxFiles only contain GdxSymbols. GdxFile was given a {type(value)}.")
if not isinstance(key, int):
raise Error(
"When adding or replacing GdxSymbols in GdxFiles, only integer, not name indices, may be used."
)
if key > len(self):
raise Error(f"Invalid key, {key}")
return
def _fixup_name_keys(self):
self._symbols = OrderedDict(
[(symbol.name, symbol) for _cur_key, symbol in self._symbols.items()]
)
return
# GAMS GDX type codes, hardcoded so `import gdxpds` needs no binding at module
# load. These mirror the gdxcc ``GMS_*`` constants; ``test_gms_constants_match_gdxcc``
# verifies the match whenever a binding is installed.
class GamsDataType(Enum):
Set = 0 # GMS_DT_SET
Parameter = 1 # GMS_DT_PAR
Variable = 2 # GMS_DT_VAR
Equation = 3 # GMS_DT_EQU
Alias = 4 # GMS_DT_ALIAS
class GamsVariableType(Enum):
Unknown = 0 # GMS_VARTYPE_UNKNOWN
Binary = 1 # GMS_VARTYPE_BINARY
Integer = 2 # GMS_VARTYPE_INTEGER
Positive = 3 # GMS_VARTYPE_POSITIVE
Negative = 4 # GMS_VARTYPE_NEGATIVE
Free = 5 # GMS_VARTYPE_FREE
SOS1 = 6 # GMS_VARTYPE_SOS1
SOS2 = 7 # GMS_VARTYPE_SOS2
Semicont = 8 # GMS_VARTYPE_SEMICONT
Semiint = 9 # GMS_VARTYPE_SEMIINT
# Offset by 53 so the values don't collide with GamsVariableType's; the GMS_EQUTYPE_*
# codes themselves are 0..5.
class GamsEquationType(Enum):
Equality = 53 + 0 # GMS_EQUTYPE_E
GreaterThan = 53 + 1 # GMS_EQUTYPE_G
LessThan = 53 + 2 # GMS_EQUTYPE_L
NothingEnforced = 53 + 3 # GMS_EQUTYPE_N
External = 53 + 4 # GMS_EQUTYPE_X
Conic = 53 + 5 # GMS_EQUTYPE_C
class GamsDomainType(Enum):
"""
Domain status of a :py:class:`GdxSymbol`. Member ``.value`` matches the
:c:func:`gdxSymbolGetDomainX` return code (1, 2, or 3 — see
``gdxcc.h``); ``gdxcc`` does not expose these as symbolic constants,
so the integers are written out explicitly here.
- ``NONE`` (1): no domain information stored.
- ``RELAXED`` (2): domain stored as plain string names (via
:c:func:`gdxSymbolSetDomainX`), no validation.
- ``REGULAR`` (3): strict domain (via :c:func:`gdxSymbolSetDomain`),
each non-wildcard dimension references an existing Set/Alias.
"""
NONE = 1
RELAXED = 2
REGULAR = 3
class GamsValueType(Enum):
Level = 0 # GMS_VAL_LEVEL, .l
Marginal = 1 # GMS_VAL_MARGINAL, .m
Lower = 2 # GMS_VAL_LOWER, .lo
Upper = 3 # GMS_VAL_UPPER, .ub
Scale = 4 # GMS_VAL_SCALE, .scale
@classmethod
def _missing_(cls, value):
if isinstance(value, str):
for value_type in cls:
if value_type.name == value:
return value_type
if value == "Value":
return GamsValueType(GamsValueType.Level)
super()._missing_(value)
GAMS_VALUE_COLS_MAP = defaultdict(lambda: [("Value", GamsValueType.Level.value)])
"""
List of value columns provided for each :py:attr:`GamsValueType`
"""
GAMS_VALUE_COLS_MAP[GamsDataType.Variable] = [
(value_type.name, value_type.value) for value_type in GamsValueType
]
GAMS_VALUE_COLS_MAP[GamsDataType.Equation] = GAMS_VALUE_COLS_MAP[GamsDataType.Variable]
GAMS_VALUE_DEFAULTS = {
GamsValueType.Level: 0.0,
GamsValueType.Marginal: 0.0,
GamsValueType.Lower: -np.inf,
GamsValueType.Upper: np.inf,
GamsValueType.Scale: 1.0,
}
"""
Default values for each :py:class:`GamsValueType`
"""
GAMS_VARIABLE_DEFAULT_LOWER_UPPER_BOUNDS = {
GamsVariableType.Unknown: (-np.inf, np.inf),
GamsVariableType.Binary: (0.0, 1.0),
GamsVariableType.Integer: (0.0, np.inf),
GamsVariableType.Positive: (0.0, np.inf),
GamsVariableType.Negative: (-np.inf, 0.0),
GamsVariableType.Free: (-np.inf, np.inf),
GamsVariableType.SOS1: (0.0, np.inf),
GamsVariableType.SOS2: (0.0, np.inf),
GamsVariableType.Semicont: (1.0, np.inf),
GamsVariableType.Semiint: (1.0, np.inf),
}
"""
Default lower and upper bounds for each :py:class:`GamsVariableType`
"""
class GdxSymbol:
def __init__(
self,
name: str,
data_type: GamsDataType | int,
dims: int | list[str] = 0,
file: GdxFile | None = None,
index: int | None = None,
description: str | None = "",
variable_type: GamsVariableType | int | None = None,
equation_type: GamsEquationType | int | None = None,
domain: Sequence[GdxSymbol | None] | None = None,
alias_of: GdxSymbol | None = None,
) -> None:
"""
In-memory representation of a GAMS GDX Symbol
Parameters
----------
name : str
data_type : :py:class:`GamsDataType`
dims : int or list of str
If dims is set to an int, then that number of dimensions will be created, each
indicated with the wildcard name '*'. Otherwise, a list of strings is expected,
each string being a dimension name.
file : None or :py:class:`GdxFile`
Users should not set file. File is set by, e.g., :py:meth:`GdxFile.read` and
:py:meth:`GdxFile.append`.
index : None or int
Users should not set file. File is set by, e.g., :py:meth:`GdxFile.read` and
:py:meth:`GdxFile.append`.
description : str
Human readable description for this :py:class:`GdxSymbol`
variable_type : None or :py:class:`GamsVariableType`
Only expected if data_type == :py:attr:`GamsDataType.Variable`
equation_type : None or :py:class:`GamsEquationType`
Only expected if data_type == :py:attr:`GamsDataType.Equation`
domain : None or list/tuple of (:py:class:`GdxSymbol` or None)
Strict (regular) domain references, one per dimension. ``None`` entries map to the
GAMS wildcard (``'*'``). When supplied, this flags the symbol for strict
:c:func:`gdxSymbolSetDomain` writes (subject to the parent existing in the file at
write time; otherwise the symbol falls back to relaxed). Plain strings are not
accepted here; use ``dims`` for string-only domains.
alias_of : None or :py:class:`GdxSymbol`
Only for ``data_type == GamsDataType.Alias``: the parent Set this alias refers to,
as a :py:class:`GdxSymbol` reference. The parent must exist in the same file at
write time (an alias has no relaxed fallback). See :py:attr:`alias_of`.
"""
self._name = name
self.description = description
self._loaded = False
self._data_type = GamsDataType(data_type)
self._variable_type = None
self.variable_type = variable_type
self._equation_type = None
self.equation_type = equation_type
self._dataframe = None
self._dims = None
self._domain = None
self._strict_on_disk = False
# Alias target: the parent Set, as a GdxSymbol ref (_alias_of) plus its
# name (_alias_of_name), the latter surviving when the ref can't yet be
# resolved (forward reference) or after a clone into another file.
self._alias_of = None
self._alias_of_name = None
# Record count per GAMS; meaningful only before load (afterwards
# num_records uses the dataframe). The engine's open_read overwrites
# this for symbols read from a file.
self._num_records = 0
self.dims = dims
if domain is not None:
self.domain = domain
if alias_of is not None:
self.alias_of = alias_of
assert self._dataframe is not None
self._file = file
self._index = index
# A symbol constructed without a file is being built for writing and is
# ready to use immediately. A symbol constructed with a file is being
# read: the engine's open_read populates its extended metadata (record
# count, variable/equation subtype, description, domain) and it stays
# unloaded until its records are pulled.
if self.file is None:
self._loaded = True
return
def clone(self) -> GdxSymbol:
"""
Create a copy of this :py:class:`GdxSymbol`.
The clone is independent of any :py:class:`GdxFile` -- its ``file`` is
``None`` and it has no ``index``. Append it to a destination
:py:class:`GdxFile` (e.g. via ``dest.append(cloned)``) before writing;
for an Alias, also call ``resolve_alias_of()`` so the parent name is
rebound to a same-file ref. :py:meth:`GdxFile.clone` does this wiring
for every symbol it copies.
Returns
-------
:py:class:`GdxSymbol`
"""
if not self.loaded:
raise Error(f"Symbol {repr(self.name)} cannot be cloned because it is not yet loaded.")
assert self.loaded
# Pass _domain directly to the constructor: the domain setter copies the list, so the
# clone has its own slot list pointing at the same parent GdxSymbols. Write-time name
# lookup resolves against whatever file the clone ends up in.
#
# For aliases, intentionally do NOT carry the live `alias_of` GdxSymbol ref --
# it would still point at the *original* file's parent, which is wrong once the
# clone is inserted elsewhere. Preserve only `alias_of_name`; the destination
# file resolves it against its own symbols at write time.
result = GdxSymbol(
self.name,
self.data_type,
dims=self.dims,
description=self.description,
variable_type=self.variable_type,
equation_type=self.equation_type,
domain=self._domain,
)
result._alias_of_name = self.alias_of_name
# An Alias has no records of its own -- its `.dataframe` is a view onto its
# parent (see the dataframe getter). The view resolves after the clone is
# inserted into a destination file and `resolve_alias_of()` rebinds the parent.
if self.data_type != GamsDataType.Alias:
result.dataframe = copy.deepcopy(self.dataframe)
assert result.loaded
return result
@property
def name(self):
"""
Name of this :py:class:`GdxSymbol`
Returns
-------
str
"""
return self._name
@name.setter
def name(self, value):
self._name = value
if self.file is not None:
self.file._fixup_name_keys()
return
@property
def description(self) -> str:
"""
Human-readable description for this :py:class:`GdxSymbol`. Never ``None``:
a ``None`` assigned here (e.g. via the ``append_*`` helpers, which default
``description`` to ``None``) is stored as ``""`` so :py:meth:`__str__` and
``gdxDataWriteStrStart`` always get a string.
Returns
-------
str
"""
return self._description
@description.setter
def description(self, value: str | None) -> None:
self._description = value if value is not None else ""
@property
def data_type(self):
"""
GAMS data type of this :py:class:`GdxSymbol`
Returns
-------
:py:class:`GamsDataType`
"""
return self._data_type
@data_type.setter
def data_type(self, value):
if not self.loaded or self.num_records > 0:
raise Error(
"Cannot change the data_type of a GdxSymbol that is yet to be read for file or contains records."
)
self._data_type = GamsDataType(value)
self.variable_type = None
self.equation_type = None
self._init_dataframe()
return
@property
def variable_type(self):
"""
Only not none if :py:attr:`data_type` == :py:attr:`GamsDataType.Variable`
Returns
-------
None or :py:attr:`GamsDataType.Variable`
"""
return self._variable_type
@variable_type.setter
def variable_type(self, value):
if self.data_type == GamsDataType.Variable:
if value is None:
# default to Free
self._variable_type = GamsVariableType.Free
else:
try:
self._variable_type = GamsVariableType(value)
except Exception:
if isinstance(self._variable_type, GamsVariableType):
logger.warning(f"Ignoring invalid GamsVariableType request '{value}'.")
return
logger.debug(f"Setting variable_type to {GamsVariableType.Free}.")
self._variable_type = GamsVariableType.Free
return
assert self.data_type != GamsDataType.Variable
if value is not None:
logger.warning("GdxSymbol is not a Variable, so setting variable_type to None")
self._variable_type = None
@property
def equation_type(self):
"""
Only not none if :py:attr:`data_type` == :py:attr:`GamsDataType.Equation`
Returns
-------
None or :py:attr:`GamsDataType.Equation`
"""
return self._equation_type
@equation_type.setter
def equation_type(self, value):
if self.data_type == GamsDataType.Equation:
if value is None:
# default to Equality
self._equation_type = GamsEquationType.Equality
else:
try:
self._equation_type = GamsEquationType(value)
except Exception:
if isinstance(self._equation_type, GamsEquationType):
logger.warning(f"Ignoring invalid GamsEquationType request '{value}'.")
return
logger.debug(f"Setting equation_type to {GamsEquationType.Equality}.")
self._equation_type = GamsEquationType.Equality
return
assert self.data_type != GamsDataType.Equation
if value is not None:
logger.warning("GdxSymbol is not an Equation, so setting equation_type to None")
self._equation_type = None
@property
def value_cols(self):
"""
List of (name, GamsValueType.value) tuples that describe the
value columns in the dataframe, that is, the columns that follow the
self.dims columns.
Returns
-------
list of (str, int)
"""
return GAMS_VALUE_COLS_MAP[self.data_type]
@property
def value_col_names(self):
"""
List of value column names, that is, the columns that follow the self.dims columns.
Returns
-------
list of str
"""
return [col_name for col_name, col_ind in self.value_cols]
def get_value_col_default(self, value_col_name):
if value_col_name not in self.value_col_names:
raise Error(
f"{value_col_name} is not one of the value columns for "
f"this GdxSymbol, which is a {self.data_type}"
)
value_col = GamsValueType(value_col_name)
if self.data_type in (GamsDataType.Set, GamsDataType.Alias):
assert value_col == GamsValueType.Level
# A Set/Alias value is its GAMS element text; "" means a member with
# no text. Membership itself is conveyed by row presence.
return ""
if (self.data_type == GamsDataType.Variable) and (
(value_col == GamsValueType.Lower) or (value_col == GamsValueType.Upper)
):
lb_default, ub_default = GAMS_VARIABLE_DEFAULT_LOWER_UPPER_BOUNDS[self.variable_type]
if value_col == GamsValueType.Lower:
return lb_default
else:
assert value_col == GamsValueType.Upper
return ub_default
return GAMS_VALUE_DEFAULTS[value_col]
@property
def file(self):
"""
:py:class:`GdxFile` file that contains this :py:class:`GdxSymbol`, if any
Returns
-------
None or :py:class:`GdxFile`
"""
return self._file
@property
def index(self):
"""
Index of this :py:class:`GdxSymbol` in its :py:class:`GdxFile`, if any
Returns
-------
None or int
"""
return self._index
@property
def loaded(self):
"""
Whether the data for this symbol has been loaded
Returns
-------
bool
"""
return self._loaded
@property
def full_typename(self):
if self.data_type == GamsDataType.Parameter and self.dims == 0:
return "Scalar"
elif self.data_type == GamsDataType.Variable:
return self.variable_type.name + " " + self.data_type.name
return self.data_type.name
@property
def dims(self):
"""
List of dimension names over which this symbol is defined. If the :py:class:`GdxSymbol` was
constructed with dims set to an integer, all dimension names will be the wildcard '*'.
Returns
-------
list of str
length of list is equal to :py:attr:`num_dims`
"""
return self._dims
@dims.setter