|
1 | 1 | @testset "MHFP Fingerprint Tests" begin |
2 | | - # TODO check if I can now test actual shinglings for circular substructures, up to cases |
3 | | - # maybe even for the larger molecule? |
4 | 2 |
|
5 | 3 | ##### Definition of the molecules used for testing ################################ |
6 | 4 |
|
|
17 | 15 |
|
18 | 16 | @testset "MHFP Molecular shingling tests" begin |
19 | 17 | ##### Note on how the testing is done ############################################# |
20 | | - # MIGHTDO: move this text into the documentation |
21 | 18 |
|
22 | | - # In general, our implmentation cannot be expected to yield identical shinglings |
23 | | - # as the fct written by the original authors in python, due to several reasons: |
| 19 | + # In general, our implementation cannot be expected to yield identical shinglings |
| 20 | + # as the implementation of the original authors in python. |
24 | 21 |
|
25 | 22 | # When writing smiles strings using MolecularGraph.jl, we can only pass a molecule |
26 | 23 | # object to the corresponding function. To generate the smiles string of a |
|
35 | 32 | # atom with an additional hydrogen; while our implementation simply returns "n". |
36 | 33 | # This as well could be due information lost due to our approach. |
37 | 34 | # The rdkit implementation used by the original authors seems to allow to write |
38 | | - # smiels strings of substructures without losing this kind of information. Thus, |
| 35 | + # smiles strings of substructures without losing this kind of information. Thus, |
39 | 36 | # our shinglings don't necessary match. |
40 | | - |
41 | | - # Furthermore, there can exist different smiles strings that are both a valid |
42 | | - # description of the same molecule or substructure. It seems like the functions in |
43 | | - # MolecularGraph.jl do not necessary return the same variant as rdkit in python. |
44 | | - # (although it is hard to rule out that this may also be due to how we create |
45 | | - # the substructure as an induced subgraph) |
46 | | - |
47 | 37 |
|
48 | 38 | # In conclusion, it is not always possible to test our shingling generating |
49 | 39 | # functions by comparing their results to the results from the original authors |
|
58 | 48 | # those of the original authors, so we may also have a different number |
59 | 49 | # of strings that overlap, and thus a different number of unique strings |
60 | 50 | # - We verify for a small, simple molecule that the actual strings also match |
61 | | - # - This is only possible for the strings generated from rings and individual |
62 | | - # atoms of the molecule. The strings from the circular substructures of |
63 | | - # the molecule differ already for this smaller molecule. |
64 | | - # - Lastly, we verify for the large atom that the shingling returned by |
65 | | - # MolecularFingerprints.mhfp_shingling_from_mol is the union of the strings returned by |
66 | | - # MolecularFingerprints.smiles_from_rings, MolecularFingerprints.smiles_from_atoms and MolecularFingerprints.smiles_from_circular_substructures. |
| 51 | + # - This is only truly possible for the strings generated from rings and |
| 52 | + # individual atoms of the molecule. The strings from the circular |
| 53 | + # substructures of the molecule differ already for this smaller molecule. |
| 54 | + # However, we up to upper-/lowercase letters, the strings do match, for the |
| 55 | + # simpler molecule. So we test that the strings match after transforming all |
| 56 | + # strings to lowercase. |
| 57 | + # - Lastly, we verify for the large molecule that the shingling returned by |
| 58 | + # mhfp_shingling_from_mol is the union of the strings returned by |
| 59 | + # smiles_from_rings, smiles_from_atoms and smiles_from_circular_substructures. |
67 | 60 |
|
68 | 61 | # A last note: As the function implemented by the original authors returns the |
69 | 62 | # entire shingling at once, the reference strings that we test against were |
70 | | - # generated manually by running parts of their function MolecularFingerprints.mhfp_shingling_from_mol |
| 63 | + # generated manually by running parts of their `function from_molecular_shingling` |
71 | 64 | # separately. |
72 | 65 |
|
73 | 66 | @testset "Shingling snippet from rings test" begin |
|
103 | 96 | ##### Testing if the number of strings is correct, on the larger molecule ##### |
104 | 97 |
|
105 | 98 | # this was manually calculated using the original author's code applied to the |
106 | | - # above molecule |
| 99 | + # above molecule "mol" |
107 | 100 | ref_shingling_snippet_atoms = ["C", "C", "C", "c", "n", "n", "c", "c", "[nH]", |
108 | 101 | "c", "n", "c", "O", "c", "c", "c", "c", "c", "c", "S", "O", "O", "N", "C", "C", |
109 | 102 | "N", "C", "C", "C", "O", "C","C", "C"] |
110 | 103 |
|
111 | | - @test length(ref_shingling_snippet_atoms) == length(MolecularFingerprints.smiles_from_atoms(mol)) |
| 104 | + @test length(ref_shingling_snippet_atoms) == length( |
| 105 | + MolecularFingerprints.smiles_from_atoms(mol)) |
112 | 106 |
|
113 | 107 | ##### Testing if the actual strings are correct, on the smaller molecule ###### |
114 | 108 |
|
115 | 109 | ref_shingling_snippet_atoms_simplemol = [ |
116 | 110 | "c", "c", "c", "C", "O", "O", "c", "c", "c"] |
117 | 111 |
|
118 | | - @test ref_shingling_snippet_atoms_simplemol == MolecularFingerprints.smiles_from_atoms(simpler_mol) |
| 112 | + @test ref_shingling_snippet_atoms_simplemol == MolecularFingerprints.smiles_from_atoms( |
| 113 | + simpler_mol) |
119 | 114 | end |
120 | 115 |
|
121 | 116 | @testset "Shingling snippet from circular substructures tests" begin |
122 | 117 | ##### Testing if the number of strings is correct, on the larger molecule ##### |
123 | 118 |
|
124 | 119 | # this was manually calculated using the original author's code applied to the |
125 | | - # above molecule |
| 120 | + # above molecule "mol" |
126 | 121 | ref_shingling_snippet_circular_substructures = [ |
127 | 122 | "CC", |
128 | 123 | "CCC", |
|
224 | 219 | "Cn(c)n", |
225 | 220 | "Cn(nc)c(c)c"] |
226 | 221 |
|
227 | | - ## calculating with radius 3, min_radius 1 |
| 222 | + ## calculating with radius 3, min_radius 1 |
228 | 223 | calc_shingling_snippet_circ_subst_r_3_mr_1 = MolecularFingerprints.smiles_from_circular_substructures( |
229 | 224 | mol, 3, 1) |
230 | 225 | # ensure length of shingling is the same as the reference shingling |
|
254 | 249 | @test issubset(calc_shingling_snippet_circ_subst_r_3_mr_2, |
255 | 250 | calc_shingling_snippet_circ_subst_r_3_mr_1) |
256 | 251 |
|
| 252 | + ##### testing if the actual strings are correct, for the smaller molecule ##### |
| 253 | + |
| 254 | + # this was manually calculated using the original author's code applied to the |
| 255 | + # above molecule "simpler_mol" |
| 256 | + ref_shingling_snippet_circular_substructures_simplermol = [ |
| 257 | + "c(c)c", |
| 258 | + "c(cc)cc", |
| 259 | + "c1cccc(C)c1", |
| 260 | + "c(c)c", |
| 261 | + "c(cc)c(c)C", |
| 262 | + "c1ccccc1C(=O)O", |
| 263 | + "c(c)(c)C", |
| 264 | + "c(cc)(cc)C(=O)O", |
| 265 | + "c1(C(=O)O)ccccc1", |
| 266 | + "C(c)(=O)O", |
| 267 | + "C(=O)(O)c(c)c", |
| 268 | + "C(=O)(O)c(cc)cc", |
| 269 | + "OC", |
| 270 | + "OC(c)=O", |
| 271 | + "OC(=O)c(c)c", |
| 272 | + "O=C", |
| 273 | + "O=C(c)O", |
| 274 | + "O=C(O)c(c)c", |
| 275 | + "c(c)c", |
| 276 | + "c(cc)c(c)C", |
| 277 | + "c1ccccc1C(=O)O", |
| 278 | + "c(c)c", |
| 279 | + "c(cc)cc", |
| 280 | + "c1cccc(C)c1", |
| 281 | + "c(c)c", |
| 282 | + "c(cc)cc", |
| 283 | + "c1ccccc1"] |
| 284 | + |
| 285 | + # Note: the case of the letters in the strings doesn't match (as our |
| 286 | + # implementation seems to lose some information on aromaticity), so we can only |
| 287 | + # test disregarding the case. |
| 288 | + @test issetequal( |
| 289 | + lowercase.(ref_shingling_snippet_circular_substructures_simplermol), |
| 290 | + lowercase.(MolecularFingerprints.smiles_from_circular_substructures( |
| 291 | + simpler_mol, 3, 1)) |
| 292 | + ) |
257 | 293 | end |
258 | 294 |
|
259 | 295 | @testset "Complete shingling tests" begin |
260 | | - ##### Testing MolecularFingerprints.mhfp_shingling_from_mol ######################################### |
| 296 | + ##### Testing mhfp_shingling_from_mol ######################################### |
261 | 297 |
|
262 | 298 | # set up calculator with parameters |
263 | 299 | calculator = MHFP(3, 0, true) # radius, min_radius, rings |
264 | 300 |
|
265 | 301 | # calculate shingling |
266 | 302 | calculated_shingling = MolecularFingerprints.mhfp_shingling_from_mol(mol, calculator) |
267 | 303 |
|
268 | | - # Test that the shingling returned from MolecularFingerprints.mhfp_shingling_from_mol is the union |
269 | | - # of MolecularFingerprints.smiles_from_rings, MolecularFingerprints.smiles_from_atoms & MolecularFingerprints.smiles_from_circular_substructures |
| 304 | + # Test that the shingling returned from mhfp_shingling_from_mol is the union |
| 305 | + # of smiles_from_rings, smiles_from_atoms & smiles_from_circular_substructures |
270 | 306 | @test symdiff(calculated_shingling, union(MolecularFingerprints.smiles_from_rings(mol), |
271 | 307 | MolecularFingerprints.smiles_from_atoms(mol), |
272 | 308 | # Note: min_radius is now 1, even though we set it to 0 in the calculator |
273 | | - # above. This is because the MolecularFingerprints.mhfp_shingling_from_mol function increases the |
| 309 | + # above. This is because the mhfp_shingling_from_mol function increases the |
274 | 310 | # min_radius to at least 1 before calling |
275 | | - # MolecularFingerprints.smiles_from_circular_substructures, as the special case of radius 0 is |
276 | | - # already taken care of by the function MolecularFingerprints.smiles_from_atoms |
| 311 | + # smiles_from_circular_substructures, as the special case of radius 0 is |
| 312 | + # already taken care of by the function smiles_from_atoms |
277 | 313 | MolecularFingerprints.smiles_from_circular_substructures(mol, 3, 1)) # radius, min_radius |
278 | 314 | ) == [] |
279 | 315 |
|
|
293 | 329 | end |
294 | 330 |
|
295 | 331 | @testset "MHFP Hashing function tests" begin |
296 | | - ##### Testing MolecularFingerprints.mhfp_hash_from_molecular_shingling ################################## |
| 332 | + ##### Testing mhfp_hash_from_molecular_shingling ################################## |
297 | 333 |
|
298 | 334 | for fp_size in [512, 2048] # test default 2048, and 2048/4 = 512 |
299 | 335 | for seed in [42, (1 << 31)] # test default value 42 and some high number |
|
322 | 358 | ## original vectors ####################################################### |
323 | 359 |
|
324 | 360 | # The original authors claim that the MinHash can be used to estimate the |
325 | | - # tanimoto_similarity similarity of two sets. |
| 361 | + # tanimoto similarity of two sets. |
326 | 362 | # However, testing the claim with their own implementation yields that the |
327 | | - # tanimoto_similarity similarity of the hashed vectors indeed relates to the similarity |
| 363 | + # tanimoto similarity of the hashed vectors indeed relates to the similarity |
328 | 364 | # of the sets that were hashed, but it's not a 1:1 relation. |
329 | 365 | # Instead, for sets with low similarity, the corresponding hashed vectors |
330 | 366 | # have a similarity which is only around half as large. |
|
334 | 370 |
|
335 | 371 | # To verify that our hashing implementation behaves similarly, we generate |
336 | 372 | # pairs of sets that share a certain amount of entries, respectively, which |
337 | | - # implies a certain tanimoto_similarity similarity of the pair of sets. |
338 | | - # Then their MinHash vectors are generated, and then the tanimoto_similarity similarity |
| 373 | + # implies a certain tanimoto similarity of the pair of sets. |
| 374 | + # Then their MinHash vectors are generated, and then the tanimoto similarity |
339 | 375 | # is calculated for these vectors. |
340 | 376 | # This is repeated 1000 times and the average calculated, to avoid random |
341 | 377 | # effects. |
|
358 | 394 | original_tanimoto_similarity_values = [] |
359 | 395 | minhash_tanimoto_similarity_values = [] |
360 | 396 | for j in 1:500 # repeat 500 times to avoid random factors |
361 | | - # seed!(j) |
362 | 397 |
|
363 | 398 | # random set of strings of size 100 |
364 | 399 | test_set = [randstring(25) for k in 1:100] |
|
369 | 404 | test_set_2 = test_set[50 + 1 - overlap_radius:end] |
370 | 405 |
|
371 | 406 | # As probably almost all non-overlapping strings are pairwise |
372 | | - # different, we should get a tanimoto_similarity (jaccard) similarity of |
| 407 | + # different, we should get a tanimoto (jaccard) similarity of |
373 | 408 | # approximately: |
374 | 409 | # 2 / 100 = 0.02, |
375 | 410 | # 12 / 100 = 0.12, |
376 | 411 | # 30 / 100 = 0.3, |
377 | 412 | # 50 / 100 = 0.5 and |
378 | 413 | # 80 / 100 = 0.8, respectively. |
379 | | - original_tanimoto_similarity_similarity = length( |
| 414 | + original_tanimoto_similarity = length( |
380 | 415 | intersect(test_set_1, test_set_2)) / length( |
381 | 416 | union(test_set_1, test_set_2)) |
382 | 417 |
|
383 | | - push!(original_tanimoto_similarity_values, original_tanimoto_similarity_similarity) |
| 418 | + push!(original_tanimoto_similarity_values, |
| 419 | + original_tanimoto_similarity) |
384 | 420 |
|
385 | 421 | # Calculate hash vectors from the given test sets |
386 | 422 | minhash_1 = MolecularFingerprints.mhfp_hash_from_molecular_shingling( |
|
397 | 433 |
|
398 | 434 | end |
399 | 435 |
|
400 | | - # Calculate average tanimoto_similarity similarity for original sets |
| 436 | + # Calculate average tanimoto similarity for original sets |
401 | 437 | avg_original_tanimoto_similarity_value = sum(original_tanimoto_similarity_values) / length( |
402 | 438 | original_tanimoto_similarity_values) |
403 | | - # Calculate average tanimoto_similarity similarity for the hashed vectors |
| 439 | + # Calculate average tanimoto similarity for the hashed vectors |
404 | 440 | avg_minhash_tanimoto_similarity_value = sum(minhash_tanimoto_similarity_values) / length( |
405 | 441 | minhash_tanimoto_similarity_values) |
406 | 442 |
|
407 | | - # test whether the average tanimoto_similarity similarity is off less than 5 % |
| 443 | + # test whether the average tanimoto similarity is off less than 5 % |
408 | 444 | # compared to the reference values |
409 | 445 | @test avg_minhash_tanimoto_similarity_value ≈ ref_minhash_tanimoto_similarity_values[i] rtol=0.05 |
410 | 446 |
|
|
454 | 490 | # giving non-positive fp_size |
455 | 491 | @test_throws "must be strictly positive" MHFP(fp_size = 0) |
456 | 492 |
|
457 | | - ##### Testing MolecularFingerprints.smiles_from_circular_substructures ################################## |
| 493 | + ##### Testing smiles_from_circular_substructures ################################## |
458 | 494 | test_mol = smilestomol("c1cc(C(O)=O)ccc1") |
459 | 495 |
|
460 | 496 | # giving non-positive radius |
|
0 commit comments