Skip to content

Commit 7a7f052

Browse files
elainethaleclaude
andcommitted
refactor!: rename aliased_with -> alias_of; document universe-alias dispatch
BREAKING (within v3.0.0 — alias support is new in this PR): rename the alias relationship attribute on GdxSymbol from `aliased_with` to `alias_of`. The old name was a gdxpds invention; gams.transfer uses `alias_with` (active voice; not `aliased_with`) and gdxcc has no Python-attribute analog. `alias_of` reads naturally ("at is `alias_of` t"), matches the active voice of the gams.transfer naming, and isn't confused with the unrelated "aliased with" phrasing that suggested help-from a parent. Renames propagated: `GdxSymbol.alias_of` / `alias_of_name`, `resolve_alias_of()`, the `alias_of=` constructor kwarg, and the corresponding private attrs. Also adds a comment in `_gdxcc_engine.GdxccEngine.write_symbol` cross- referencing the gams.transfer engine's `gt.Alias` / `gt.UniverseAlias` dispatch: `gdxAddAlias` accepts `"*"` as a parent without any special-case call, so the gdxcc engine has a single uniform write path for both named-Set aliases and universe aliases. Test matrix on this branch: PASS / PASS / PASS / OK across .venv-gams-{34,49,51} and .venv-no-gams. Ruff and pyright clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bcca44f commit 7a7f052

10 files changed

Lines changed: 103 additions & 100 deletions

File tree

