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

169 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-01 00:52 +0000

1# 

2# Copyright (c) Microsoft Corporation. 

3# Licensed under the MIT License. 

4# 

5"""Unit tests for checking tunable definition rules.""" 

6 

7import json5 as json 

8import pytest 

9 

10from mlos_bench.tunables.tunable import Tunable 

11from mlos_bench.tunables.tunable_types import TunableValueTypeName 

12 

13 

14def test_tunable_name() -> None: 

15 """Check that tunable name is valid.""" 

16 with pytest.raises(ValueError): 

17 # ! characters are currently disallowed in tunable names 

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

19 

20 

21def test_categorical_required_params() -> None: 

22 """Check that required parameters are present for categorical tunables.""" 

23 json_config = """ 

24 { 

25 "type": "categorical", 

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

27 "default": "foo" 

28 } 

29 """ 

30 config = json.loads(json_config) 

31 assert isinstance(config, dict) 

32 with pytest.raises(ValueError): 

33 Tunable(name="test", config=config) 

34 

35 

36def test_categorical_weights() -> None: 

37 """Instantiate a categorical tunable with weights.""" 

38 json_config = """ 

39 { 

40 "type": "categorical", 

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

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

43 "default": "foo" 

44 } 

45 """ 

46 config = json.loads(json_config) 

47 assert isinstance(config, dict) 

48 tunable = Tunable(name="test", config=config) 

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

50 

51 

52def test_categorical_weights_wrong_count() -> None: 

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

54 json_config = """ 

55 { 

56 "type": "categorical", 

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

58 "values_weights": [50, 50], 

59 "default": "foo" 

60 } 

61 """ 

62 config = json.loads(json_config) 

63 assert isinstance(config, dict) 

64 with pytest.raises(ValueError): 

65 Tunable(name="test", config=config) 

66 

67 

68def test_categorical_weights_wrong_values() -> None: 

69 """Try to instantiate a categorical tunable with invalid weights.""" 

70 json_config = """ 

71 { 

72 "type": "categorical", 

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

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

75 "default": "foo" 

76 } 

77 """ 

78 config = json.loads(json_config) 

79 assert isinstance(config, dict) 

80 with pytest.raises(ValueError): 

81 Tunable(name="test", config=config) 

82 

83 

84def test_categorical_wrong_params() -> None: 

85 """Disallow range param for categorical tunables.""" 

86 json_config = """ 

87 { 

88 "type": "categorical", 

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

90 "range": [0, 1], 

91 "default": "foo" 

92 } 

93 """ 

94 config = json.loads(json_config) 

95 assert isinstance(config, dict) 

96 with pytest.raises(ValueError): 

97 Tunable(name="test", config=config) 

98 

99 

100def test_categorical_disallow_special_values() -> None: 

101 """Disallow special values for categorical values.""" 

102 json_config = """ 

103 { 

104 "type": "categorical", 

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

106 "special": ["baz"], 

107 "default": "foo" 

108 } 

109 """ 

110 config = json.loads(json_config) 

111 assert isinstance(config, dict) 

112 with pytest.raises(ValueError): 

113 Tunable(name="test", config=config) 

114 

115 

116def test_categorical_tunable_disallow_repeats() -> None: 

117 """Disallow duplicate values in categorical tunables.""" 

118 with pytest.raises(ValueError): 

119 Tunable( 

120 name="test", 

121 config={ 

122 "type": "categorical", 

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

124 "default": "foo", 

125 }, 

126 ) 

127 

128 

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

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

131 """Disallow null values as default for numerical tunables.""" 

132 with pytest.raises(ValueError): 

133 Tunable( 

134 name=f"test_{tunable_type}", 

135 config={ 

136 "type": tunable_type, 

137 "range": [0, 10], 

138 "default": None, 

139 }, 

140 ) 

141 

142 

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

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

145 """Disallow out of range values as default for numerical tunables.""" 

146 with pytest.raises(ValueError): 

147 Tunable( 

148 name=f"test_{tunable_type}", 

149 config={ 

150 "type": tunable_type, 

151 "range": [0, 10], 

152 "default": 11, 

153 }, 

154 ) 

155 

156 

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

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

159 """Disallow values param for numerical tunables.""" 

160 with pytest.raises(ValueError): 

161 Tunable( 

162 name=f"test_{tunable_type}", 

163 config={ 

164 "type": tunable_type, 

165 "range": [0, 10], 

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

167 "default": 0, 

168 }, 

169 ) 

170 

171 

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

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

174 """Disallow null values param for numerical tunables.""" 

175 json_config = f""" 

176 { 

177 "type": "{tunable_type}", 

178 "range_missing": [0, 10], 

179 "default": 0 

180 } 

181 """ 

182 config = json.loads(json_config) 

183 assert isinstance(config, dict) 

184 with pytest.raises(ValueError): 

185 Tunable(name=f"test_{tunable_type}", config=config) 

186 

187 

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

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

190 """Disallow invalid range param for numerical tunables.""" 

191 json_config = f""" 

192 { 

193 "type": "{tunable_type}", 

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

195 "default": 0 

196 } 

197 """ 

198 config = json.loads(json_config) 

199 assert isinstance(config, dict) 

200 with pytest.raises(AssertionError): 

201 Tunable(name=f"test_{tunable_type}", config=config) 

202 

203 

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

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

206 """Disallow reverse range param for numerical tunables.""" 

207 json_config = f""" 

208 { 

209 "type": "{tunable_type}", 

210 "range": [10, 0], 

211 "default": 0 

212 } 

213 """ 

214 config = json.loads(json_config) 

215 assert isinstance(config, dict) 

216 with pytest.raises(ValueError): 

