Source code for gamspy._symbols.alias

from __future__ import annotations

import os
import threading
import weakref
from typing import TYPE_CHECKING, Any, cast

from gams.core.gdx import GMS_DT_ALIAS

import gamspy as gp
import gamspy._algebra.condition as condition
import gamspy._algebra.expression as expression
import gamspy._algebra.operable as operable
import gamspy._symbols.implicits as implicits
import gamspy._validation as validation
import gamspy.utils as utils
from gamspy._symbols.base import BaseSymbol
from gamspy._symbols.set import SetMixin
from gamspy.exceptions import ValidationError

if TYPE_CHECKING:
    import pandas as pd

    from gamspy import Alias, Container, Set
    from gamspy._algebra.expression import Expression
    from gamspy._algebra.operation import Operation
    from gamspy._symbols.implicits import ImplicitSet
    from gamspy._types import IndexType, NormalizedDomainType, SetRecordsType


[docs] class Alias(operable.Operable, BaseSymbol, SetMixin): """ Represents an Alias symbol in GAMS. https://gamspy.readthedocs.io/en/latest/user/basics/alias.html Parameters ---------- container : Container Container of the alias. name : str, optional Name of the alias. alias_with : Set Alias set object. Examples -------- >>> import gamspy as gp >>> m = gp.Container() >>> i = gp.Set(m, "i") >>> j = gp.Alias(m, "j", i) """ @classmethod def _constructor_bypass( cls, container: Container, name: str, alias_with: Set | Alias ) -> Alias: # create new symbol object obj = object.__new__(cls) # legacy gtp attributes ## set private properties directly obj._container = cast( "Container", weakref.proxy(container) if not isinstance(container, weakref.ProxyType) else container, ) obj.name = name obj._alias_with = alias_with ## typing obj._gams_type = GMS_DT_ALIAS obj._gams_subtype = 1 ## add to container obj._container._data.update({name: obj}) # gamspy attributes obj.where = condition.Condition(obj) obj._latex_name = name.replace("_", r"\_") obj.container._add_statement(obj) obj._metadata = {} return obj def __new__( cls, container: Container | None = None, name: str | None = None, alias_with: Set | Alias | None = None, ): if container is not None and not isinstance(container, gp.Container): raise TypeError( f"Container must of type `Container` but found {type(container)}" ) if name is None: return object.__new__(cls) else: if not isinstance(name, str): raise TypeError(f"Name must of type `str` but found {type(name)}") try: if not container: container = gp._ctx_managers[ (os.getpid(), threading.get_native_id()) ] symbol = container._data[name] except KeyError: return object.__new__(cls) if isinstance(symbol, cls): if id(symbol.alias_with) != id(alias_with): raise ValueError( "Redefinition of an Alias symbol with a different `alias_with` object is not allowed!" ) return symbol raise TypeError( f"Cannot overwrite symbol `{name}` in container" " because it is not an Alias object)" ) def __init__( self, container: Container | None = None, name: str | None = None, alias_with: Set | Alias = None, # type: ignore ): self._metadata: dict[str, Any] = {} self._assignment: Expression | None = None if container is None: try: container = gp._ctx_managers[(os.getpid(), threading.get_native_id())] except KeyError as e: raise ValidationError("Alias requires a container.") from e self._container = cast("Container", weakref.proxy(container)) if name is not None: name = validation.validate_name(name) else: name = container._get_symbol_name(prefix="a") self.name = name # gtp attributes self._alias_with = self._validate_alias_with(alias_with) self._gams_type = GMS_DT_ALIAS self._gams_subtype = 1 self._container._data.update({name: self}) # gamspy attributes self._latex_name = self.name.replace("_", r"\_") self.where = condition.Condition(self) self._container._add_statement(self) self._container._synch_with_gams() @property def _should_unload_to_gams(self) -> bool: return self.alias_with._should_unload_to_gams @_should_unload_to_gams.setter def _should_unload_to_gams(self, value: bool) -> None: self.alias_with._should_unload_to_gams = value @property def _should_load_from_gams(self) -> bool: return self.alias_with._should_load_from_gams @_should_load_from_gams.setter def _should_load_from_gams(self, value: bool) -> None: self.alias_with._should_load_from_gams = value def _serialize(self) -> dict: info: dict[str, Any] = {"_metadata": self._metadata} if self._assignment is not None: info["_assignment"] = self._assignment.getDeclaration() return info def _deserialize(self, info: dict) -> None: for key, value in info.items(): if key == "_assignment": left, right = value.split(" = ") value = expression.Expression(left, "=", right) setattr(self, key, value) def __bool__(self): raise ValidationError( "Alias cannot be used as a truth value. Use len(<symbol>.records) instead." ) def __repr__(self) -> str: return f"Alias(name='{self.name}', alias_with={self.alias_with})" def __getitem__(self, indices: IndexType) -> ImplicitSet: domain = validation.validate_domain(self, indices) return implicits.ImplicitSet(self, name=self.name, domain=domain) def __setitem__(self, indices: IndexType, rhs: Expression | Operation | bool | str): # self[domain] = rhs domain = validation.validate_domain(self, indices) if isinstance(rhs, bool): rhs = "yes" if rhs is True else "no" statement = expression.Expression( implicits.ImplicitSet(self, name=self.name, domain=domain), "=", rhs, ) # Cannot validate definition if we are in a gp.Loop since the control indices can be provided by the gp.Loop if not self._container._in_loop: statement._validate_definition(utils._unpack(domain)) self._container._add_statement(statement) self._assignment = statement self.container._synch_with_gams() self._should_load_from_gams = True @property def records(self) -> pd.DataFrame | None: """ Returns the main symbol records Returns ------- DataFrame | None The main symbol records, None if no records were set """ return self.alias_with.records @records.setter def records(self, records): self.alias_with.records = records
[docs] def equals( self, other: Set | Alias, check_element_text: bool = True, check_meta_data: bool = True, ) -> bool: """ Used to compare the symbol to another symbol. Parameters ---------- other : Set or Alias The other symbol (Set or Alias) to compare with the current alias. check_element_text : bool, optional If True, check that all set elements have the same descriptive element text, otherwise skip. check_meta_data : bool, optional If True, check that symbol name and description are the same, otherwise skip. Returns ------- bool True if the two symbols are equal in the specified aspects; False if they are not equal. Examples -------- >>> import gamspy as gp >>> m = gp.Container() >>> i = gp.Set(m, "i") >>> j = gp.Alias(m, "j", i) >>> print(i.equals(j)) # Compare the Set 'i' with the Alias 'j' True """ return self.alias_with.equals( other, check_element_text=check_element_text, check_meta_data=check_meta_data, )
[docs] def toList(self, include_element_text: bool = False) -> list: """ Convenience method to return symbol records as a python list Parameters ---------- include_element_text : bool, optional If True, include the element text as tuples (record, element text). If False, return a list of records only. Returns ------- list A list containing the records of the symbol. Examples -------- >> import gamspy as gp >>> m = gp.Container() >>> i = gp.Set(m, "i", records=["new-york", "chicago", "topeka"]) >>> j = gp.Alias(m, "j", i) >>> print(j.toList()) ['new-york', 'chicago', 'topeka'] """ return self.alias_with.toList(include_element_text=include_element_text)
[docs] def pivot( self, index: list[str] | str | None = None, columns: list[str] | str | None = None, fill_value: int | float | None = None, ) -> pd.DataFrame | None: """ Convenience function to pivot records into a new shape (only symbols with > 1D can be pivoted). Parameters ---------- index : list[str] | str, optional If index is None then it is set to dimensions [0..dimension-1] columns : list[str] | str, optional If columns is None then it is set to the last dimension. fill_value : int | float, optional Missing values in the pivot will take the value provided by fill_value Returns ------- DataFrame A new DataFrame containing the pivoted data. Examples -------- >>> import gamspy as gp >>> m = gp.Container() >>> i = gp.Set(m, "i", records=["seattle", "san-diego"]) >>> j = gp.Set(m, "j", records=["new-york", "chicago", "topeka"]) >>> ij = gp.Set(m, "ij", [i,j], records=[("seattle", "chicago"), ("seattle", "topeka"), ("san-diego", "new-york")]) >>> routes = gp.Alias(m, name="routes", alias_with=ij) >>> print(routes.pivot(fill_value="")) # doctest: +NORMALIZE_WHITESPACE chicago topeka new-york seattle True True san-diego True """ return self.alias_with.pivot(index, columns, fill_value)
[docs] def getSparsity(self) -> float | None: """ Gets the sparsity of the symbol w.r.t the cardinality Returns ------- float | None Sparsity of an alias """ return self.alias_with.getSparsity()
@property def is_singleton(self) -> bool: """ if symbol is a singleton set Returns ------- bool True if the alias is singleton; False otherwise """ return self.alias_with.is_singleton def _getUELCodes(self, dimension, ignore_unused=False): return self.alias_with._getUELCodes(dimension, ignore_unused=ignore_unused) def _getUELs( self, dimensions: list[int] | int | None = None, ignore_unused: bool = False, ) -> list[str]: return self.alias_with._getUELs( dimensions=dimensions, ignore_unused=ignore_unused ) def _removeUELs( self, uels: list[str] | str | None = None, dimensions: list[int] | int | None = None, ) -> None: return self.alias_with._removeUELs(uels=uels, dimensions=dimensions) def _assert_valid_records(self): self.alias_with._assert_valid_records() @property def alias_with(self) -> Set: """ Returns the aliased object Returns ------- Set The aliased Set """ return self._alias_with def _validate_alias_with(self, alias_with: Set | Alias) -> Set: from gamspy._symbols import Set, UniverseAlias if alias_with is None: raise ValueError("`alias_With` cannot be None.") if isinstance(alias_with, UniverseAlias): raise TypeError( "Cannot create an Alias to a UniverseAlias, create a new UniverseAlias symbol instead." ) if not isinstance(alias_with, (Set, Alias)): raise TypeError("Symbol 'alias_with' must be type Set or Alias") if isinstance(alias_with, Alias): parent = alias_with while not isinstance(parent, Set): parent = parent.alias_with alias_with = parent return alias_with @property def domain_names(self) -> list[str]: """ Returns the string version of domain names Returns ------- list[str] A list of string version of domain names """ return self.alias_with.domain_names @property def domain(self) -> NormalizedDomainType: """ Returns list of domains given either as string (* for universe set) or as reference to the Set/Alias object Returns ------- list[Set | str] A list of domains given either as string (* for universe set) or as reference to the Set/Alias object """ return self.alias_with.domain @property def domain_type(self) -> str | None: """ Returns the state of domain links Returns ------- str none, relaxed or regular """ return self.alias_with.domain_type @property def description(self) -> str: """ Returns description of symbol Returns ------- str Description of symbol """ return self.alias_with.description @property def dimension(self) -> int: """ Returns the dimension of symbol Returns ------- int Dimension of symbol """ return self.alias_with.dimension @property def number_records(self) -> int: """ Returns the number of symbol records Returns ------- int Number of symbol records """ return self.alias_with.number_records @property def domain_labels(self) -> list[str] | None: """ Returns the column headings for the records DataFrame Returns ------- list[str] | None Column headings for the records DataFrame """ return self.alias_with.domain_labels @domain_labels.setter def domain_labels(self, labels): self.alias_with.domain_labels = labels @property def summary(self) -> dict: """ Returns a dict of only the metadata Returns ------- dict Outputs a dict of only the metadata """ return { "name": self.name, "description": self.description, "alias_with": self.alias_with.name, "is_singleton": self.is_singleton, "domain": self.domain_names, "domain_type": self.domain_type, "dimension": self.dimension, "number_records": self.number_records, } def _setRecords( self, records: SetRecordsType, *, uels_on_axes: bool = False ) -> None: self.alias_with.setRecords(records, uels_on_axes=uels_on_axes)
[docs] def setRecords(self, records: SetRecordsType, uels_on_axes: bool = False) -> None: """ Sets the records of the Set that is aliased. This is a convenience method to load data into the set. It handles various input formats like lists and pandas DataFrames. Parameters ---------- records : pd.DataFrame | pd.Series | Sequence The data to load. Common formats: - List of strings: `['i1', 'i2']` - List of tuples (for multi-dimensional sets): `[('a', '1'), ('b', '2')]` - pandas DataFrame. uels_on_axes : bool, optional If True, assumes that the domain information is located in the axes (index/columns) of the `records` object rather than the data values. Use this when passing a DataFrame where the indices represent the set elements. Examples -------- >>> import gamspy as gp >>> m = gp.Container() >>> i = gp.Set(m, name="i") >>> j = gp.Alias(m, name="j", alias_with=i) >>> j.setRecords(["seattle", "san-diego"]) >>> j.records.values.tolist() [['seattle', ''], ['san-diego', '']] """ self.alias_with.setRecords(records, uels_on_axes)
[docs] def gamsRepr(self) -> str: """ Returns the string representation of this Alias in the GAMS language. (e.g., 'j'). Returns ------- str The GAMS string representation. Examples -------- >>> import gamspy as gp >>> m = gp.Container() >>> i = gp.Set(m, "i", domain=["*"], records=['i1','i2']) >>> j = gp.Alias(m, "j", i) >>> j.gamsRepr() 'j' """ return self.name
[docs] def getDeclaration(self) -> str: """ Returns the GAMS declaration statement for this Alias. (e.g., 'Alias(i, j);'). Returns ------- str The GAMS declaration string. Examples -------- >>> import gamspy as gp >>> m = gp.Container() >>> i = gp.Set(m, "i", records=['i1','i2']) >>> j = gp.Alias(m, "j", i) >>> j.getDeclaration() 'Alias(i,j);' """ return f"Alias({self.alias_with.name},{self.name});"
def getAssignment(self) -> str: """ Latest assignment to the Set in GAMS Returns ------- str Examples -------- >>> import gamspy as gp >>> m = gp.Container() >>> i = gp.Set(m, "i", records=['i1','i2']) >>> j = gp.Alias(m, "j", alias_with=i) >>> j['i1'] = False >>> j.getAssignment() 'j("i1") = no;' """ if self._assignment is None: raise ValidationError("Set was not assigned!") return self._assignment.getDeclaration()