Skip to content

Commit c0e5d05

Browse files
committed
Add test of shingling generated from circular substructures
This wasn't done earlier, as an unnoticed bug lead to our smiles strings being so different from the ones from rdkit that they couldn't be compared. Now that that bug was fixed, this test could be added.
1 parent 3384714 commit c0e5d05

1 file changed

Lines changed: 80 additions & 44 deletions

File tree

test/unit/algorithms/mhfp_tests.jl

Lines changed: 80 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
@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?
42

53
##### Definition of the molecules used for testing ################################
64

@@ -17,10 +15,9 @@
1715

1816
@testset "MHFP Molecular shingling tests" begin
1917
##### Note on how the testing is done #############################################
20-
# MIGHTDO: move this text into the documentation
2118

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.
2421

2522
# When writing smiles strings using MolecularGraph.jl, we can only pass a molecule
2623
# object to the corresponding function. To generate the smiles string of a
@@ -35,15 +32,8 @@
3532
# atom with an additional hydrogen; while our implementation simply returns "n".
3633
# This as well could be due information lost due to our approach.
3734
# 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,
3936
# 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-
4737

4838
# In conclusion, it is not always possible to test our shingling generating
4939
# functions by comparing their results to the results from the original authors
@@ -58,16 +48,19 @@
5848
# those of the original authors, so we may also have a different number
5949
# of strings that overlap, and thus a different number of unique strings
6050
# - 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.
6760

6861
# A last note: As the function implemented by the original authors returns the
6962
# 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`
7164
# separately.
7265

7366
@testset "Shingling snippet from rings test" begin
@@ -103,26 +96,28 @@
10396
##### Testing if the number of strings is correct, on the larger molecule #####
10497

10598
# this was manually calculated using the original author's code applied to the
106-
# above molecule
99+
# above molecule "mol"
107100
ref_shingling_snippet_atoms = ["C", "C", "C", "c", "n", "n", "c", "c", "[nH]",
108101
"c", "n", "c", "O", "c", "c", "c", "c", "c", "c", "S", "O", "O", "N", "C", "C",
109102
"N", "C", "C", "C", "O", "C","C", "C"]
110103

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))
112106

113107
##### Testing if the actual strings are correct, on the smaller molecule ######
114108

115109
ref_shingling_snippet_atoms_simplemol = [
116110
"c", "c", "c", "C", "O", "O", "c", "c", "c"]
117111

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)
119114
end
120115

121116
@testset "Shingling snippet from circular substructures tests" begin
122117
##### Testing if the number of strings is correct, on the larger molecule #####
123118

124119
# this was manually calculated using the original author's code applied to the
125-
# above molecule
120+
# above molecule "mol"
126121
ref_shingling_snippet_circular_substructures = [
127122
"CC",
128123
"CCC",
@@ -224,7 +219,7 @@
224219
"Cn(c)n",
225220
"Cn(nc)c(c)c"]
226221

227-
## calculating with radius 3, min_radius 1
222+
## calculating with radius 3, min_radius 1
228223
calc_shingling_snippet_circ_subst_r_3_mr_1 = MolecularFingerprints.smiles_from_circular_substructures(
229224
mol, 3, 1)
230225
# ensure length of shingling is the same as the reference shingling
@@ -254,26 +249,67 @@
254249
@test issubset(calc_shingling_snippet_circ_subst_r_3_mr_2,
255250
calc_shingling_snippet_circ_subst_r_3_mr_1)
256251

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+
)
257293
end
258294

259295
@testset "Complete shingling tests" begin
260-
##### Testing MolecularFingerprints.mhfp_shingling_from_mol #########################################
296+
##### Testing mhfp_shingling_from_mol #########################################
261297

262298
# set up calculator with parameters
263299
calculator = MHFP(3, 0, true) # radius, min_radius, rings
264300

