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

304 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-03 16:47 -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 self.assertEqual(x.sub({x: 1*gpkit.units.m}).c.magnitude, 100) 

115 # NOTE: uncomment the below if requiring Quantity substitutions 

116 # self.assertRaises(ValueError, x.sub, x, 1) 

117 self.assertRaises(DimensionalityError, x.sub, {x: 1*gpkit.ureg.N}) 

118 self.assertRaises(DimensionalityError, y.sub, {y: 1*gpkit.ureg.N}) 

119 v = gpkit.VectorVariable(3, "v", "cm") 

120 subbed = v.sub({v: [1, 2, 3]*gpkit.ureg.m}) 

121 self.assertEqual([z.c.magnitude for z in subbed], [100, 200, 300]) 

122 v = VectorVariable(1, "v", "km") 

123 v_min = VectorVariable(1, "v_min", "km") 

124 m = Model(v.prod(), [v >= v_min], 

125 {v_min: [2*gpkit.units("nmi")]}) 

126 cost = m.solve(verbosity=0)["cost"] 

127 self.assertAlmostEqual(cost/3.704, 1.0) 

128 m = Model(v.prod(), [v >= v_min], 

129 {v_min: np.array([2])*gpkit.units("nmi")}) 

130 cost = m.solve(verbosity=0)["cost"] 

131 self.assertAlmostEqual(cost/3.704, 1.0) 

132 

133 def test_phantoms(self): 

134 x = Variable("x") 

135 x_ = Variable("x", 1, lineage=[("test", 0)]) 

136 xv = VectorVariable(2, "x", [1, 1], lineage=[("vec", 0)]) 

137 m = Model(x, [x >= x_, x_ == xv.prod()]) 

138 m.solve(verbosity=0) 

139 with self.assertRaises(ValueError): 

140 _ = m.substitutions["x"] 

141 with self.assertRaises(KeyError): 

142 _ = m.substitutions["y"] 

143 with self.assertRaises(ValueError): 

144 _ = m["x"] 

145 self.assertIn(x, m.variables_byname("x")) 

146 self.assertIn(x_, m.variables_byname("x")) 

147 

148 def test_persistence(self): 

149 x = gpkit.Variable("x") 

150 y = gpkit.Variable("y") 

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

152 

153 with gpkit.SignomialsEnabled(): 

154 m = gpkit.Model(x, [x >= 1-y, y <= ymax]) 

155 m.substitutions[ymax] = 0.2 

156 self.assertAlmostEqual(m.localsolve(verbosity=0)["cost"], 0.8, 3) 

157 m = gpkit.Model(x, [x >= 1-y, y <= ymax]) 

158 with self.assertRaises(UnboundedGP): # from unbounded ymax 

159 m.localsolve(verbosity=0) 

160 m = gpkit.Model(x, [x >= 1-y, y <= ymax]) 

161 m.substitutions[ymax] = 0.1 

162 self.assertAlmostEqual(m.localsolve(verbosity=0)["cost"], 0.9, 3) 

163 

164 def test_united_sub_sweep(self): 

165 A = Variable("A", "USD") 

166 h = Variable("h", "USD/count") 

167 Q = Variable("Q", "count") 

168 Y = Variable("Y", "USD") 

169 m = Model(Y, [Y >= h*Q + A/Q]) 

170 m.substitutions.update({A: 500*gpkit.units("USD"), 

171 h: 35*gpkit.units("USD"), 

172 Q: ("sweep", [50, 100, 500])}) 

173 firstcost = m.solve(verbosity=0)["cost"][0] 

174 self.assertAlmostEqual(1760/firstcost, 1, 5) 

175 

176 def test_skipfailures(self): 

177 x = Variable("x") 

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

179 

180 m = Model(x, [x <= 1, x >= x_min]) 

181 sol = m.solve(verbosity=0, skipsweepfailures=True) 

182 sol.table() 

183 self.assertEqual(len(sol), 1) 

184 

185 with self.assertRaises(RuntimeWarning): 

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

187 

188 m.substitutions[x_min][1][0] = 5 # so no sweeps solve 

189 with self.assertRaises(RuntimeWarning): 

190 sol = m.solve(verbosity=0, skipsweepfailures=True) 

191 

192 def test_vector_sweep(self): 

193 """Test sweep involving VectorVariables""" 

194 x = Variable("x") 

195 x_min = Variable("x_min", 1) 

196 y = VectorVariable(2, "y") 

197 m = Model(x, [x >= y.prod()]) 

