Skip to content

Commit fa23ccd

Browse files
committed
break out HCV specific changes to separate module
1 parent b45ec17 commit fa23ccd

File tree

2 files changed

+318
-16
lines changed

2 files changed

+318
-16
lines changed

pyvdrm/asi2.py

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,6 @@ def __call__(self, mutations):
8383
return Score(not child_score.score, child_score.residues)
8484

8585

86-
class BoolTrue(AsiExpr):
87-
"""Boolean True constant"""
88-
def __call__(self, *args):
89-
return Score(True, [])
90-
91-
92-
class BoolFalse(AsiExpr):
93-
"""Boolean False constant"""
94-
def __call__(self, *args):
95-
return Score(False, [])
96-
97-
9886
class AndExpr(AsiExpr):
9987
"""Fold boolean AND on children"""
10088

@@ -291,11 +279,8 @@ def parser(self, rule):
291279
selectstatement = select + select_quantifier + from_ + residue_list
292280
selectstatement.setParseAction(SelectFrom)
293281

294-
bool_ = Literal('TRUE').suppress().setParseAction(BoolTrue) |\
295-
Literal('FALSE').suppress().setParseAction(BoolFalse)
296-
297282
booleancondition = Forward()
298-
condition = residue | excludestatement | selectstatement | bool_
283+
condition = residue | excludestatement | selectstatement
299284

