Source code for gamspy.math.matrix

#
# GAMS - General Algebraic Modeling System Python API
#
# Copyright (c) 2023 GAMS Development Corp. <support@gams.com>
# Copyright (c) 2023 GAMS Software GmbH <support@gams.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
from __future__ import annotations

from collections.abc import Iterable
from dataclasses import dataclass
from typing import TYPE_CHECKING

import gamspy._symbols.implicits as implicits
import gamspy.math
import gamspy.utils as utils
from gamspy._symbols.parameter import Parameter
from gamspy._symbols.set import Set
from gamspy._symbols.variable import Variable
from gamspy.exceptions import GamspyException, ValidationError

if TYPE_CHECKING:
    from gamspy import Container
    from gamspy._algebra.expression import Expression
    from gamspy._algebra.operation import Operation
    from gamspy._symbols.alias import Alias


[docs] def vector_norm( x: ( Parameter | Variable | implicits.ImplicitParameter | implicits.ImplicitVariable | Expression | Operation ), ord: float | int = 2, dim: list[int] | list[Set | Alias] | None = None, ) -> Operation | Expression: """ Returns the vector norm of the provided vector x. If ord is not an even integer, absolute value is used which requires DNLP. Parameters ---------- x : Parameter | Variable | implicits.ImplicitParameter | implicits.ImplicitVariable | Expression | Operation ord: int | float dim: list[int] | list[Set | Alias], optional Returns ------- Expression | Operation Examples -------- >>> import gamspy as gp >>> import math >>> m = gp.Container() >>> i = gp.Set(m, name="i", records=["i1", "i2"]) >>> vec = gp.Parameter(m, "vec", domain=[i], records=[("i1", 3), ("i2", 4)]) >>> vlen = gp.Parameter(m, "vlen", domain=[]) >>> vlen[...] = gp.math.vector_norm(vec) >>> math.isclose(vlen.toValue(), 5, rel_tol=1e-4) True """ import gamspy._algebra.operation as operation from gamspy._symbols.alias import Alias if isinstance(ord, float): if ord.is_integer(): ord = int(ord) elif ord in [float("inf"), float("-inf")]: raise ValidationError("Infinity norms are not supported") if ord == 0: raise ValidationError("0 norm is not supported") even = isinstance(ord, int) and ord % 2 == 0 domain = x.domain if len(domain) == 0: raise ValidationError("Provided argument is a scalar") sum_domain = domain if dim is not None: if not isinstance(dim, Iterable): raise ValidationError("dim must be an Iterable") if len(dim) == 0: raise ValidationError("If dim is provided, it must contain items") if isinstance(dim[0], int): for item in dim: if not isinstance(item, int): raise ValidationError( "If dim is provided, either all items must be integers" " or all items must be Sets" ) sum_domain = [] for d in dim: sum_domain.append(domain[d]) else: for item in dim: if not isinstance(item, (Set, Alias)): raise ValidationError( "If dim is provided, either all items must be integers" " or all items must be Sets" ) sum_domain = dim if ord == 2: return gamspy.math.sqrt( operation.Sum(sum_domain, gamspy.math.sqr(x[domain])), safe_cancel=True, ) elif even: return gamspy.math.rpower( operation.Sum(sum_domain, x[domain] ** ord), (1 / ord) ) elif ord == 1: return operation.Sum(sum_domain, gamspy.math.abs(x[domain])) return gamspy.math.rpower( operation.Sum(sum_domain, gamspy.math.abs(x[domain]) ** ord), (1 / ord), )
[docs] def next_alias(symbol: Alias | Set) -> Alias: """Provided the set or alias, it returns the next alias. If it is not found, it creates the alias. This function is mainly for matrix multiplication conflict resolution but it might be helpful in the cases where you need to generate many aliases from a set. Parameters ---------- symbol : Set | Alias Returns ------- Alias Examples -------- >>> import gamspy as gp >>> m = gp.Container() >>> i = gp.Set(m, name="i", records=["i1", "i2", "i3"]) >>> j = gp.math.next_alias(i) >>> j.name 'AliasOfi_2' >>> k = gp.math._generate_dims(m, [10])[0] >>> k.name 'DenseDim10_1' >>> k2 = gp.math.next_alias(k) >>> k2.name 'DenseDim10_2' """ current = symbol if symbol.name.startswith("DenseDim") or symbol.name.startswith("AliasOf"): prefix, num = symbol.name.split("_") else: prefix, num = f"AliasOf{symbol.name}", 1 num = int(num) + 1 expected_name = f"{prefix}_{num}" find_x = symbol.container.data.get(expected_name, None) if find_x is None: find_x = symbol.container.addAlias(expected_name, alias_with=current) return find_x
[docs] @dataclass class Dim: dims: list[int]
[docs] def dim(dims: list[int]) -> Dim: """Returns an array where each element corresponds to a set where the dimension of the set is equal to the element in dims. If same dimension size used, then next free alias will be returned. Symbols are generated once the Dim object is passed to a constructor that supports it. Parameters ---------- dims: list[int] Returns ------- Dim Examples -------- >>> import gamspy as gp >>> import math >>> m = gp.Container() >>> a = gp.math.dim([10, 20]) # nothing generated yet >>> a Dim(dims=[10, 20]) >>> par = gp.Parameter(m, name="par", domain=a) # now two sets are generated >>> par.domain [Set(name=DenseDim10_1, domain=['*']), Set(name=DenseDim20_1, domain=['*'])] >>> par2 = gp.Parameter(m, name="par2", domain=a) # same 2 sets are used >>> par2.domain [Set(name=DenseDim10_1, domain=['*']), Set(name=DenseDim20_1, domain=['*'])] """ for x in dims: if not isinstance(x, int): raise ValidationError("Dimensions must be integers") return Dim(dims=dims) # type: ignore
def _generate_dims(m: Container, dims: list[int]) -> list[Alias | Set]: sets_so_far = [] for x in dims: expected_name = f"DenseDim{x}_1" find_x = m.data.get(expected_name, None) if find_x is None: find_x = m.addSet(name=expected_name, records=range(x)) while find_x in sets_so_far: find_x = next_alias(find_x) sets_so_far.append(find_x) return sets_so_far
[docs] def trace( x: ( Parameter | implicits.ImplicitParameter | Variable | implicits.ImplicitVariable ), axis1: int = 0, axis2: int = 1, ) -> Operation: """Returns trace of the given input x. By default trace of zeroth and first axis used. `axis1` and `axis2` parameters control on which axes to get trace. Domains at the axis1 and axis2 must be same or aliases. Parameters ---------- x: ( Parameter | implicits.ImplicitParameter | Variable | implicits.ImplicitVariable ) axis1=0 axis2=1 Returns ------- Operation Examples -------- >>> import gamspy as gp >>> import numpy as np >>> m = gp.Container() >>> identity = np.eye(3, 3) >>> mat = gp.Parameter(m, name="mat", domain=gp.math.dim([3, 3]), records=identity, uels_on_axes=True) >>> sc = gp.Parameter(m, name="sc", domain=[]) >>> sc[...] = gp.math.trace(mat) >>> int(sc.toDense()) 3 """ import gamspy._algebra.operation as operation if len(x.domain) < 2: raise ValidationError("Trace requires at least 2 dimensions") if not utils.setBaseEqual(x.domain[axis1], x.domain[axis2]): raise ValidationError("Matrix dimensions are not equal") domain = [i for i in x.domain] domain[axis1] = domain[axis2] return operation.Sum(domain[axis2], x[domain])
[docs] def permute( x: ( Parameter | implicits.ImplicitParameter | Variable | implicits.ImplicitVariable ), dims: list[int], ) -> implicits.ImplicitVariable | implicits.ImplicitParameter: """Permutes the dimensions provided input `x` using `dim`. Similar to PyTorch permute. Parameters ---------- x: ( Parameter | implicits.ImplicitParameter | Variable | implicits.ImplicitVariable ) dims: list[int] Returns ------- implicits.ImplicitVariable | implicits.ImplicitParameter Examples -------- >>> import gamspy as gp >>> m = gp.Container() >>> i = gp.Set(m, name="i") >>> j = gp.Set(m, name="j") >>> k = gp.Set(m, name="k") >>> p = gp.Parameter(m, name="p", domain=[i, j, k]) >>> p2 = gp.math.permute(p, [2, 0, 1]) >>> p2.domain [Set(name=k, domain=['*']), Set(name=i, domain=['*']), Set(name=j, domain=['*'])] """ # TODO Accept permuting expressions! # Might be needed in some context for i in dims: if not isinstance(i, int): raise ValidationError("Permute dimensions must be integers") dims_len = len(dims) if min(dims) != 0 or max(dims) != dims_len - 1: raise ValidationError( "Permute requires the order of indices from 0 to n-1" ) if len(set(dims)) != dims_len: raise ValidationError("Permute dimensions must be unique") permuted_domain = utils._permute_domain(x.domain, dims) if isinstance(x, Parameter): return implicits.ImplicitParameter( x, name=x.name, records=x.records, domain=permuted_domain, permutation=dims, ) elif isinstance(x, implicits.ImplicitParameter): if x.permutation is not None: dims = utils._permute_domain(x.permutation, dims) return implicits.ImplicitParameter( x.parent, name=x.name, records=x._records, domain=permuted_domain, permutation=dims, scalar_domains=x._scalar_domains, ) elif isinstance(x, Variable): return implicits.ImplicitVariable( x, name=x.name, domain=permuted_domain, permutation=dims ) elif isinstance(x, implicits.ImplicitVariable): if x.permutation is not None: dims = utils._permute_domain(x.permutation, dims) return implicits.ImplicitVariable( x.parent, name=x.name, domain=permuted_domain, permutation=dims, scalar_domains=x._scalar_domains, ) raise GamspyException(f"permute not implemented for {type(x)}")
def _validate_matrix_mult_dims(left, right): """Validates the dimensions for the matrix multiplication""" left_len = len(left.domain) right_len = len(right.domain) dim_no_match_err = "Matrix multiplication dimensions do not match" if left_len == 0: raise ValidationError( "Matrix multiplication requires at least 1 domain, left side" " is a scalar" ) if right_len == 0: raise ValidationError( "Matrix multiplication requires at least 1 domain, right side" " is a scalar" ) lr = (left_len, right_len) left_controlled = getattr(left, "controlled_domain", []) right_controlled = getattr(right, "controlled_domain", []) controlled_domain = [*left_controlled, *right_controlled] if lr == (1, 1): # Dot product if not utils.setBaseEqual(left.domain[0], right.domain[0]): raise ValidationError("Dot product requires same domain") sum_domain = left.domain[0] while sum_domain in controlled_domain: sum_domain = next_alias(sum_domain) return [sum_domain], [sum_domain], sum_domain elif lr == (2, 2): # Matrix multiplication if not utils.setBaseEqual(left.domain[1], right.domain[0]): raise ValidationError(dim_no_match_err) left_domain = left.domain[0] right_domain = right.domain[1] if left_domain == right_domain: left_domain = next_alias(left_domain) sum_domain = left.domain[1] while ( sum_domain in [left_domain, right_domain] or sum_domain in controlled_domain ): sum_domain = next_alias(sum_domain) return ( [left_domain, sum_domain], [sum_domain, right_domain], sum_domain, ) elif lr == (1, 2): # Vector matrix, vector 1-prepended if not utils.setBaseEqual(left.domain[0], right.domain[0]): raise ValidationError(dim_no_match_err) if utils.setBaseEqual(right.domain[0], right.domain[1]): sum_domain = right.domain[1] right_domain = right.domain[0] else: sum_domain = right.domain[0] right_domain = right.domain[1] while sum_domain == right.domain[1] or sum_domain in controlled_domain: sum_domain = next_alias(sum_domain) return [sum_domain], [sum_domain, right_domain], sum_domain elif lr == (2, 1): # Matrix vector, ordinary if not utils.setBaseEqual(left.domain[1], right.domain[0]): raise ValidationError(dim_no_match_err) sum_domain = left.domain[1] while left.domain[0] == sum_domain or sum_domain in controlled_domain: sum_domain = next_alias(sum_domain) return [left.domain[0], sum_domain], [sum_domain], sum_domain elif left_len == 1 and right_len > 2: # Vector batched-matrix, vector 1-prepended if not utils.setBaseEqual(left.domain[0], right.domain[-2]): raise ValidationError(dim_no_match_err) sum_domain = left.domain[0] while ( sum_domain in right.domain[:-2] or sum_domain == right.domain[-1] or sum_domain in controlled_domain ): sum_domain = next_alias(sum_domain) return ( [sum_domain], [*right.domain[:-2], sum_domain, right.domain[-1]], sum_domain, ) elif left_len > 2 and right_len == 1: # batched-matrix vector, ordinary if not utils.setBaseEqual(left.domain[-1], right.domain[0]): raise ValidationError(dim_no_match_err) sum_domain = left.domain[-1] while ( sum_domain in left.domain[:-1] or sum_domain in controlled_domain ): sum_domain = next_alias(sum_domain) return ( [*left.domain[:-1], sum_domain], [sum_domain], sum_domain, ) elif left_len >= 2 and right_len >= 2: # batched-matrix batched-matrix if not utils.setBaseEqual(left.domain[-1], right.domain[-2]): raise ValidationError(dim_no_match_err) batch_dim_1 = left.domain[:-2] batch_dim_2 = right.domain[:-2] if len(batch_dim_1) > 0 and len(batch_dim_2) > 0: if len(batch_dim_1) != len(batch_dim_2): raise ValidationError("Batch dimensions do not match") if any([x != y for x, y in zip(batch_dim_1, batch_dim_2)]): raise ValidationError("Batch dimensions do not match") left_domain = left.domain[-2] right_domain = right.domain[-1] if left_domain == right_domain: left_domain = next_alias(left_domain) sum_domain = left.domain[-1] while ( sum_domain in left.domain[:-1] or sum_domain in right.domain[:-2] or sum_domain in [right_domain, left_domain] or sum_domain in controlled_domain ): sum_domain = next_alias(sum_domain) return ( [*left.domain[:-2], left_domain, sum_domain], [*right.domain[:-2], sum_domain, right_domain], sum_domain, ) raise ValidationError( f"Matrix multiplication for left dim: {left_len}," f" right dim: {right_len} not implemented" )