|
38 | 38 | FilterByStereochemistrySchema, |
39 | 39 | FilterBySubstances, |
40 | 40 | FilterBySubstancesSchema, |
| 41 | + FilterByTautomers, |
| 42 | + FilterByTautomersSchema, |
41 | 43 | FilterByTemperature, |
42 | 44 | FilterByTemperatureSchema, |
43 | 45 | FilterDuplicates, |
@@ -1212,3 +1214,224 @@ def test_curation_does_not_alter_precision(): |
1212 | 1214 | ) |
1213 | 1215 |
|
1214 | 1216 | assert list(data_frame["Mole Fraction 1"]) == list(filtered["Mole Fraction 1"]) |
| 1217 | + |
| 1218 | + |
| 1219 | +def test_validate_filter_by_tautomers(): |
| 1220 | + FilterByTautomersSchema() |
| 1221 | + FilterByTautomersSchema(categories_to_include=None, categories_to_exclude=[]) |
| 1222 | + FilterByTautomersSchema(categories_to_include=["AMIDE_IMIDIC_ACID"]) |
| 1223 | + |
| 1224 | + with pytest.raises(ValidationError): |
| 1225 | + FilterByTautomersSchema(categories_to_include=None, categories_to_exclude=None) |
| 1226 | + |
| 1227 | + with pytest.raises(ValidationError): |
| 1228 | + FilterByTautomersSchema( |
| 1229 | + categories_to_include=["AMIDE_IMIDIC_ACID"], |
| 1230 | + categories_to_exclude=["BETA_DIKETONE"], |
| 1231 | + ) |
| 1232 | + |
| 1233 | + with pytest.raises(ValidationError): |
| 1234 | + FilterByTautomersSchema(categories_to_include=["NOT_A_CATEGORY"]) |
| 1235 | + |
| 1236 | + |
| 1237 | +def test_filter_by_tautomers_schema_default_whitelist(): |
| 1238 | + schema = FilterByTautomersSchema() |
| 1239 | + |
| 1240 | + assert set(schema.categories_to_include) == { |
| 1241 | + "ALPHA_AMINO_ACID", |
| 1242 | + "AMIDE_ENOL", |
| 1243 | + "AMIDE_IMIDIC_ACID", |
| 1244 | + "CARBOXYLIC_ACID_ENOL", |
| 1245 | + "ESTER_ENOL", |
| 1246 | + "IMINE_ENAMINE_PRIMARY", |
| 1247 | + "IMINE_ENAMINE_SECONDARY", |
| 1248 | + "KETO_ENOL_ALIPHATIC", |
| 1249 | + "KETO_ENOL_CYCLIC", |
| 1250 | + "KETO_ENOL_AROMATIC", |
| 1251 | + "KETENE_YNOL", |
| 1252 | + "LACTAM_LACTIM", |
| 1253 | + "OXIME_NITROSO", |
| 1254 | + } |
| 1255 | + |
| 1256 | + |
| 1257 | +def test_filter_by_tautomers_via_workflow(): |
| 1258 | + data_frame = pandas.DataFrame( |
| 1259 | + [ |
| 1260 | + {"N Components": 1, "Component 1": "C"}, |
| 1261 | + { |
| 1262 | + "N Components": 1, |
| 1263 | + "Component 1": "CC(=O)CC(=O)C", |
| 1264 | + }, # beta-diketone, not in whitelist |
| 1265 | + ] |
| 1266 | + ) |
| 1267 | + |
| 1268 | + schema = CurationWorkflowSchema(component_schemas=[FilterByTautomersSchema()]) |
| 1269 | + filtered_frame = CurationWorkflow.apply(data_frame, schema) |
| 1270 | + |
| 1271 | + assert len(filtered_frame) == 1 |
| 1272 | + assert filtered_frame.iloc[0]["Component 1"] == "C" |
| 1273 | + |
| 1274 | + |
| 1275 | +def test_filter_by_tautomers_no_match_removed_default(): |
| 1276 | + data_frame = pandas.DataFrame( |
| 1277 | + # ensure we don't start picking anything up crazy like ethanol |
| 1278 | + [ |
| 1279 | + {"N Components": 1, "Component 1": "C"}, |
| 1280 | + {"N Components": 1, "Component 1": "CCO"}, |
| 1281 | + ] |
| 1282 | + ) |
| 1283 | + |
| 1284 | + filtered_frame = FilterByTautomers.apply(data_frame, FilterByTautomersSchema()) |
| 1285 | + |
| 1286 | + assert len(filtered_frame) == len(data_frame) |
| 1287 | + |
| 1288 | + |
| 1289 | +def test_filter_by_tautomers_whitelist_keto_enol_aliphatic_kept(): |
| 1290 | + """Acetone (KETO_ENOL_ALIPHATIC) must survive the default filter.""" |
| 1291 | + data_frame = pandas.DataFrame([{"N Components": 1, "Component 1": "CC(C)=O"}]) |
| 1292 | + |
| 1293 | + filtered_frame = FilterByTautomers.apply(data_frame, FilterByTautomersSchema()) |
| 1294 | + |
| 1295 | + assert len(filtered_frame) == 1 |
| 1296 | + |
| 1297 | + |
| 1298 | +def test_filter_by_tautomers_default_whitelist_only(): |
| 1299 | + data_frame = pandas.DataFrame( |
| 1300 | + [ |
| 1301 | + {"N Components": 1, "Component 1": "C"}, |
| 1302 | + {"N Components": 1, "Component 1": "CC(=O)N"}, # amide → kept |
| 1303 | + { |
| 1304 | + "N Components": 1, |
| 1305 | + "Component 1": "CC(=O)CC(=O)C", |
| 1306 | + }, # beta-diketone → removed |
| 1307 | + {"N Components": 1, "Component 1": "O=C1CCCCC1"}, # cyclic keto/enol → kept |
| 1308 | + ] |
| 1309 | + ) |
| 1310 | + |
| 1311 | + filtered_frame = FilterByTautomers.apply(data_frame, FilterByTautomersSchema()) |
| 1312 | + |
| 1313 | + assert len(filtered_frame) == 3 |
| 1314 | + assert set(filtered_frame["Component 1"]) == {"C", "CC(=O)N", "O=C1CCCCC1"} |
| 1315 | + |
| 1316 | + |
| 1317 | +def test_filter_by_tautomers_explicit_include(): |
| 1318 | + data_frame = pandas.DataFrame( |
| 1319 | + [ |
| 1320 | + {"N Components": 1, "Component 1": "C"}, |
| 1321 | + {"N Components": 1, "Component 1": "CC(=O)N"}, |
| 1322 | + {"N Components": 1, "Component 1": "CC(=O)CC(=O)C"}, |
| 1323 | + ] |
| 1324 | + ) |
| 1325 | + |
| 1326 | + filtered_frame = FilterByTautomers.apply( |
| 1327 | + data_frame, |
| 1328 | + FilterByTautomersSchema(categories_to_include=["AMIDE_IMIDIC_ACID"]), |
| 1329 | + ) |
| 1330 | + |
| 1331 | + assert len(filtered_frame) == 2 |
| 1332 | + assert set(filtered_frame["Component 1"]) == {"C", "CC(=O)N"} |
| 1333 | + |
| 1334 | + |
| 1335 | +def test_filter_by_tautomers_explicit_exclude(): |
| 1336 | + data_frame = pandas.DataFrame( |
| 1337 | + [ |
| 1338 | + {"N Components": 1, "Component 1": "CC(=O)N"}, |
| 1339 | + {"N Components": 1, "Component 1": "C[N+](=O)[O-]"}, |
| 1340 | + ] |
| 1341 | + ) |
| 1342 | + |
| 1343 | + filtered_frame = FilterByTautomers.apply( |
| 1344 | + data_frame, |
| 1345 | + FilterByTautomersSchema( |
| 1346 | + categories_to_include=None, categories_to_exclude=["AMIDE_IMIDIC_ACID"] |
| 1347 | + ), |
| 1348 | + ) |
| 1349 | + |
| 1350 | + assert len(filtered_frame) == 1 |
| 1351 | + assert filtered_frame.iloc[0]["Component 1"] == "C[N+](=O)[O-]" |
| 1352 | + |
| 1353 | + |
| 1354 | +def test_filter_by_tautomers_exclude_suppression_canonical(): |
| 1355 | + """Acetylacetone is canonically BETA_DIKETONE (suppression hides KETO_ENOL_ALIPHATIC). |
| 1356 | + Excluding KETO_ENOL_ALIPHATIC should therefore keep it; excluding BETA_DIKETONE |
| 1357 | + should remove it.""" |
| 1358 | + data_frame = pandas.DataFrame([{"N Components": 1, "Component 1": "CC(=O)CC(=O)C"}]) |
| 1359 | + |
| 1360 | + # KETO_ENOL_ALIPHATIC is suppressed → molecule is kept |
| 1361 | + kept = FilterByTautomers.apply( |
| 1362 | + data_frame, |
| 1363 | + FilterByTautomersSchema( |
| 1364 | + categories_to_include=None, |
| 1365 | + categories_to_exclude=["KETO_ENOL_ALIPHATIC"], |
| 1366 | + ), |
| 1367 | + ) |
| 1368 | + assert len(kept) == 1 |
| 1369 | + |
| 1370 | + # BETA_DIKETONE is its canonical category → molecule is removed |
| 1371 | + removed = FilterByTautomers.apply( |
| 1372 | + data_frame, |
| 1373 | + FilterByTautomersSchema( |
| 1374 | + categories_to_include=None, |
| 1375 | + categories_to_exclude=["BETA_DIKETONE"], |
| 1376 | + ), |
| 1377 | + ) |
| 1378 | + assert len(removed) == 0 |
| 1379 | + |
| 1380 | + |
| 1381 | +def test_filter_by_tautomers_include_suppression_applied(): |
| 1382 | + """In include mode, suppression must apply: enol-acetylacetone matches both |
| 1383 | + BETA_DIKETONE and KETO_ENOL_ALIPHATIC, but BETA_DIKETONE suppresses the |
| 1384 | + latter, so specifying categories_to_include=["BETA_DIKETONE"] must keep it. |
| 1385 | + The enol form CC(O)=CC(=O)C is used because BETA_DIKETONE's dominant form |
| 1386 | + is the enol.""" |
| 1387 | + data_frame = pandas.DataFrame([{"N Components": 1, "Component 1": "CC(O)=CC(=O)C"}]) |
| 1388 | + |
| 1389 | + filtered_frame = FilterByTautomers.apply( |
| 1390 | + data_frame, |
| 1391 | + FilterByTautomersSchema(categories_to_include=["BETA_DIKETONE"]), |
| 1392 | + ) |
| 1393 | + |
| 1394 | + assert len(filtered_frame) == 1 |
| 1395 | + |
| 1396 | + |
| 1397 | +def test_filter_by_tautomers_minor_form_rejected(): |
| 1398 | + """A molecule in the minor tautomeric form must be rejected even if its |
| 1399 | + category is whitelisted. CC(=O)O (acetic acid) is the dominant form and |
| 1400 | + passes; C=C(O)O (the enol/minor form) must be removed.""" |
| 1401 | + data_frame = pandas.DataFrame( |
| 1402 | + [ |
| 1403 | + {"N Components": 1, "Component 1": "CC(=O)O"}, # dominant — kept |
| 1404 | + {"N Components": 1, "Component 1": "C=C(O)O"}, # minor — removed |
| 1405 | + ] |
| 1406 | + ) |
| 1407 | + |
| 1408 | + filtered_frame = FilterByTautomers.apply(data_frame, FilterByTautomersSchema()) |
| 1409 | + |
| 1410 | + assert len(filtered_frame) == 1 |
| 1411 | + assert filtered_frame.iloc[0]["Component 1"] == "CC(=O)O" |
| 1412 | + |
| 1413 | + |
| 1414 | +def test_filter_by_tautomers_multi_component_row_removed(): |
| 1415 | + data_frame = pandas.DataFrame( |
| 1416 | + [ |
| 1417 | + {"N Components": 2, "Component 1": "C", "Component 2": "CCO"}, |
| 1418 | + { |
| 1419 | + "N Components": 2, |
| 1420 | + "Component 1": "C", |
| 1421 | + "Component 2": "CC(=O)CC(=O)C", |
| 1422 | + }, |
| 1423 | + ] |
| 1424 | + ) |
| 1425 | + |
| 1426 | + filtered_frame = FilterByTautomers.apply(data_frame, FilterByTautomersSchema()) |
| 1427 | + |
| 1428 | + assert len(filtered_frame) == 1 |
| 1429 | + assert filtered_frame.iloc[0]["Component 2"] == "CCO" |
| 1430 | + |
| 1431 | + |
| 1432 | +def test_filter_by_tautomers_empty_frame_no_failure(): |
| 1433 | + data_frame = pandas.DataFrame(columns=["N Components", "Component 1"]) |
| 1434 | + |
| 1435 | + filtered_frame = FilterByTautomers.apply(data_frame, FilterByTautomersSchema()) |
| 1436 | + |
| 1437 | + assert len(filtered_frame) == 0 |
0 commit comments