198 m.substitutions.update({y: ('sweep', [[2, 3], [5, 7], [9, 11]])}) 

199 a = m.solve(verbosity=0)["cost"] 

200 b = [6, 15, 27, 14, 35, 63, 22, 55, 99] 

201 self.assertTrue(all(abs(a-b)/(a+b) < 1e-7)) 

202 x_min = Variable("x_min", 1) # constant to check array indexing 

203 m = Model(x, [x >= y.prod(), x >= x_min]) 

204 m.substitutions.update({y: ('sweep', [[2, 3], [5, 7, 11]])}) 

205 sol = m.solve(verbosity=0) 

206 a = sol["cost"] 

207 b = [10, 15, 14, 21, 22, 33] 

208 self.assertTrue(all(abs(a-b)/(a+b) < 1e-7)) 

209 self.assertEqual(sol["constants"][x_min], 1) 

210 for i, bi in enumerate(b): 

211 self.assertEqual(sol.atindex(i)["constants"][x_min], 1) 

212 ai = m.solution.atindex(i)["cost"] 

213 self.assertTrue(abs(ai-bi)/(ai+bi) < 1e-7) 

214 m = Model(x, [x >= y.prod()]) 

215 m.substitutions.update({y: ('sweep', [[2, 3, 9], [5, 7, 11]])}) 

216 self.assertRaises(ValueError, m.solve, verbosity=0) 

217 m.substitutions.update({y: [2, ("sweep", [3, 5])]}) 

218 a = m.solve(verbosity=0)["cost"] 

219 b = [6, 10] 

220 self.assertTrue(all(abs(a-b)/(a+b) < 1e-7)) 

221 # create a numpy float array, then insert a sweep element 

222 m.substitutions.update({y: [2, 3]}) 

223 m.substitutions.update({y[1]: ("sweep", [3, 5])}) 

224 a = m.solve(verbosity=0)["cost"] 

225 self.assertTrue(all(abs(a-b)/(a+b) < 1e-7)) 

226 

227 def test_calcconst(self): 

228 x = Variable("x", "hours") 

229 t_day = Variable("t_{day}", 12, "hours") 

230 t_night = Variable("t_{night}", 

231 lambda c: 1*gpkit.ureg.day - c(t_day), "hours") 

232 _ = pickle.dumps(t_night) 

233 m = Model(x, [x >= t_day, x >= t_night]) 

234 sol = m.solve(verbosity=0) 

235 self.assertAlmostEqual(sol(t_night)/gpkit.ureg.hours, 12) 

236 m.substitutions.update({t_day: ("sweep", [6, 8, 9, 13])}) 

237 sol = m.solve(verbosity=0) 

238 npt.assert_allclose(sol["sensitivities"]["variables"][t_day], 

239 [-1/3, -0.5, -0.6, +1], 1e-3) 

240 self.assertEqual(len(sol["cost"]), 4) 

241 npt.assert_allclose([float(l) for l in 

242 (sol(t_day) + sol(t_night))/gpkit.ureg.hours], 24) 

243 

244 def test_vector_init(self): 

245 N = 6 

246 Weight = 50000 

247 xi_dist = 6*Weight/float(N)*( 

248 (np.array(range(1, N+1)) - .5/float(N))/float(N) - 

249 (np.array(range(1, N+1)) - .5/float(N))**2/float(N)**2) 

250 

251 xi = VectorVariable(N, "xi", xi_dist, "N", "Constant Thrust per Bin") 

252 P = Variable("P", "N", "Total Power") 

253 phys_constraints = [P >= xi.sum()] 

254 objective = P 

255 eqns = phys_constraints 

256 m = Model(objective, eqns) 

257 sol = m.solve(verbosity=0) 

258 a, b = sol("xi"), xi_dist*gpkit.ureg.N 

259 self.assertTrue(all(abs(a-b)/(a+b) < 1e-7)) 

260 

261 # pylint: disable=too-many-locals 

262 def test_model_composition_units(self): 

263 class Above(Model): 

264 """A simple upper bound on x 

265 

266 Lower Unbounded 

267 --------------- 

268 x 

269 """ 

270 def setup(self): 

271 x = self.x = Variable("x", "ft") 

272 x_max = Variable("x_{max}", 1, "yard") 

273 self.cost = 1/x 

274 return [x <= x_max] 

275 

276 class Below(Model): 

277 """A simple lower bound on x 

278 

279 Upper Unbounded 

280 --------------- 

281 x 

282 """ 