265301
# calculate shingling
266302
calculated_shingling = MolecularFingerprints.mhfp_shingling_from_mol(mol, calculator)
267303

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
270306
@test symdiff(calculated_shingling, union(MolecularFingerprints.smiles_from_rings(mol),
271307
MolecularFingerprints.smiles_from_atoms(mol),
272308
# 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
274310
# 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
277313
MolecularFingerprints.smiles_from_circular_substructures(mol, 3, 1)) # radius, min_radius
278314
) == []
279315

@@ -293,7 +329,7 @@
293329
end
294330

295331
@testset "MHFP Hashing function tests" begin
296-
##### Testing MolecularFingerprints.mhfp_hash_from_molecular_shingling ##################################
332+
##### Testing mhfp_hash_from_molecular_shingling ##################################
297333

298334
for fp_size in [512, 2048] # test default 2048, and 2048/4 = 512
299335
for seed in [42, (1 << 31)] # test default value 42 and some high number
@@ -322,9 +358,9 @@
322358
## original vectors #######################################################
323359

324360
# 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.
326362
# 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
328364
# of the sets that were hashed, but it's not a 1:1 relation.
329365
# Instead, for sets with low similarity, the corresponding hashed vectors
330366
# have a similarity which is only around half as large.
@@ -334,8 +370,8 @@
334370

335371
# To verify that our hashing implementation behaves similarly, we generate
336372
# 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
339375
# is calculated for these vectors.
340376
# This is repeated 1000 times and the average calculated, to avoid random
341377
# effects.
@@ -358,7 +394,6 @@
358394
original_tanimoto_similarity_values = []
359395
minhash_tanimoto_similarity_values = []
360396
for j in 1:500 # repeat 500 times to avoid random factors
361-
# seed!(j)
362397

363398
# random set of strings of size 100
364399
test_set = [randstring(25) for k in 1:100]
@@ -369,18 +404,19 @@
369404
test_set_2 = test_set[50 + 1 - overlap_radius:end]
370405

371406
# 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
373408
# approximately:
374409
# 2 / 100 = 0.02,
375410
# 12 / 100 = 0.12,
376411
# 30 / 100 = 0.3,
377412
# 50 / 100 = 0.5 and
378413
# 80 / 100 = 0.8, respectively.
379-
original_tanimoto_similarity_similarity = length(
414+
original_tanimoto_similarity = length(
380415
intersect(test_set_1, test_set_2)) / length(
381416
union(test_set_1, test_set_2))
382417

383-
push!(original_tanimoto_similarity_values, original_tanimoto_similarity_similarity)
418+
push!(original_tanimoto_similarity_values,
419+
original_tanimoto_similarity)
384420

385421
# Calculate hash vectors from the given test sets
386422
minhash_1 = MolecularFingerprints.mhfp_hash_from_molecular_shingling(
@@ -397,14 +433,14 @@
397433

398434
end
399435

400-
# Calculate average tanimoto_similarity similarity for original sets
436+
# Calculate average tanimoto similarity for original sets
401437
avg_original_tanimoto_similarity_value = sum(original_tanimoto_similarity_values) / length(
402438
original_tanimoto_similarity_values)
403-
# Calculate average tanimoto_similarity similarity for the hashed vectors
439+
# Calculate average tanimoto similarity for the hashed vectors
404440
avg_minhash_tanimoto_similarity_value = sum(minhash_tanimoto_similarity_values) / length(
405441
minhash_tanimoto_similarity_values)
406442

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 %
408444
# compared to the reference values
409445
@test avg_minhash_tanimoto_similarity_value ref_minhash_tanimoto_similarity_values[i] rtol=0.05
410446

@@ -454,7 +490,7 @@
454490
# giving non-positive fp_size
455491
@test_throws "must be strictly positive" MHFP(fp_size = 0)
456492

457-
##### Testing MolecularFingerprints.smiles_from_circular_substructures ##################################
493+
##### Testing smiles_from_circular_substructures ##################################
458494
test_mol = smilestomol("c1cc(C(O)=O)ccc1")
459495

460496
# giving non-positive radius

0 commit comments

Comments
 (0)