Coverage for mlos_bench/mlos_bench/tests/tunables/tunable_definition_test.py: 100%

146 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-05 00:36 +0000

1# 

2# Copyright (c) Microsoft Corporation. 

3# Licensed under the MIT License. 

4# 

5""" 

6Unit tests for checking tunable definition rules. 

7""" 

8 

9import json5 as json 

10import pytest 

11 

12from mlos_bench.tunables.tunable import Tunable, TunableValueTypeName 

13 

14 

15def test_tunable_name() -> None: 

16 """ 

17 Check that tunable name is valid. 

18 """ 

19 with pytest.raises(ValueError): 

20 # ! characters are currently disallowed in tunable names 

21 Tunable(name='test!tunable', config={"type": "float", "range": [0, 1], "default": 0}) 

22 

23 

24def test_categorical_required_params() -> None: 

25 """ 

26 Check that required parameters are present for categorical tunables. 

27 """ 

28 json_config = """ 

29 { 

30 "type": "categorical", 

31 "values_missing": ["foo", "bar", "baz"], 

32 "default": "foo" 

33 } 

34 """ 

35 config = json.loads(json_config) 

36 with pytest.raises(ValueError): 

37 Tunable(name='test', config=config) 

38 

39 

40def test_categorical_weights() -> None: 

41 """ 

42 Instantiate a categorical tunable with weights. 

43 """ 

44 json_config = """ 

45 { 

46 "type": "categorical", 

47 "values": ["foo", "bar", "baz"], 

48 "values_weights": [25, 25, 50], 

49 "default": "foo" 

50 } 

51 """ 

52 config = json.loads(json_config) 

53 tunable = Tunable(name='test', config=config) 

54 assert tunable.weights == [25, 25, 50] 

55 

56 

57def test_categorical_weights_wrong_count() -> None: 

58 """ 

59 Try to instantiate a categorical tunable with incorrect number of weights. 

60 """ 

61 json_config = """ 

62 { 

63 "type": "categorical", 

64 "values": ["foo", "bar", "baz"], 

65 "values_weights": [50, 50], 

66 "default": "foo" 

67 } 

68 """ 

69 config = json.loads(json_config) 

70 with pytest.raises(ValueError): 

71 Tunable(name='test', config=config) 

72 

73 

74def test_categorical_weights_wrong_values() -> None: 

75 """ 

76 Try to instantiate a categorical tunable with invalid weights. 

77 """ 

78 json_config = """ 

79 { 

80 "type": "categorical", 

81 "values": ["foo", "bar", "baz"], 

82 "values_weights": [-1, 50, 50], 

83 "default": "foo" 

84 } 

85 """ 

86 config = json.loads(json_config) 

87 with pytest.raises(ValueError): 

88 Tunable(name='test', config=config) 

89 

90 

91def test_categorical_wrong_params() -> None: 

92 """ 

93 Disallow range param for categorical tunables. 

94 """ 

95 json_config = """ 

96 { 

97 "type": "categorical", 

98 "values": ["foo", "bar", "foo"], 

99 "range": [0, 1], 

100 "default": "foo" 

101 } 

102 """ 

103 config = json.loads(json_config) 

104 with pytest.raises(ValueError): 

105 Tunable(name='test', config=config) 

106 

107 

108def test_categorical_disallow_special_values() -> None: 

109 """ 

110 Disallow special values for categorical values. 

111 """ 

112 json_config = """ 

113 { 

114 "type": "categorical", 

115 "values": ["foo", "bar", "foo"], 

116 "special": ["baz"], 

117 "default": "foo" 

118 } 

119 """ 

120 config = json.loads(json_config) 

121 with pytest.raises(ValueError): 

122 Tunable(name='test', config=config) 

123 

124 

125def test_categorical_tunable_disallow_repeats() -> None: 

126 """ 

127 Disallow duplicate values in categorical tunables. 

128 """ 

129 with pytest.raises(ValueError): 

130 Tunable(name='test', config={ 

131 "type": "categorical", 

132 "values": ["foo", "bar", "foo"], 

133 "default": "foo", 

134 }) 

135 

136 

137@pytest.mark.parametrize("tunable_type", ["int", "float"]) 

138def test_numerical_tunable_disallow_null_default(tunable_type: TunableValueTypeName) -> None: 

139 """ 

140 Disallow null values as default for numerical tunables. 

141 """ 

142 with pytest.raises(ValueError): 

143 Tunable(name=f'test_{tunable_type}', config={ 

144 "type": tunable_type, 

145 "range": [0, 10], 

146 "default": None, 

147 }) 

148 

149 

150@pytest.mark.parametrize("tunable_type", ["int", "float"]) 

151def test_numerical_tunable_disallow_out_of_range(tunable_type: TunableValueTypeName) -> None: 

152 """ 

153 Disallow out of range values as default for numerical tunables. 

154 """ 

155 with pytest.raises(ValueError): 

156 Tunable(name=f'test_{tunable_type}', config={ 

157 "type": tunable_type, 

158 "range": [0, 10], 

159 "default": 11, 

160 }) 

