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