Coverage for gpkit/tests/t_sub.py: 94%
304 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-05 22:09 -0500
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-05 22:09 -0500
1"""Test substitution capability across gpkit"""
2import pickle
3import unittest
4import numpy as np
5import numpy.testing as npt
6from adce import adnumber, ADV
7import gpkit
8from gpkit import SignomialsEnabled, NamedVariables
9from gpkit import Variable, VectorVariable, Model, Signomial
10from gpkit.small_scripts import mag
11from gpkit.tests.helpers import run_tests
12from gpkit.exceptions import UnboundedGP, DimensionalityError
14# pylint: disable=invalid-name,attribute-defined-outside-init,unused-variable
17class TestNomialSubs(unittest.TestCase):
18 """Test substitution for nomial-family objects"""
20 def test_vectorized_linked(self):
21 class VectorLinked(Model):
22 "simple vectorized link"
23 def setup(self):
24 self.y = y = Variable("y", 1)
26 def vectorlink(c):
27 "linked vector function"
28 if isinstance(c[y], ADV):
29 return np.array(c[y])+adnumber([1, 2, 3])
30 return c[y]+np.array([1, 2, 3])
31 self.x = x = VectorVariable(3, "x", vectorlink)
32 m = VectorLinked()
33 self.assertEqual(m.substitutions[m.x[0].key](m.substitutions), 2)
34 self.assertEqual(m.gp().substitutions[m.x[0].key], 2)
35 self.assertEqual(m.gp().substitutions[m.x[1].key], 3)
36 self.assertEqual(m.gp().substitutions[m.x[2].key], 4)
38 def test_numeric(self):
39 """Basic substitution of numeric value"""
40 x = Variable("x")
41 p = x**2
42 self.assertEqual(p.sub({x: 3}), 9)
43 self.assertEqual(p.sub({x.key: 3}), 9)
44 self.assertEqual(p.sub({"x": 3}), 9)
46 def test_dimensionless_units(self):
47 x = Variable('x', 3, 'ft')
48 y = Variable('y', 1, 'm')
49 if x.units is not None:
50 # units are enabled
51 self.assertAlmostEqual((x/y).value, 0.9144)
53 def test_vector(self):
54 x = Variable("x")
55 y = Variable("y")
56 z = VectorVariable(2, "z")
57 p = x*y*z
58 self.assertTrue(all(p.sub({x: 1, "y": 2}) == 2*z))
59 self.assertTrue(all(p.sub({x: 1, y: 2, "z": [1, 2]}) ==
60 z.sub({z: [2, 4]})))
61 self.assertRaises(ValueError, z.sub, {z: [1, 2, 3]})
63 xvec = VectorVariable(3, "x", "m")
64 xs = xvec[:2].sum()
65 for x_ in ["x", xvec]:
66 self.assertAlmostEqual(mag(xs.sub({x_: [1, 2, 3]}).c), 3.0)
68 def test_variable(self):
69 """Test special single-argument substitution for Variable"""
70 x = Variable('x')
71 y = Variable('y')
72 m = x*y**2
73 self.assertEqual(x.sub(3), 3)
74 # make sure x was not mutated
75 self.assertEqual(x, Variable('x'))
76 self.assertNotEqual(x.sub(3), Variable('x'))
77 # also make sure the old way works
78 self.assertEqual(x.sub({x: 3}), 3)
79 # and for vectors
80 xvec = VectorVariable(3, 'x')
81 self.assertEqual(xvec[1].sub(3), 3)
83 def test_signomial(self):
84 """Test Signomial substitution"""
85 D = Variable('D', units="N")
86 x = Variable('x', units="N")
87 y = Variable('y', units="N")
88 a = Variable('a')
89 with SignomialsEnabled():
90 sc = a*x + (1 - a)*y - D
91 subbed = sc.sub({a: 0.1})
92 self.assertEqual(subbed, 0.1*x + 0.9*y - D)
93 self.assertTrue(isinstance(subbed, Signomial))
94 subbed = sc.sub({a: 2.0})
95 self.assertTrue(isinstance(subbed, Signomial))
96 self.assertEqual(subbed, 2*x - y - D)
97 _ = a.sub({a: -1}).value # fix monomial assumptions
100class TestModelSubs(unittest.TestCase):
101 """Test substitution for Model objects"""
103 def test_bad_gp_sub(self):
104 x = Variable("x")
105 y = Variable("y")
106 m = Model(x, [y >= 1], {y: x})
107 with self.assertRaises(TypeError):
108 m.solve()
110 def test_quantity_sub(self):
111 if gpkit.units:
112 x = Variable("x", 1, "cm")
113 y = Variable("y", 1)
114 # pylint: disable=no-member # for .c below
115 self.assertEqual(x.sub({x: 1*gpkit.units.m}).c.magnitude, 100)
116 # NOTE: uncomment the below if requiring Quantity substitutions
117 # self.assertRaises(ValueError, x.sub, x, 1)
118 self.assertRaises(DimensionalityError, x.sub, {x: 1*gpkit.ureg.N})
119 self.assertRaises(DimensionalityError, y.sub, {y: 1*gpkit.ureg.N})
120 v = gpkit.VectorVariable(3, "v", "cm")
121 subbed = v.sub({v: [1, 2, 3]*gpkit.ureg.m})
122 self.assertEqual([z.c.magnitude for z in subbed], [100, 200, 300])
123 v = VectorVariable(1, "v", "km")
124 v_min = VectorVariable(1, "v_min", "km")
125 m = Model(v.prod(), [v >= v_min],
126 {v_min: [2*gpkit.units("nmi")]})
127 cost = m.solve(verbosity=0)["cost"]
128 self.assertAlmostEqual(cost/3.704, 1.0)
129 m = Model(v.prod(), [v >= v_min],
130 {v_min: np.array([2])*gpkit.units("nmi")})
131 cost = m.solve(verbosity=0)["cost"]
132 self.assertAlmostEqual(cost/3.704, 1.0)
134 def test_phantoms(self):
135 x = Variable("x")
136 x_ = Variable("x", 1, lineage=[("test", 0)])
137 xv = VectorVariable(2, "x", [1, 1], lineage=[("vec", 0)])
138 m = Model(x, [x >= x_, x_ == xv.prod()])
139 m.solve(verbosity=0)
140 with self.assertRaises(ValueError):
141 _ = m.substitutions["x"]
142 with self.assertRaises(KeyError):
143 _ = m.substitutions["y"]
144 with self.assertRaises(ValueError):
145 _ = m["x"]
146 self.assertIn(x, m.variables_byname("x"))
147 self.assertIn(x_, m.variables_byname("x"))
149 def test_persistence(self):
150 x = gpkit.Variable("x")
151 y = gpkit.Variable("y")
152 ymax = gpkit.Variable("y_{max}", 0.1)
154 with gpkit.SignomialsEnabled():
155 m = gpkit.Model(x, [x >= 1-y, y <= ymax])
156 m.substitutions[ymax] = 0.2
157 self.assertAlmostEqual(m.localsolve(verbosity=0)["cost"], 0.8, 3)
158 m = gpkit.Model(x, [x >= 1-y, y <= ymax])
159 with self.assertRaises(UnboundedGP): # from unbounded ymax
160 m.localsolve(verbosity=0)
161 m = gpkit.Model(x, [x >= 1-y, y <= ymax])
162 m.substitutions[ymax] = 0.1
163 self.assertAlmostEqual(m.localsolve(verbosity=0)["cost"], 0.9, 3)
165 def test_united_sub_sweep(self):
166 A = Variable("A", "USD")
167 h = Variable("h", "USD/count")
168 Q = Variable("Q", "count")
169 Y = Variable("Y", "USD")
170 m = Model(Y, [Y >= h*Q + A/Q])
171 m.substitutions.update({A: 500*gpkit.units("USD"),
172 h: 35*gpkit.units("USD"),
173 Q: ("sweep", [50, 100, 500])})
174 firstcost = m.solve(verbosity=0)["cost"][0]
175 self.assertAlmostEqual(1760/firstcost, 1, 5)
177 def test_skipfailures(self):
178 x = Variable("x")
179 x_min = Variable("x_{min}", [1, 2])
181 m = Model(x, [x <= 1, x >= x_min])
182 sol = m.solve(verbosity=0, skipsweepfailures=True)
183 sol.table()
184 self.assertEqual(len(sol), 1)
186 with self.assertRaises(RuntimeWarning):
187 sol = m.solve(verbosity=0, skipsweepfailures=False)
189 m.substitutions[x_min][1][0] = 5 # so no sweeps solve
190 with self.assertRaises(RuntimeWarning):
191 sol = m.solve(verbosity=0, skipsweepfailures=True)
193 def test_vector_sweep(self):
194 """Test sweep involving VectorVariables"""
195 x = Variable("x")
196 x_min = Variable("x_min", 1)
197 y = VectorVariable(2, "y")
198 m = Model(x, [x >= y.prod()])
199 m.substitutions.update({y: ('sweep', [[2, 3], [5, 7], [9, 11]])})
200 a = m.solve(verbosity=0)["cost"]
201 b = [6, 15, 27, 14, 35, 63, 22, 55, 99]
202 self.assertTrue(all(abs(a-b)/(a+b) < 1e-7))
203 x_min = Variable("x_min", 1) # constant to check array indexing
204 m = Model(x, [x >= y.prod(), x >= x_min])
205 m.substitutions.update({y: ('sweep', [[2, 3], [5, 7, 11]])})
206 sol = m.solve(verbosity=0)
207 a = sol["cost"]
208 b = [10, 15, 14, 21, 22, 33]
209 self.assertTrue(all(abs(a-b)/(a+b) < 1e-7))
210 self.assertEqual(sol["constants"][x_min], 1)
211 for i, bi in enumerate(b):
212 self.assertEqual(sol.atindex(i)["constants"][x_min], 1)
213 ai = m.solution.atindex(i)["cost"]
214 self.assertTrue(abs(ai-bi)/(ai+bi) < 1e-7)
215 m = Model(x, [x >= y.prod()])
216 m.substitutions.update({y: ('sweep', [[2, 3, 9], [5, 7, 11]])})
217 self.assertRaises(ValueError, m.solve, verbosity=0)
218 m.substitutions.update({y: [2, ("sweep", [3, 5])]})
219 a = m.solve(verbosity=0)["cost"]
220 b = [6, 10]
221 self.assertTrue(all(abs(a-b)/(a+b) < 1e-7))
222 # create a numpy float array, then insert a sweep element
223 m.substitutions.update({y: [2, 3]})
224 m.substitutions.update({y[1]: ("sweep", [3, 5])})
225 a = m.solve(verbosity=0)["cost"]
226 self.assertTrue(all(abs(a-b)/(a+b) < 1e-7))
228 def test_calcconst(self):
229 x = Variable("x", "hours")
230 t_day = Variable("t_{day}", 12, "hours")
231 t_night = Variable("t_{night}",
232 lambda c: 1*gpkit.ureg.day - c(t_day), "hours")
233 _ = pickle.dumps(t_night)
234 m = Model(x, [x >= t_day, x >= t_night])
235 sol = m.solve(verbosity=0)
236 self.assertAlmostEqual(sol(t_night)/gpkit.ureg.hours, 12)
237 m.substitutions.update({t_day: ("sweep", [6, 8, 9, 13])})
238 sol = m.solve(verbosity=0)
239 npt.assert_allclose(sol["sensitivities"]["variables"][t_day],
240 [-1/3, -0.5, -0.6, +1], 1e-3)
241 self.assertEqual(len(sol["cost"]), 4)
242 npt.assert_allclose([float(l) for l in
243 (sol(t_day) + sol(t_night))/gpkit.ureg.hours], 24)
245 def test_vector_init(self):
246 N = 6
247 Weight = 50000
248 xi_dist = 6*Weight/float(N)*(
249 (np.array(range(1, N+1)) - .5/float(N))/float(N) -
250 (np.array(range(1, N+1)) - .5/float(N))**2/float(N)**2)
252 xi = VectorVariable(N, "xi", xi_dist, "N", "Constant Thrust per Bin")
253 P = Variable("P", "N", "Total Power")
254 phys_constraints = [P >= xi.sum()]
255 objective = P
256 eqns = phys_constraints
257 m = Model(objective, eqns)
258 sol = m.solve(verbosity=0)
259 a, b = sol("xi"), xi_dist*gpkit.ureg.N
260 self.assertTrue(all(abs(a-b)/(a+b) < 1e-7))
262 # pylint: disable=too-many-locals
263 def test_model_composition_units(self):
264 class Above(Model):
265 """A simple upper bound on x
267 Lower Unbounded
268 ---------------
269 x
270 """
271 def setup(self):
272 x = self.x = Variable("x", "ft")
273 x_max = Variable("x_{max}", 1, "yard")
274 self.cost = 1/x
275 return [x <= x_max]
277 class Below(Model):
278 """A simple lower bound on x
280 Upper Unbounded
281 ---------------
282 x
283 """
284 def setup(self):
285 x = self.x = Variable("x", "m")
286 x_min = Variable("x_{min}", 1, "cm")
287 self.cost = x
288 return [x >= x_min]
290 a, b = Above(), Below()
291 concatm = Model(a.cost*b.cost, [a, b])
292 concat_cost = concatm.solve(verbosity=0)["cost"]
293 almostequal = self.assertAlmostEqual
294 yard, cm = gpkit.ureg("yard"), gpkit.ureg("cm")
295 ft, meter = gpkit.ureg("ft"), gpkit.ureg("m")
296 if not isinstance(a["x"].key.units, str):
297 almostequal(a.solve(verbosity=0)["cost"], ft/yard, 5)
298 almostequal(b.solve(verbosity=0)["cost"], cm/meter, 5)
299 almostequal(cm/yard, concat_cost, 5)
300 NamedVariables.reset_modelnumbers()
301 a1, b1 = Above(), Below()
302 self.assertEqual(a1["x"].key.lineage, (("Above", 0),))
303 m = Model(a1["x"], [a1, b1, b1["x"] == a1["x"]])
304 sol = m.solve(verbosity=0)
305 if not isinstance(a1["x"].key.units, str):
306 almostequal(sol["cost"], cm/ft, 5)
307 a1, b1 = Above(), Below()
308 self.assertEqual(a1["x"].key.lineage, (("Above", 1),))
309 m = Model(b1["x"], [a1, b1, b1["x"] == a1["x"]])
310 sol = m.solve(verbosity=0)
311 if not isinstance(b1["x"].key.units, str):
312 almostequal(sol["cost"], cm/meter, 5)
313 self.assertIn(a1["x"], sol["variables"])
314 self.assertIn(b1["x"], sol["variables"])
315 self.assertNotIn(a["x"], sol["variables"])
316 self.assertNotIn(b["x"], sol["variables"])
318 def test_getkey(self):
319 class Top(Model):
320 """Some high level model
322 Upper Unbounded
323 ---------------
324 y
325 """
326 def setup(self):
327 y = self.y = Variable('y')
328 s = Sub()
329 sy = s["y"]
330 self.cost = y
331 return [s, y >= sy, sy >= 1]
333 class Sub(Model):
334 """A simple sub model
336 Upper Unbounded
337 ---------------
338 y
339 """
340 def setup(self):
341 y = self.y = Variable('y')
342 self.cost = y
343 return [y >= 2]
345 sol = Top().solve(verbosity=0)
346 self.assertAlmostEqual(sol['cost'], 2)
348 def test_model_recursion(self):
349 class Top(Model):
350 """Some high level model
352 Upper Unbounded
353 ---------------
354 x
356 """
357 def setup(self):
358 sub = Sub()
359 x = self.x = Variable("x")
360 self.cost = x
361 return sub, [x >= sub["y"], sub["y"] >= 1]
363 class Sub(Model):
364 """A simple sub model
366 Upper Unbounded
367 ---------------
368 y
370 """
371 def setup(self):
372 y = self.y = Variable('y')
373 self.cost = y
374 return [y >= 2]
376 sol = Top().solve(verbosity=0)
377 self.assertAlmostEqual(sol['cost'], 2)
379 def test_vector_sub(self):
380 x = VectorVariable(3, "x")
381 y = VectorVariable(3, "y")
382 ymax = VectorVariable(3, "ymax")
384 with SignomialsEnabled():
385 # issue1077 links to a case that failed for SPs only
386 m = Model(x.prod(), [x + y >= 1, y <= ymax])
388 m.substitutions["ymax"] = [0.3, 0.5, 0.8]
389 m.localsolve(verbosity=0)
391 def test_spsubs(self):
392 x = Variable("x", 5)
393 y = Variable("y", lambda c: 2*c[x])
394 z = Variable("z")
395 w = Variable("w")
397 with SignomialsEnabled():
398 cnstr = [z + w >= y*x, w <= y]
400 m = Model(z, cnstr)
401 m.localsolve(verbosity=0)
402 self.assertTrue(m.substitutions["y"], "__call__")
404class TestNomialMapSubs(unittest.TestCase):
405 "Tests substitutions of nomialmaps"
406 def test_monomial_sub(self):
407 z = Variable("z")
408 w = Variable("w")
410 with self.assertRaises(ValueError):
411 z.hmap.sub({z.key: w.key}, varkeys=z.vks)
413 def test_subinplace_zero(self):
414 z = Variable("z")
415 w = Variable("w")
417 p = 2*w + z*w + 2
419 self.assertEqual(p.sub({z: -2}), 2)
421TESTS = [TestNomialSubs, TestModelSubs, TestNomialMapSubs]
423if __name__ == "__main__": # pragma: no cover
424 run_tests(TESTS)