161 

162 

163@pytest.mark.parametrize("tunable_type", ["int", "float"]) 

164def test_numerical_tunable_wrong_params(tunable_type: TunableValueTypeName) -> None: 

165 """ 

166 Disallow values param for numerical tunables. 

167 """ 

168 with pytest.raises(ValueError): 

169 Tunable(name=f'test_{tunable_type}', config={ 

170 "type": tunable_type, 

171 "range": [0, 10], 

172 "values": ["foo", "bar"], 

173 "default": 0, 

174 }) 

175 

176 

177@pytest.mark.parametrize("tunable_type", ["int", "float"]) 

178def test_numerical_tunable_required_params(tunable_type: TunableValueTypeName) -> None: 

179 """ 

180 Disallow null values param for numerical tunables. 

181 """ 

182 json_config = f""" 

183 {{ 

184 "type": "{tunable_type}", 

185 "range_missing": [0, 10], 

186 "default": 0 

187 }} 

188 """ 

189 config = json.loads(json_config) 

190 with pytest.raises(ValueError): 

191 Tunable(name=f'test_{tunable_type}', config=config) 

192 

193 

194@pytest.mark.parametrize("tunable_type", ["int", "float"]) 

195def test_numerical_tunable_invalid_range(tunable_type: TunableValueTypeName) -> None: 

196 """ 

197 Disallow invalid range param for numerical tunables. 

198 """ 

199 json_config = f""" 

200 {{ 

201 "type": "{tunable_type}", 

202 "range": [0, 10, 7], 

203 "default": 0 

204 }} 

205 """ 

206 config = json.loads(json_config) 

207 with pytest.raises(AssertionError): 

208 Tunable(name=f'test_{tunable_type}', config=config) 

209 

210 

211@pytest.mark.parametrize("tunable_type", ["int", "float"]) 

212def test_numerical_tunable_reversed_range(tunable_type: TunableValueTypeName) -> None: 

213 """ 

214 Disallow reverse range param for numerical tunables. 

215 """ 

216 json_config = f""" 

217 {{ 

218 "type": "{tunable_type}", 

219 "range": [10, 0], 

220 "default": 0 

221 }} 

222 """ 

223 config = json.loads(json_config) 

224 with pytest.raises(ValueError): 

225 Tunable(name=f'test_{tunable_type}', config=config) 

226 

227 

228@pytest.mark.parametrize("tunable_type", ["int", "float"]) 

229def test_numerical_weights(tunable_type: TunableValueTypeName) -> None: 

230 """ 

231 Instantiate a numerical tunable with weighted special values. 

232 """ 

233 json_config = f""" 

234 {{ 

235 "type": "{tunable_type}", 

236 "range": [0, 100], 

237 "special": [0], 

238 "special_weights": [0.1], 

239 "range_weight": 0.9, 

240 "default": 0 

241 }} 

242 """ 

243 config = json.loads(json_config) 

244 tunable = Tunable(name='test', config=config) 

245 assert tunable.special == [0] 

246 assert tunable.weights == [0.1] 

247 assert tunable.range_weight == 0.9 

248 

249 

250@pytest.mark.parametrize("tunable_type", ["int", "float"]) 

251def test_numerical_quantization(tunable_type: TunableValueTypeName) -> None: 

252 """ 

253 Instantiate a numerical tunable with quantization. 

254 """ 

255 json_config = f""" 

256 {{ 

257 "type": "{tunable_type}", 

258 "range": [0, 100], 

259 "quantization": 10, 

260 "default": 0 

261 }} 

262 """ 

263 config = json.loads(json_config) 

264 tunable = Tunable(name='test', config=config) 

265 assert tunable.quantization == 10 

266 assert not tunable.is_log 

267 

268 

269@pytest.mark.parametrize("tunable_type", ["int", "float"]) 

270def test_numerical_log(tunable_type: TunableValueTypeName) -> None: 

271 """ 

272 Instantiate a numerical tunable with log scale. 

273 """ 

274 json_config = f""" 

275 {{ 

276 "type": "{tunable_type}", 

277 "range": [0, 100], 

278 "log": true, 

279 "default": 0 

280 }} 

281 """ 

282 config = json.loads(json_config) 

283 tunable = Tunable(name='test', config=config) 

284 assert tunable.is_log 

285 

286 

287@pytest.mark.parametrize("tunable_type", ["int", "float"]) 

288def test_numerical_weights_no_specials(tunable_type: TunableValueTypeName) -> None: 

289 """ 

290 Raise an error if special_weights are specified but no special values. 

291 """ 

292 json_config = f""" 

293 {{ 

294 "type": "{tunable_type}", 

295 "range": [0, 100], 

296 "special_weights": [0.1, 0.9], 

297 "default": 0 

298 }} 

299 """ 

300 config = json.loads(json_config) 

301 with pytest.raises(ValueError): 

302 Tunable(name='test', config=config) 

303 

304 

305@pytest.mark.parametrize("tunable_type", ["int", "float"]) 