283 def setup(self): 

284 x = self.x = Variable("x", "m") 

285 x_min = Variable("x_{min}", 1, "cm") 

286 self.cost = x 

287 return [x >= x_min] 

288 

289 a, b = Above(), Below() 

290 concatm = Model(a.cost*b.cost, [a, b]) 

291 concat_cost = concatm.solve(verbosity=0)["cost"] 

292 almostequal = self.assertAlmostEqual 

293 yard, cm = gpkit.ureg("yard"), gpkit.ureg("cm") 

294 ft, meter = gpkit.ureg("ft"), gpkit.ureg("m") 

295 if not isinstance(a["x"].key.units, str): 

296 almostequal(a.solve(verbosity=0)["cost"], ft/yard, 5) 

297 almostequal(b.solve(verbosity=0)["cost"], cm/meter, 5) 

298 almostequal(cm/yard, concat_cost, 5) 

299 NamedVariables.reset_modelnumbers() 

300 a1, b1 = Above(), Below() 

301 self.assertEqual(a1["x"].key.lineage, (("Above", 0),)) 

302 m = Model(a1["x"], [a1, b1, b1["x"] == a1["x"]]) 

303 sol = m.solve(verbosity=0) 

304 if not isinstance(a1["x"].key.units, str): 

305 almostequal(sol["cost"], cm/ft, 5) 

306 a1, b1 = Above(), Below() 

307 self.assertEqual(a1["x"].key.lineage, (("Above", 1),)) 

308 m = Model(b1["x"], [a1, b1, b1["x"] == a1["x"]]) 

309 sol = m.solve(verbosity=0) 

310 if not isinstance(b1["x"].key.units, str): 

311 almostequal(sol["cost"], cm/meter, 5) 

312 self.assertIn(a1["x"], sol["variables"]) 

313 self.assertIn(b1["x"], sol["variables"]) 

314 self.assertNotIn(a["x"], sol["variables"]) 

315 self.assertNotIn(b["x"], sol["variables"]) 

316 

317 def test_getkey(self): 

318 class Top(Model): 

319 """Some high level model 

320 

321 Upper Unbounded 

322 --------------- 

323 y 

324 """ 

325 def setup(self): 

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

327 s = Sub() 

328 sy = s["y"] 

329 self.cost = y 

330 return [s, y >= sy, sy >= 1] 

331 

332 class Sub(Model): 

333 """A simple sub model 

334 

335 Upper Unbounded 

336 --------------- 

337 y 

338 """ 

339 def setup(self): 

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

341 self.cost = y 

342 return [y >= 2] 

343 

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

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

346 

347 def test_model_recursion(self): 

348 class Top(Model): 

349 """Some high level model 

350 

351 Upper Unbounded 

352 --------------- 

353 x 

354 

355 """ 

356 def setup(self): 

357 sub = Sub() 

358 x = self.x = Variable("x") 

359 self.cost = x 

360 return sub, [x >= sub["y"], sub["y"] >= 1] 

361 

362 class Sub(Model): 

363 """A simple sub model 

364 

365 Upper Unbounded 

366 --------------- 

367 y 

368 

369 """ 

370 def setup(self): 

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

372 self.cost = y 

373 return [y >= 2] 

374 

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

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

377 

378 def test_vector_sub(self): 

379 x = VectorVariable(3, "x") 

380 y = VectorVariable(3, "y") 

381 ymax = VectorVariable(3, "ymax") 

382 

383 with SignomialsEnabled(): 

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

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

386 

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

388 m.localsolve(verbosity=0) 

389 

390 def test_spsubs(self): 

391 x = Variable("x", 5) 

392 y = Variable("y", lambda c: 2*c[x]) 

393 z = Variable("z") 

394 w = Variable("w") 

395 

396 with SignomialsEnabled(): 

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

398 

399 m = Model(z, cnstr) 

400 m.localsolve(verbosity=0) 

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

402 

403class TestNomialMapSubs(unittest.TestCase): 

404 "Tests substitutions of nomialmaps" 

405 def test_monomial_sub(self): 

406 z = Variable("z") 

407 w = Variable("w") 

408 

409 with self.assertRaises(ValueError): 

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

411 

412 def test_subinplace_zero(self): 

413 z = Variable("z") 

414 w = Variable("w") 

415 

416 p = 2*w + z*w + 2 

417 

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

419 

420TESTS = [TestNomialSubs, TestModelSubs, TestNomialMapSubs] 

421 

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

423 run_tests(TESTS)