Skip to content

Commit adf2eba

Browse files
Merge pull request #79 from LukaszSztukiewicz/improve-test-of-mhfp-shingling-from-circular-substructures
Improve test of mhfp shingling from circular substructures
2 parents c2fffad + c0e5d05 commit adf2eba

2 files changed

Lines changed: 81 additions & 45 deletions

File tree

test/runtests.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ using MolecularGraph: smilestomol, MolGraph
44
using MolecularFingerprints
55
using Random: seed!, randstring
66
using SparseArrays
7-
using Graphs: nv, all_simple_paths, degree
7+
using Graphs: nv, all_simple_paths, degree, vertices
88
using PythonCall: Py, pyimport, pyconvert
99
using Distances: cosine_dist
1010

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)