300285
booleancondition << infixNotation(condition,
301286
[(and_, 2, opAssoc.LEFT, AndExpr),

pyvdrm/hcvr.py

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
"""
2+
ASI2 Parser definition
3+
"""
4+
5+
from functools import reduce, total_ordering
6+
from pyparsing import (Literal, nums, Word, Forward, Optional, Regex,
7+
infixNotation, delimitedList, opAssoc)
8+
from pyvdrm.drm import AsiExpr, AsiBinaryExpr, AsiUnaryExpr, DRMParser
9+
from pyvdrm.vcf import MutationSet
10+
11+
12+
def maybe_foldl(func, noneable):
13+
"""Safely fold a function over a potentially empty list of
14+
potentially null values"""
15+
if noneable is None:
16+
return None
17+
clean = [x for x in noneable if x is not None]
18+
if not clean:
19+
return None
20+
return reduce(func, clean)
21+
22+
23+
def maybe_map(func, noneable):
24+
if noneable is None:
25+
return None
26+
r_list = []
27+
for x in noneable:
28+
if x is None:
29+
continue
30+
result = func(x)
31+
if result is None:
32+
continue
33+
r_list.append(result)
34+
if not r_list:
35+
return None
36+
return r_list
37+
38+
39+
@total_ordering
40+
class Score(object):
41+
"""Encapsulate a score and the residues that support it"""
42+
43+
residues = set([])
44+
score = None
45+
46+
def __init__(self, score, residues):
47+
""" Initialize.
48+
49+
:param bool|float score: value of the score
50+
:param residues: sequence of Mutations
51+
"""
52+
self.score = score
53+
self.residues = set(residues)
54+
55+
def __add__(self, other):
56+
return Score(self.score + other.score, self.residues | other.residues)
57+
58+
def __sub__(self, other):
59+
return Score(self.score - other.score, self.residues | other.residues)
60+
61+
def __repr__(self):
62+
return "Score({!r}, {!r})".format(self.score, self.residues)
63+
64+
def __eq__(self, other):
65+
return self.score == other.score
66+
67+
def __lt__(self, other):
68+
# the total_ordering decorator populates the other 5 comparison
69+
# operations. Implement them explicitly if this causes performance
70+
# issues
71+
return self.score < other.score
72+
73+
def __bool__(self):
74+
return self.score
75+
76+
77+
class Negate(AsiExpr):
78+
"""Unary negation of boolean child"""
79+
def __call__(self, mutations):
80+
child_score = self.children[0](mutations)
81+
if child_score is None:
82+
return Score(True, []) # TODO: propagate negative residues
83+
return Score(not child_score.score, child_score.residues)
84+
85+
86+
class BoolTrue(AsiExpr):
87+
"""Boolean True constant"""
88+
def __call__(self, *args):
89+
return Score(True, [])
90+
91+
92+
class BoolFalse(AsiExpr):
93+
"""Boolean False constant"""
94+
def __call__(self, *args):
95+
return Score(False, [])
96+
97+
98+
class AndExpr(AsiExpr):
99+
"""Fold boolean AND on children"""
100+
101+
def __call__(self, mutations):
102+
scores = map(lambda f: f(mutations), self.children[0])
103+
scores = [Score(False, []) if s is None else s for s in scores]
104+
if not scores:
105+
raise ValueError
106+
107+
residues = set([])
108+
for s in scores:
109+
if not s.score:
110+
return Score(False, [])
111+
residues = residues | s.residues
112+
113+
return Score(True, residues)
114+
115+
116+
class OrExpr(AsiBinaryExpr):
117+
"""Boolean OR on children (binary only)"""
118+
119+
def __call__(self, mutations):
120+
arg1, arg2 = self.children
121+
122+
score1 = arg1(mutations)
123+
score2 = arg2(mutations)
124+
125+
if score1 is None:
126+
score1 = Score(False, [])
127+
if score2 is None:
128+
score2 = Score(False, [])
129+
130+
return Score(score1.score or score2.score,
131+
score1.residues | score2.residues)
132+
133+
134+
class EqualityExpr(AsiExpr):
135+
"""ASI2 inequality expressions"""
136+
137+
def __init__(self, label, pos, children):
138+
super().__init__(label, pos, children)
139+
self.operation, limit = children
140+
self.limit = int(limit)
141+
142+
def __call__(self, x):
143+
if self.operation == 'ATLEAST':
144+
return x >= self.limit
145+
elif self.operation == 'EXACTLY':
146+
return x == self.limit
147+
elif self.operation == 'NOMORETHAN':
148+
return x <= self.limit
149+
150+
raise NotImplementedError
151+
152+
153+
class ScoreExpr(AsiExpr):
154+
"""Score expressions propagate DRM scores"""
155+
156+
def __call__(self, mutations):
157+
if len(self.children) == 3:
158+
operation, minus, score = self.children
159+
if minus != '-':
160+
raise ValueError
161+
score = -1 * int(score)
162+
elif len(self.children) == 2:
163+
operation, score = self.children
164+
score = int(score)
165+
else:
166+
raise ValueError
167+
168+
# evaluate operation and return score
169+
result = operation(mutations)
170+
if result is None:
171+
return None
172+
173+
if result.score is False:
174+
return Score(0, [])
175+
return Score(score, result.residues)
176+
177+
178+
class ScoreList(AsiExpr):
179+
"""Lists of scores are either summed or maxed"""
180+
181+
def __call__(self, mutations):
182+
operation, *rest = self.children
183+
if operation == 'MAX':
184+
return maybe_foldl(max, [f(mutations) for f in rest])
185+
186+
# the default operation is sum
187+
return maybe_foldl(lambda x, y: x+y, [f(mutations) for f in self.children])
188+
189+
190+
class SelectFrom(AsiExpr):
191+
"""Return True if some number of mutations match"""
192+
193+
def typecheck(self, tokens):
194+
# if type(tokens[0]) != EqualityExpr:
195+
# raise TypeError()
196+
pass
197+
198+
def __call__(self, mutations):
199+
operation, *rest = self.children
200+
# the head of the arg list must be an equality expression
201+
202+
scored = list(maybe_map(lambda f: f(mutations), rest))
203+
passing = len(scored)
204+
205+
if operation(passing):
206+
return Score(True, maybe_foldl(
207+
lambda x, y: x.residues.union(y.residues), scored))
208+
else:
209+
return None
210+
211+
212+
class AsiScoreCond(AsiExpr):
213+
"""Score condition"""
214+
215+
label = "ScoreCond"
216+
217+
def __call__(self, args):
218+
"""Score conditions evaluate a list of expressions and sum scores"""
219+
return maybe_foldl(lambda x, y: x+y, map(lambda x: x(args), self.children))
220+
221+
222+
class AsiMutations(object):
223+
"""List of mutations given an ambiguous pattern"""
224+
225+
def __init__(self, _label=None, _pos=None, args=None):
226+
"""Initialize set of mutations from a potentially ambiguous residue
227+
"""
228+
self.mutations = args and MutationSet(''.join(args))
229+
230+
def __repr__(self):
231+
if self.mutations is None:
232+
return "AsiMutations()"
233+
return "AsiMutations(args={!r})".format(str(self.mutations))
234+
235+
def __call__(self, env):
236+
for mutation_set in env:
237+
intersection = self.mutations.mutations & mutation_set.mutations
238+
if len(intersection) > 0:
239+
return Score(True, intersection)
240+
return None
241+
242+
243+
class ASI2(DRMParser):
244+
"""ASI2 Syntax definition"""
245+
246+
def parser(self, rule):
247+
248+
select = Literal('SELECT').suppress()
249+
except_ = Literal('EXCEPT')
250+
exactly = Literal('EXACTLY')
251+
atleast = Literal('ATLEAST')
252+
253+
from_ = Literal('FROM').suppress()
254+
255+
max_ = Literal('MAX')
256+
257+
and_ = Literal('AND').suppress()
258+
or_ = Literal('OR').suppress()
259+
# min_ = Literal('MIN')
260+
261+
notmorethan = Literal('NOTMORETHAN')
262+
l_par = Literal('(').suppress()
263+
r_par = Literal(')').suppress()
264+
mapper = Literal('=>').suppress()
265+
integer = Word(nums)
266+
267+
mutation = Optional(Regex(r'[A-Z]')) + integer + Regex(r'[diA-Z]+')
268+
mutation.setParseAction(AsiMutations)
269+
270+
not_ = Literal('NOT').suppress() + mutation
271+
not_.setParseAction(Negate)
272+
273+
residue = mutation | not_
274+
# integer + l_par + not_ + Regex(r'[A-Z]+') + r_par
275+
# roll this next rule into the mutation object
276+
277+
# Syntax of ASI expressions
278+
excludestatement = except_ + residue
279+
280+
quantifier = exactly | atleast | notmorethan
281+
inequality = quantifier + integer
282+
inequality.setParseAction(EqualityExpr)
283+
284+
select_quantifier = infixNotation(inequality,
285+
[(and_, 2, opAssoc.LEFT, AndExpr),
286+
(or_, 2, opAssoc.LEFT, OrExpr)])
287+
288+
residue_list = l_par + delimitedList(residue) + r_par
289+
290+
# so selectstatement.eval :: [Mutation] -> Maybe Bool
291+
selectstatement = select + select_quantifier + from_ + residue_list
292+
selectstatement.setParseAction(SelectFrom)
293+
294+
bool_ = Literal('TRUE').suppress().setParseAction(BoolTrue) |\
295+
Literal('FALSE').suppress().setParseAction(BoolFalse)
296+
297+
booleancondition = Forward()
298+
condition = residue | excludestatement | selectstatement | bool_
299+
300+
booleancondition << infixNotation(condition,
301+
[(and_, 2, opAssoc.LEFT, AndExpr),
302+
(or_, 2, opAssoc.LEFT, OrExpr)]) | condition
303+
304+
scoreitem = booleancondition + mapper + Optional(Literal('-')) + integer
305+
scoreitem.setParseAction(ScoreExpr)
306+
scorelist = max_ + l_par + delimitedList(scoreitem) + r_par |\
307+
delimitedList(scoreitem)
308+
scorelist.setParseAction(ScoreList)
309+
310+
scorecondition = Literal('SCORE FROM').suppress() +\
311+
l_par + delimitedList(scorelist) + r_par
312+
313+
scorecondition.setParseAction(AsiScoreCond)
314+
315+
statement = booleancondition | scorecondition
316+
317+
return statement.parseString(rule)

0 commit comments

Comments
 (0)