ScanDL2 / fix_dur_module /scasim.py
Di0nigi's picture
First commit
95456ed verified
Raw
History Blame Contribute Delete
6.66 kB
"""
Script that implements the scanpath similarity metric ScaSim by
Von der Malsburg, Titus, and Shravan Vasishth.
"What is the scanpath signature of syntactic reanalysis?."
Journal of Memory and Language 65.2 (2011): 109-127.
"""
from __future__ import annotations
from math import pi, sin, cos, acos
import numpy as np
from typing import List, Tuple, Optional, Any, Union
# only need 0, 2 of s/t due to word index instead of x/y location
def scasim(
s: List[Tuple[int, int, Union[int, float]]],
t: List[Tuple[int, int, Union[int, float]]],
modulator: Optional[float] = 0.83,
normalize: Optional[str] = None, # fixations, durations, None
) -> float:
"""
Calculate the similarity between two scanpaths s and t.
:param s: scanpath s, consisting of fixation locations (word indices) and fixation durations
:param t: scanpath t, consisting of fixation locations (word indices) and fixation durations
:param modulator: modulator specifies how spatial distances between fixations are assessed. When set to 0, any spatial divergence of two
compared scanpaths is penalized independently of its degree. When set to 1, the scanpaths are compared only with respect to their
temporal patterns. The default value approximates the sensitivity to spatial distance found in the human visual system.
:param normalize: if 'fixations', the similarity score is normalized by the number of fixations in the two scanpaths. If 'durations',
the similarity score is normalized by the sum of fixation durations in the two scanpaths. If None, no normalization is applied.
:return: similarity between scanpaths s and t
"""
m, n = len(s), len(t)
d = [list(map(lambda i: 0, range(n + 1))) for _ in range(m + 1)]
# sum of fixation durations of the two scanpaths
s_fixdur_sum = sum([fix[2] for fix in s])
t_fixdur_sum = sum([fix[2] for fix in t])
# number of fixations in the two scanpaths
s_nfix = len(s)
t_nfix = len(t)
acc = 0
# sequence alignment
# loop over fixations in scanpath s:
for fix_i in range(1, m + 1):
acc += s[fix_i - 1][2]
d[fix_i][0] = acc
# loop over fixations in scanpath t:
acc = 0
for fix_j in range(1, n + 1):
acc += t[fix_j - 1][2]
d[0][fix_j] = acc
# Compute similarity:
for fix_i in range(n):
for fix_j in range(m):
# calculating angle between fixation targets:
slon = s[fix_j][0] / (180 / pi) # longitude (x-axis)
tlon = t[fix_i][0] / (180 / pi)
slat = s[fix_j][1] / (180 / pi) # latitude (y-axis)
tlat = t[fix_i][1] / (180 / pi)
angle = acos(sin(slat) * sin(tlat) + cos(slat) * cos(tlat) * cos(slon - tlon)) * (
180 / pi
)
# approximation of cortical magnification:
mixer = modulator**angle
# cost for substitution:
cost = abs(t[fix_i][2] - s[fix_j][2]) * mixer + (t[fix_i][2] + s[fix_j][2]) * (
1.0 - mixer
)
# select optimal edit operation
ops = (
d[fix_j][fix_i + 1] + s[fix_j][2],
d[fix_j + 1][fix_i] + t[fix_i][2],
d[fix_j][fix_i] + cost,
)
# mi = which_min(*ops)
mi = np.argmin(ops)
d[fix_j + 1][fix_i + 1] = ops[mi]
result = d[-1][-1]
if normalize == "fixations":
result /= s_nfix + t_nfix
elif normalize == "durations":
result /= s_fixdur_sum + t_fixdur_sum
return result
def main():
predicted_sp_ids = [
[0, 1, 2, 3, 4, 5, 6, 8, 10, 10, 10, 11],
[0, 1, 1, 2, 4, 5, 7, 4, 3, 7, 1, 8],
[0, 1, 2, 4, 4, 5, 7, 7, 8, 9],
]
original_sp_ids = [
[0, 1, 2, 4, 2, 3, 5, 6, 8, 9, 10, 4, 11],
[0, 1, 2, 4, 3, 8],
[0, 1, 6, 8, 9],
]
predicted_fix_durs = [
[69, 52, 374, 374, 374, 374, 256, 423, 423, 423, 423, 188],
[69, 52, 374, 384, 374, 374, 423, 423, 423, 423, 52, 423],
[69, 52, 374, 502, 502, 374, 423, 423, 374, 374],
]
original_fix_durs = [
[0, 208, 232, 197, 314, 151, 219, 308, 195, 280, 260, 102],
[0, 192, 182, 297, 134],
[0, 195, 130, 101],
]
# remove last element in each sublist of list for predicted_sp_ids, original_sp_ids, and predicted_fix_durs
# these are the pad tokens and they are not contained in original_fix_durs
predicted_sp_ids = [sublist[:-1] for sublist in predicted_sp_ids]
original_sp_ids = [sublist[:-1] for sublist in original_sp_ids]
predicted_fix_durs = [sublist[:-1] for sublist in predicted_fix_durs]
# create dummy y values for original_sp_ids and predicted_sp_ids
dummy_y_original_sp_ids = [[1] * len(sublist) for sublist in original_sp_ids]
dummy_y_predicted_sp_ids = [[1] * len(sublist) for sublist in predicted_sp_ids]
# zip together the predicted_sp_ids and predicted_fix_durs lists as list of list of tuples
predicted_sp = list(
map(
lambda x, y, z: list(zip(x, y, z)),
predicted_sp_ids,
dummy_y_predicted_sp_ids,
predicted_fix_durs,
)
)
# zip together the original_sp_ids and original_fix_durs lists as list of list of tuples
original_sp = list(
map(
lambda x, y, z: list(zip(x, y, z)),
original_sp_ids,
dummy_y_original_sp_ids,
original_fix_durs,
)
)
s1 = predicted_sp[0]
t1 = original_sp[0]
sim1 = scasim(s=s1, t=t1)
s2 = predicted_sp[1]
t2 = original_sp[1]
sim2 = scasim(s=s2, t=t2)
s3 = predicted_sp[2]
t3 = original_sp[2]
sim3 = scasim(s=s3, t=t3)
# normalize by fixations
sim10 = scasim(s=s1, t=t1, normalize="fixations")
sim11 = scasim(s=s2, t=t2, normalize="fixations")
sim12 = scasim(s=s3, t=t3, normalize="fixations")
# normalize by durations
sim13 = scasim(s=s1, t=t1, normalize="durations")
sim14 = scasim(s=s2, t=t2, normalize="durations")
sim15 = scasim(s=s3, t=t3, normalize="durations")
print("normalize by fixations")
print("sim10:", sim10)
print("sim11:", sim11)
print("sim12:", sim12)
print("normalize by durations")
print("sim13:", sim13)
print("sim14:", sim14)
print("sim15:", sim15)
breakpoint()
if __name__ == "__main__":
raise SystemExit(main())