CHANGES.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ v3.0.0, 05/25/26 -- BREAKING: the default I/O engine is now gams.transfer when i
66
conveyed by row presence. The load_set_text argument is removed: text
77
is always read, and any non-empty text is always written
88
aliases are now fully supported: they read with a populated
9-
GdxSymbol.aliased_with (the parent Set), round-trip through both
9+
GdxSymbol.alias_of (the parent Set), round-trip through both
1010
engines, and can be created via to_gdx(aliases={alias: parent}) or
1111
gdxpds.gdx.append_alias()
1212
BREAKING: GDX UNDEF (Python None) is now preserved on write by both

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ Things that aren't obvious from one file:
9797
- **Symbol kinds drive column shape.** `GamsDataType` ([src/gdxpds/gdx.py](src/gdxpds/gdx.py)) — Set, Parameter, Variable, Equation, Alias. Variables and Equations get five value columns (Level, Marginal, Lower, Upper, Scale); Parameters and Sets get a single `Value` column. Write code in [src/gdxpds/write_gdx.py](src/gdxpds/write_gdx.py) infers the type from DataFrame shape and naming.
9898
- **Special values.** GAMS encodes NA/EPS/+Inf/-Inf/UNDEF as fixed magic floats (e.g. 1E300, 2E300, 3E300). [src/gdxpds/special.py](src/gdxpds/special.py) converts these to/from numpy equivalents (`np.nan`, `np.inf`, and `None` for UNDEF) on read/write. Parameters get this conversion; Sets/Aliases do not (their value column is text, see below). UNDEF (`None`) is preserved on write by both engines; the gams.transfer write passes `eps_to_zero=False` so EPS survives too.
9999
- **Set value = element text; membership = row presence.** A Set/Alias has one `Value` column holding the GAMS element text (a string; `""` = a member with no text). Every row is a member — there is no boolean. `_fixup_set_value` ([src/gdxpds/gdx.py](src/gdxpds/gdx.py)) normalizes the column to text on assignment (a `bool`/`c_bool`/missing value → `""`), so a Set can be built from dims alone, from booleans, or from text. The read path always fetches text (`gdxGetElemText()` on gdxcc; the records frame on gams.transfer); there is no `load_set_text` flag.
100-
- **Aliases.** An Alias reads like the Set it aliases and records the parent in `GdxSymbol.aliased_with` (a parent ref, or `None`). It is written by both engines (`gdxAddAlias` / `gt.Alias`); the parent must precede it (no relaxed fallback — `DomainError` otherwise). `to_gdx(aliases={alias: parent})` and `gdxpds.gdx.append_alias()` build them; ordering follows `reorder_for_strict_domains()`, which now adds the alias→parent edge. The parent is typically a Set, but an Alias is accepted too: GDX supports chained aliases, and the gdxcc engine preserves the chain on disk (`aat -> at -> t`) while gams.transfer flattens it to the root (`aat -> t`). On read both engines produce a same-file `aliased_with` ref. *Universe* aliases (alias of `*`) are a documented edge: they read without error (`aliased_with` resolves to `universal_set`) and round-trip within one engine, but the engines disagree on membership (gdxcc includes the `*` element, gams.transfer doesn't), so `universe_alias_fixture.gdx` is excluded from the cross-engine parity glob and covered by `tests/test_alias.py`.
100+
- **Aliases.** An Alias reads like the Set it aliases and records the parent in `GdxSymbol.alias_of` (a parent ref, or `None`). It is written by both engines (`gdxAddAlias` / `gt.Alias`); the parent must precede it (no relaxed fallback — `DomainError` otherwise). `to_gdx(aliases={alias: parent})` and `gdxpds.gdx.append_alias()` build them; ordering follows `reorder_for_strict_domains()`, which now adds the alias→parent edge. The parent is typically a Set, but an Alias is accepted too: GDX supports chained aliases, and the gdxcc engine preserves the chain on disk (`aat -> at -> t`) while gams.transfer flattens it to the root (`aat -> t`). On read both engines produce a same-file `alias_of` ref. *Universe* aliases (alias of `*`) are a documented edge: they read without error (`alias_of` resolves to `universal_set`) and round-trip within one engine, but the engines disagree on membership (gdxcc includes the `*` element, gams.transfer doesn't), so `universe_alias_fixture.gdx` is excluded from the cross-engine parity glob and covered by `tests/test_alias.py`.
101101
- **Lazy + idempotent GAMS bind.** `load_gdxcc()` in [src/gdxpds/tools.py](src/gdxpds/tools.py) binds the GAMS library and populates `gdxpds.special` dicts on the first GDX op (called by the gdxcc engine's `__init__` before it creates a handle, and by `info()` inside try/except for diagnostics). Process state: `tools._bindings_source`, `tools._loaded_gams_dir`.
102102
- **`gams_dir=` on the first GDX op selects the bound install.** Once loaded, subsequent `gdxCreateD(H, <dir>, ...)` calls are no-ops against the bound library regardless of `<dir>`; `load_gdxcc()` warns when a caller passes a `gams_dir` that differs from `_loaded_gams_dir`. One GAMS library per process — multi-version testing is one-venv-per-GAMS. In-process swap is feasible via `gdxLibraryUnload()` but unimplemented (design notes tracked in a GitHub issue).
103103
- **GDX handle lifecycle** (SWIG-bound `gdxcc`; gdxcc engine only — the gams.transfer engine holds no handle). The full `new_gdxHandle_tp``gdxCreateD``gdxFree``delete_gdxHandle_tp` sequence lives in one place: the `_GdxHandle` RAII class in [src/gdxpds/tools.py](src/gdxpds/tools.py), used by all three create sites (`load_gdxcc` and `load_specials` via `with`; `GdxccEngine.__init__` keeps the instance). It encodes two SWIG hazards so callers don't have to:

doc/source/overview.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ If you prefer the simpler API that works with dicts of DataFrames, see [the `dom
293293
GAMS lets one Set be an *alias* of another — `alias(t, at)` makes `at` another name for set `t`. `gdxpds` surfaces the relationship on `GdxSymbol`:
294294

295295
- `GdxSymbol.data_type` is {py:attr}`gdxpds.gdx.GamsDataType.Alias`, and the symbol reads like the Set it aliases (same elements, same element text).
296-
- `GdxSymbol.aliased_with` is the parent Set as a `GdxSymbol` reference (or `None` for non-aliases). Unlike a relaxed domain, an alias has no fallback: its parent must exist in the file when it is written, or the write raises {py:class}`gdxpds.DomainError`.
296+
- `GdxSymbol.alias_of` is the parent Set as a `GdxSymbol` reference (or `None` for non-aliases). Unlike a relaxed domain, an alias has no fallback: its parent must exist in the file when it is written, or the write raises {py:class}`gdxpds.DomainError`.
297297

298298
**Viewing on read:**
299299

@@ -303,10 +303,10 @@ with gdxpds.gdx.GdxFile(lazy_load=False) as gdx:
303303
gdx.read('data.gdx')
304304
at = gdx['at']
305305
print(at.data_type) # GamsDataType.Alias
306-
print(at.aliased_with is gdx['t']) # True — points at the parent Set
306+
print(at.alias_of is gdx['t']) # True — points at the parent Set
307307
```
308308

309-
**Setting on write.** Build the parent Set, then the alias — via {py:func}`gdxpds.gdx.append_alias` or a `GdxSymbol` with `aliased_with`:
309+
**Setting on write.** Build the parent Set, then the alias — via {py:func}`gdxpds.gdx.append_alias` or a `GdxSymbol` with `alias_of`:
310310

311311
```python
312312
import gdxpds.gdx
@@ -327,9 +327,9 @@ print(gdxpds.get_aliases('data.gdx')) # {'at': 't'}
327327
```
328328

329329
:::{note}
330-
Aliases of a *named Set* (the common case) are fully supported on both engines. A **universe alias** — an alias of the universe set `*` (`aliased_with` resolves to the file's `universal_set`) — reads without error and round-trips within a single engine, but the engines disagree on its membership (`gdxcc` includes the `*` element, `gams.transfer` does not), so it is not cross-engine identical.
330+
Aliases of a *named Set* (the common case) are fully supported on both engines. A **universe alias** — an alias of the universe set `*` (`alias_of` resolves to the file's `universal_set`) — reads without error and round-trips within a single engine, but the engines disagree on its membership (`gdxcc` includes the `*` element, `gams.transfer` does not), so it is not cross-engine identical.
331331

332-
**Chained aliases (alias of an alias)** are also supported: GDX itself permits a chain (`aat -> at -> t`), and both engines accept it on write and resolve `aliased_with` to a same-file symbol on read. The two engines differ on what reaches disk: the `gdxcc` engine preserves the chain (`aat -> at`), while `gams_transfer` flattens to the root (`aat -> t`). Either form reads back identically through `gdxpds`.
332+
**Chained aliases (alias of an alias)** are also supported: GDX itself permits a chain (`aat -> at -> t`), and both engines accept it on write and resolve `alias_of` to a same-file symbol on read. The two engines differ on what reaches disk: the `gdxcc` engine preserves the chain (`aat -> at`), while `gams_transfer` flattens to the root (`aat -> t`). Either form reads back identically through `gdxpds`.
333333
:::
334334

335335
### Parameter, Variable, and Equation details

src/gdxpds/_gdxcc_engine.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def open_read(self, gdx_file: GdxFile, filename: str | os.PathLike[str]) -> None
114114
# No-op for well-formed files -- each in-line attempt already succeeded.
115115
for symbol in gdx_file:
116116
symbol.resolve_domain()
117-
symbol.resolve_aliased_with()
117+
symbol.resolve_alias_of()
118118

119119
def _make_symbol(self, gdx_file: GdxFile, name: str, data_type, dims, index: int) -> GdxSymbol:
120120
"""Construct a GdxSymbol and populate its extended gdxcc metadata."""
@@ -134,8 +134,8 @@ def _make_symbol(self, gdx_file: GdxFile, name: str, data_type, dims, index: int
134134
# resolve it to a same-file ref (open_read self-heals forward refs).
135135
pret, parent_name, _pdims, _pdtype = gdxcc.gdxSymbolInfo(H, userinfo)
136136
if pret == 1:
137-
symbol._aliased_with_name = parent_name
138-
symbol.resolve_aliased_with()
137+
symbol._alias_of_name = parent_name
138+
symbol.resolve_alias_of()
139139
symbol.description = description
140140
if index > 0:
141141
ret, gdx_domain = gdxcc.gdxSymbolGetDomainX(H, index)
@@ -264,10 +264,15 @@ def write_symbol(
264264
if symbol.data_type == GamsDataType.Alias:
265265
# An alias carries no records of its own; it is registered against its
266266
# parent Set, which must already be written (no relaxed fallback).
267-
parent = symbol.aliased_with_name
267+
# gdxAddAlias accepts the universe set "*" as a parent without any
268+
# special-cased call, so a universe alias goes through the same path
269+
# as a named-Set alias (cf. the gt.Alias / gt.UniverseAlias dispatch
270+
# in the gams.transfer engine, which is needed because that library
271+
# has separate types for the two cases).
272+
parent = symbol.alias_of_name
268273
if parent is None:
269274
raise DomainError(
270-
f"Cannot write alias {symbol.name!r}: no parent Set (aliased_with) is set."
275+
f"Cannot write alias {symbol.name!r}: no parent Set (alias_of) is set."
271276
)
272277
if not gdxcc.gdxAddAlias(H, parent, symbol.name):
273278
raise GdxError(

src/gdxpds/_transfer_engine.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -209,10 +209,10 @@ def _add_symbol(self, container, symbol: GdxSymbol, name_positions: dict) -> Non
209209
if data_type == GamsDataType.Alias:
210210
# An alias carries no records of its own; it points at its parent Set,
211211
# which must already be in the container (no relaxed fallback).
212-
parent = symbol.aliased_with_name
212+
parent = symbol.alias_of_name
213213
if parent is None:
214214
raise DomainError(
215-
f"Cannot write alias {symbol.name!r}: no parent Set (aliased_with) is set."
215+
f"Cannot write alias {symbol.name!r}: no parent Set (alias_of) is set."
216216
)
217217
universe = (
218218
symbol.file.universal_set.name
@@ -302,7 +302,7 @@ def open_read(self, gdx_file: GdxFile, filename: str | os.PathLike[str]) -> None
302302
# the dependent symbol).
303303
for symbol in gdx_file:
304304
symbol.resolve_domain()
305-
symbol.resolve_aliased_with()
305+
symbol.resolve_alias_of()
306306

307307
def _make_symbol(self, gdx_file: GdxFile, name: str, gt_sym, index: int) -> GdxSymbol:
308308
data_type = _data_type_of(gt_sym)
@@ -319,8 +319,8 @@ def _make_symbol(self, gdx_file: GdxFile, name: str, gt_sym, index: int) -> GdxS
319319
parent = getattr(gt_sym, "alias_with", None)
320320
parent_name = parent if isinstance(parent, str) else getattr(parent, "name", None)
321321
if parent_name is not None:
322-
symbol._aliased_with_name = parent_name
323-
symbol.resolve_aliased_with()
322+
symbol._alias_of_name = parent_name
323+
symbol.resolve_alias_of()
324324
# A non-wildcard domain entry (a Set reference) means a strict/regular
325325
# domain; mark it and resolve names to same-file GdxSymbol refs.
326326
if any(not isinstance(d, str) for d in gt_sym.domain):
@@ -345,7 +345,7 @@ def load_symbols(
345345
read_names = {s.name for s in targets}
346346
universe = gdx_file.universal_set.name if gdx_file.universal_set is not None else "*"
347347
for s in targets:
348-
parent = s.aliased_with_name
348+
parent = s.alias_of_name
349349
if s.data_type == GamsDataType.Alias and parent and parent != universe:
350350
read_names.add(parent)
351351
container = self._read_records(gdx_file, list(read_names)) if targets else None

0 commit comments

Comments
 (0)