Coverage for gpkit/tests/t_sub.py: 94%

304 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-07 22:15 -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 

13 

14# pylint: disable=invalid-name,attribute-defined-outside-init,unused-variable 

15 

16 

17class TestNomialSubs(unittest.TestCase): 

18 """Test substitution for nomial-family objects""" 

19 

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) 

25 

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) 

37 

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) 

45 

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) 

52 

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]}) 

62 

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) 

67 

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) 

82 

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 

98 

99 

100class TestModelSubs(unittest.TestCase): 

101 """Test substitution for Model objects""" 

102 

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() 

109 

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) 

133 

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")) 

148 

149 def test_persistence(self): 

150 x = gpkit.Variable("x") 

151 y = gpkit.Variable("y") 

152 ymax = gpkit.Variable("y_{max}", 0.1) 

153 

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) 

164 

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) 

176 

177 def test_skipfailures(self): 

178 x = Variable("x") 

179 x_min = Variable("x_{min}", [1, 2]) 

180 

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) 

185 

186 with self.assertRaises(RuntimeWarning): 

187 sol = m.solve(verbosity=0, skipsweepfailures=False) 

188 

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) 

192 

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)) 

227 

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) 

244 

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) 

251 

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)) 

261 

262 # pylint: disable=too-many-locals 

263 def test_model_composition_units(self): 

264 class Above(Model): 

265 """A simple upper bound on x 

266 

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] 

276 

277 class Below(Model): 

278 """A simple lower bound on x 

279 

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] 

289 

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"]) 

317 

318 def test_getkey(self): 

319 class Top(Model): 

320 """Some high level model 

321 

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] 

332 

333 class Sub(Model): 

334 """A simple sub model 

335 

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] 

344 

345 sol = Top().solve(verbosity=0) 

346 self.assertAlmostEqual(sol['cost'], 2) 

347 

348 def test_model_recursion(self): 

349 class Top(Model): 

350 """Some high level model 

351 

352 Upper Unbounded 

353 --------------- 

354 x 

355 

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] 

362 

363 class Sub(Model): 

364 """A simple sub model 

365 

366 Upper Unbounded 

367 --------------- 

368 y 

369 

370 """ 

371 def setup(self): 

372 y = self.y = Variable('y') 

373 self.cost = y 

374 return [y >= 2] 

375 

376 sol = Top().solve(verbosity=0) 

377 self.assertAlmostEqual(sol['cost'], 2) 

378 

379 def test_vector_sub(self): 

380 x = VectorVariable(3, "x") 

381 y = VectorVariable(3, "y") 

382 ymax = VectorVariable(3, "ymax") 

383 

384 with SignomialsEnabled(): 

385 # issue1077 links to a case that failed for SPs only 

386 m = Model(x.prod(), [x + y >= 1, y <= ymax]) 

387 

388 m.substitutions["ymax"] = [0.3, 0.5, 0.8] 

389 m.localsolve(verbosity=0) 

390 

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") 

396 

397 with SignomialsEnabled(): 

398 cnstr = [z + w >= y*x, w <= y] 

399 

400 m = Model(z, cnstr) 

401 m.localsolve(verbosity=0) 

402 self.assertTrue(m.substitutions["y"], "__call__") 

403 

404class TestNomialMapSubs(unittest.TestCase): 

405 "Tests substitutions of nomialmaps" 

406 def test_monomial_sub(self): 

407 z = Variable("z") 

408 w = Variable("w") 

409 

410 with self.assertRaises(ValueError): 

411 z.hmap.sub({z.key: w.key}, varkeys=z.vks) 

412 

413 def test_subinplace_zero(self): 

414 z = Variable("z") 

415 w = Variable("w") 

416 

417 p = 2*w + z*w + 2 

418 

419 self.assertEqual(p.sub({z: -2}), 2) 

420 

421TESTS = [TestNomialSubs, TestModelSubs, TestNomialMapSubs] 

422 

423if __name__ == "__main__": # pragma: no cover 

424 run_tests(TESTS)