306def test_numerical_weights_non_normalized(tunable_type: TunableValueTypeName) -> None: 

307 """ 

308 Instantiate a numerical tunable with non-normalized weights 

309 of the special values. 

310 """ 

311 json_config = f""" 

312 {{ 

313 "type": "{tunable_type}", 

314 "range": [0, 100], 

315 "special": [-1, 0], 

316 "special_weights": [0, 10], 

317 "range_weight": 90, 

318 "default": 0 

319 }} 

320 """ 

321 config = json.loads(json_config) 

322 tunable = Tunable(name='test', config=config) 

323 assert tunable.special == [-1, 0] 

324 assert tunable.weights == [0, 10] # Zero weights are ok 

325 assert tunable.range_weight == 90 

326 

327 

328@pytest.mark.parametrize("tunable_type", ["int", "float"]) 

329def test_numerical_weights_wrong_count(tunable_type: TunableValueTypeName) -> None: 

330 """ 

331 Try to instantiate a numerical tunable with incorrect number of weights. 

332 """ 

333 json_config = f""" 

334 {{ 

335 "type": "{tunable_type}", 

336 "range": [0, 100], 

337 "special": [0], 

338 "special_weights": [0.1, 0.1, 0.8], 

339 "range_weight": 0.1, 

340 "default": 0 

341 }} 

342 """ 

343 config = json.loads(json_config) 

344 with pytest.raises(ValueError): 

345 Tunable(name='test', config=config) 

346 

347 

348@pytest.mark.parametrize("tunable_type", ["int", "float"]) 

349def test_numerical_weights_no_range_weight(tunable_type: TunableValueTypeName) -> None: 

350 """ 

351 Try to instantiate a numerical tunable with weights but no range_weight. 

352 """ 

353 json_config = f""" 

354 {{ 

355 "type": "{tunable_type}", 

356 "range": [0, 100], 

357 "special": [0, -1], 

358 "special_weights": [0.1, 0.2], 

359 "default": 0 

360 }} 

361 """ 

362 config = json.loads(json_config) 

363 with pytest.raises(ValueError): 

364 Tunable(name='test', config=config) 

365 

366 

367@pytest.mark.parametrize("tunable_type", ["int", "float"]) 

368def test_numerical_range_weight_no_weights(tunable_type: TunableValueTypeName) -> None: 

369 """ 

370 Try to instantiate a numerical tunable with specials but no range_weight. 

371 """ 

372 json_config = f""" 

373 {{ 

374 "type": "{tunable_type}", 

375 "range": [0, 100], 

376 "special": [0, -1], 

377 "range_weight": 0.3, 

378 "default": 0 

379 }} 

380 """ 

381 config = json.loads(json_config) 

382 with pytest.raises(ValueError): 

383 Tunable(name='test', config=config) 

384 

385 

386@pytest.mark.parametrize("tunable_type", ["int", "float"]) 

387def test_numerical_range_weight_no_specials(tunable_type: TunableValueTypeName) -> None: 

388 """ 

389 Try to instantiate a numerical tunable with specials but no range_weight. 

390 """ 

391 json_config = f""" 

392 {{ 

393 "type": "{tunable_type}", 

394 "range": [0, 100], 

395 "range_weight": 0.3, 

396 "default": 0 

397 }} 

398 """ 

399 config = json.loads(json_config) 

400 with pytest.raises(ValueError): 

401 Tunable(name='test', config=config) 

402 

403 

404@pytest.mark.parametrize("tunable_type", ["int", "float"]) 

405def test_numerical_weights_wrong_values(tunable_type: TunableValueTypeName) -> None: 

406 """ 

407 Try to instantiate a numerical tunable with incorrect number of weights. 

408 """ 

409 json_config = f""" 

410 {{ 

411 "type": "{tunable_type}", 

412 "range": [0, 100], 

413 "special": [0], 

414 "special_weights": [-1], 

415 "range_weight": 10, 

416 "default": 0 

417 }} 

418 """ 

419 config = json.loads(json_config) 

420 with pytest.raises(ValueError): 

421 Tunable(name='test', config=config) 

422 

423 

424@pytest.mark.parametrize("tunable_type", ["int", "float"]) 

425def test_numerical_quantization_wrong(tunable_type: TunableValueTypeName) -> None: 

426 """ 

427 Instantiate a numerical tunable with invalid number of quantization points. 

428 """ 

429 json_config = f""" 

430 {{ 

431 "type": "{tunable_type}", 

432 "range": [0, 100], 

433 "quantization": 0, 

434 "default": 0 

435 }} 

436 """ 

437 config = json.loads(json_config) 

438 with pytest.raises(ValueError): 

439 Tunable(name='test', config=config) 

440 

441 

442def test_bad_type() -> None: 

443 """ 

444 Disallow bad types. 

445 """ 

446 json_config = """ 

447 { 

448 "type": "foo", 

449 "range": [0, 10], 

450 "default": 0 

451 } 

452 """ 

453 config = json.loads(json_config) 

454 with pytest.raises(ValueError): 

455 Tunable(name='test_bad_type', config=config)