217 Tunable(name=f"test_{tunable_type}", config=config) 

218 

219 

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

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

222 """Instantiate a numerical tunable with weighted special values.""" 

223 json_config = f""" 

224 { 

225 "type": "{tunable_type}", 

226 "range": [0, 100], 

227 "special": [0], 

228 "special_weights": [0.1], 

229 "range_weight": 0.9, 

230 "default": 0 

231 } 

232 """ 

233 config = json.loads(json_config) 

234 assert isinstance(config, dict) 

235 tunable = Tunable(name="test", config=config) 

236 assert tunable.special == [0] 

237 assert tunable.weights == [0.1] 

238 assert tunable.range_weight == 0.9 

239 

240 

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

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

243 """Instantiate a numerical tunable with quantization.""" 

244 json_config = f""" 

245 { 

246 "type": "{tunable_type}", 

247 "range": [0, 100], 

248 "quantization_bins": 11, 

249 "default": 0 

250 } 

251 """ 

252 config = json.loads(json_config) 

253 assert isinstance(config, dict) 

254 tunable = Tunable(name="test", config=config) 

255 expected = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100] 

256 assert tunable.quantization_bins == len(expected) 

257 assert pytest.approx(list(tunable.quantized_values or []), 1e-8) == expected 

258 assert not tunable.is_log 

259 

260 

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

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

263 """Instantiate a numerical tunable with log scale.""" 

264 json_config = f""" 

265 { 

266 "type": "{tunable_type}", 

267 "range": [0, 100], 

268 "log": true, 

269 "default": 0 

270 } 

271 """ 

272 config = json.loads(json_config) 

273 assert isinstance(config, dict) 

274 tunable = Tunable(name="test", config=config) 

275 assert tunable.is_log 

276 

277 

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

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

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

281 json_config = f""" 

282 { 

283 "type": "{tunable_type}", 

284 "range": [0, 100], 

285 "special_weights": [0.1, 0.9], 

286 "default": 0 

287 } 

288 """ 

289 config = json.loads(json_config) 

290 assert isinstance(config, dict) 

291 with pytest.raises(ValueError): 

292 Tunable(name="test", config=config) 

293 

294 

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

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

297 """Instantiate a numerical tunable with non-normalized weights of the special 

298 values. 

299 """ 

300 json_config = f""" 

301 { 

302 "type": "{tunable_type}", 

303 "range": [0, 100], 

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

305 "special_weights": [0, 10], 

306 "range_weight": 90, 

307 "default": 0 

308 } 

309 """ 

310 config = json.loads(json_config) 

311 assert isinstance(config, dict) 

312 tunable = Tunable(name="test", config=config) 

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

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

315 assert tunable.range_weight == 90 

316 

317 

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

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

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

321 json_config = f""" 

322 { 

323 "type": "{tunable_type}", 

324 "range": [0, 100], 

325 "special": [0], 

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

327 "range_weight": 0.1, 

328 "default": 0 

329 } 

330 """ 

331 config = json.loads(json_config) 

332 assert isinstance(config, dict) 

333 with pytest.raises(ValueError): 

334 Tunable(name="test", config=config) 

335 

336 

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

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

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

340 json_config = f""" 

341 { 

342 "type": "{tunable_type}", 

343 "range": [0, 100], 

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

345 "special_weights": [0.1, 0.2], 

346 "default": 0 

347 } 

348 """ 

349 config = json.loads(json_config) 

350 assert isinstance(config, dict) 

351 with pytest.raises(ValueError): 

352 Tunable(name="test", config=config) 

353 

354 

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

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

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

358 json_config = f""" 

359 { 

360 "type": "{tunable_type}", 

361 "range": [0, 100], 

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

363 "range_weight": 0.3, 

364 "default": 0 

365 } 

366 """ 

367 config = json.loads(json_config) 

368 assert isinstance(config, dict) 

369 with pytest.raises(ValueError): 

370 Tunable(name="test", config=config) 

371 

372 

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

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

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

376 json_config = f""" 

377 { 

378 "type": "{tunable_type}", 

379 "range": [0, 100], 

380 "range_weight": 0.3, 

381 "default": 0 

382 } 

383 """ 

384 config = json.loads(json_config) 

385 assert isinstance(config, dict) 

386 with pytest.raises(ValueError): 

387 Tunable(name="test", config=config) 

388 

389 

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

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

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

393 json_config = f""" 

394 { 

395 "type": "{tunable_type}", 

396 "range": [0, 100], 

397 "special": [0], 

398 "special_weights": [-1], 

399 "range_weight": 10, 

400 "default": 0 

401 } 

402 """ 

403 config = json.loads(json_config) 

404 assert isinstance(config, dict) 

405 with pytest.raises(ValueError): 

406 Tunable(name="test", config=config) 

407 

408 

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

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

411 """Instantiate a numerical tunable with invalid number of quantization points.""" 

412 json_config = f""" 

413 { 

414 "type": "{tunable_type}", 

415 "range": [0, 100], 

416 "quantization_bins": 0, 

417 "default": 0 

418 } 

419 """ 

420 config = json.loads(json_config) 

421 assert isinstance(config, dict) 

422 with pytest.raises(ValueError): 

423 Tunable(name="test", config=config) 

424 

425 

426def test_bad_type() -> None: 

427 """Disallow bad types.""" 

428 json_config = """ 

429 { 

430 "type": "foo", 

431 "range": [0, 10], 

432 "default": 0 

433 } 

434 """ 

435 config = json.loads(json_config) 

436 assert isinstance(config, dict) 

437 with pytest.raises(ValueError): 

438 Tunable(name="test_bad_type", config=config)