Coverage for gpkit/nomials/map.py: 100%
122 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-07 22:15 -0500
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-07 22:15 -0500
1"Implements the NomialMap class"
2from collections import defaultdict
3import numpy as np
4from .. import units
5from ..exceptions import DimensionalityError
6from ..small_classes import HashVector, Strings, EMPTY_HV
7from ..units import qty
8from .substitution import parse_subs
10DIMLESS_QUANTITY = qty("dimensionless")
13class NomialMap(HashVector):
14 """Class for efficent algebraic represention of a nomial
16 A NomialMap is a mapping between hashvectors representing exponents
17 and their coefficients in a posynomial.
19 For example, {{x : 1}: 2.0, {y: 1}: 3.0} represents 2*x + 3*y, where
20 x and y are VarKey objects.
21 """
22 units = None
23 expmap = None # used for monomial-mapping postsubstitution; see .mmap()
24 csmap = None # used for monomial-mapping postsubstitution; see .mmap()
26 def copy(self):
27 "Return a copy of this"
28 return self.__class__(self)
30 def units_of_product(self, thing, thing2=None):
31 "Sets units to those of `thing*thing2`. Ugly optimized code."
32 if thing is None and thing2 is None:
33 self.units = None
34 elif hasattr(thing, "units"):
35 if hasattr(thing2, "units"):
36 self.units, dimless_convert = units.of_product(thing, thing2)
37 if dimless_convert:
38 for key in self:
39 self[key] *= dimless_convert
40 else:
41 self.units = qty(thing.units)
42 elif hasattr(thing2, "units"):
43 self.units = qty(thing2.units)
44 elif thing2 is None and isinstance(thing, Strings):
45 self.units = qty(thing)
46 else:
47 self.units = None
49 def to(self, to_units):
50 "Returns a new NomialMap of the given units"
51 sunits = self.units or DIMLESS_QUANTITY
52 nm = self * sunits.to(to_units).magnitude # note that * creates a copy
53 nm.units_of_product(to_units) # pylint: disable=no-member
54 return nm
56 def __add__(self, other):
57 "Adds NomialMaps together"
58 if self.units != other.units:
59 try:
60 other *= float(other.units/self.units)
61 except (TypeError, AttributeError) as exc: # one of those is None
62 raise DimensionalityError(self.units, other.units) from exc
63 hmap = HashVector.__add__(self, other)
64 hmap.units = self.units
65 return hmap
67 def diff(self, varkey):
68 "Differentiates a NomialMap with respect to a varkey"
69 out = NomialMap()
70 out.units_of_product(self.units,
71 1/varkey.units if varkey.units else None)
72 for exp in self:
73 if varkey in exp:
74 x = exp[varkey]
75 c = self[exp] * x
76 exp = exp.copy()
77 if x == 1:
78 exp.hashvalue ^= hash((varkey, 1))
79 del exp[varkey]
80 else:
81 exp.hashvalue ^= hash((varkey, x)) ^ hash((varkey, x-1))
82 exp[varkey] = x-1
83 out[exp] = c
84 return out
86 def sub(self, substitutions, varkeys, parsedsubs=False):
87 """Applies substitutions to a NomialMap
89 Parameters
90 ----------
91 substitutions : (dict-like)
92 list of substitutions to perform
94 varkeys : (set-like)
95 varkeys that are present in self
96 (required argument so as to require efficient code)
98 parsedsubs : bool
99 flag if the substitutions have already been parsed
100 to contain only keys in varkeys
102 """
103 # pylint: disable=too-many-locals, too-many-branches
104 if parsedsubs or not substitutions:
105 fixed = substitutions
106 else:
107 fixed, _, _ = parse_subs(varkeys, substitutions)
109 if not fixed:
110 if not self.expmap:
111 self.expmap, self.csmap = {exp: exp for exp in self}, {}
112 return self
114 cp = NomialMap()
115 cp.units = self.units
116 # csmap is modified during substitution, but keeps the same exps
117 cp.expmap, cp.csmap = {}, self.copy()
118 varlocs = defaultdict(set)
119 for exp, c in self.items():
120 new_exp = exp.copy()
121 cp.expmap[exp] = new_exp # cp modifies exps, so it needs new ones
122 cp[new_exp] = c
123 for vk in new_exp:
124 if vk in fixed:
125 varlocs[vk].add((exp, new_exp))
127 squished = set()
128 for vk in varlocs:
129 exps, cval = varlocs[vk], fixed[vk]
130 if hasattr(cval, "hmap"):
131 if cval.hmap is None or any(cval.hmap.keys()):
132 raise ValueError("Monomial substitutions are not"
133 " supported.")
134 cval, = cval.hmap.to(vk.units or DIMLESS_QUANTITY).values()
135 elif hasattr(cval, "to"):
136 cval = cval.to(vk.units or DIMLESS_QUANTITY).magnitude
137 for o_exp, exp in exps:
138 subinplace(cp, exp, o_exp, vk, cval, squished)
139 return cp
141 def mmap(self, orig):
142 """Maps substituted monomials back to the original nomial
144 self.expmap is the map from pre- to post-substitution exponents, and
145 takes the form {original_exp: new_exp}
147 self.csmap is the map from pre-substitution exponents to coefficients.
149 m_from_ms is of the form {new_exp: [old_exps, ]}
151 pmap is of the form [{orig_idx1: fraction1, orig_idx2: fraction2, }, ]
152 where at the index corresponding to each new_exp is a dictionary
153 mapping the indices corresponding to the old exps to their
154 fraction of the post-substitution coefficient
155 """
156 pmap = [{} for _ in self]
157 origexps = list(orig.keys())
158 selfexps = list(self.keys())
159 for orig_exp, self_exp in self.expmap.items():
160 if self_exp not in self: # can occur in tautological constraints
161 continue # after substitution
162 fraction = self.csmap.get(orig_exp, orig[orig_exp])/self[self_exp]
163 orig_idx = origexps.index(orig_exp)
164 pmap[selfexps.index(self_exp)][orig_idx] = fraction
165 return pmap
168# pylint: disable=invalid-name, too-many-arguments
169def subinplace(cp, exp, o_exp, vk, cval, squished):
170 "Modifies cp by substituing cval/expval for vk in exp"
171 x = exp[vk]
172 powval = float(cval)**x if cval != 0 or x >= 0 else np.sign(cval)*np.inf
173 cp.csmap[o_exp] *= powval
174 if exp in cp:
175 c = cp.pop(exp)
176 exp.hashvalue ^= hash((vk, x)) # remove (key, value) from hashvalue
177 del exp[vk]
178 value = powval * c
179 if exp in cp:
180 squished.add(exp.copy())
181 currentvalue = cp[exp]
182 if value != -currentvalue:
183 cp[exp] += value
184 else:
185 del cp[exp] # remove zeros created during substitution
186 elif value:
187 cp[exp] = value
188 if not cp: # make sure it's never an empty hmap
189 cp[EMPTY_HV] = 0.0
190 elif exp in squished:
191 exp.hashvalue ^= hash((vk, x)) # remove (key, value) from hashvalue
192 del exp[vk]