# The modeling features language
# A high level language to describe model features and ranges of model features
# ; and \n separates features
# feature names are case insensitive
# FEATURE_NAME(options) options is a comma separated list
# absorption(x) - x can be FO, ZO, SEQ-ZO-FO, [FO, ZO] for multiple or * for all
# elimination(x) - x can be FO, ZO, MM and MIX-FO-MM
# peripherals(n) - n can be 0, 1, ... or a..b for a range (inclusive in both ends)
# transits(n) - n as above
import functools
import itertools
from lark import Lark
from lark.visitors import Interpreter
import pharmpy.modeling as modeling
grammar = r"""
start: feature (_SEPARATOR feature)*
feature: absorption | elimination | peripherals | transits | lagtime
absorption: "ABSORPTION"i "(" (option) ")"
elimination: "ELIMINATION"i "(" (option) ")"
peripherals: "PERIPHERALS"i "(" (option) ")"
transits: "TRANSITS"i "(" (option) ")"
lagtime: "LAGTIME()"
option: (wildcard | range | value | array)
wildcard: "*"
range: NUMBER ".." NUMBER
value: /[a-zA-Z0-9-]+/
array: "[" [value ("," value)*] "]"
_SEPARATOR: /;|\n/
NUMBER: /\d+/
%ignore " "
"""
[docs]class ModelFeature:
pass
[docs]class Absorption(ModelFeature):
def __init__(self, tree):
self.args = OneArgInterpreter('absorption', ['FO', 'ZO', 'SEQ-ZO-FO']).interpret(tree)
self._funcs = dict()
for arg in self.args:
name = f'ABSORPTION({arg})'
if arg == 'FO':
self._funcs[name] = modeling.set_first_order_absorption
elif arg == 'ZO':
self._funcs[name] = modeling.set_zero_order_absorption
elif arg == 'SEQ-ZO-FO':
self._funcs[name] = modeling.set_seq_zo_fo_absorption
else:
raise ValueError(f'Absorption {arg} not supported')
[docs]class Elimination(ModelFeature):
def __init__(self, tree):
self.args = OneArgInterpreter('elimination', ['FO', 'ZO', 'MM', 'MIX-FO-MM']).interpret(
tree
)
self._funcs = dict()
for arg in self.args:
name = f'ELIMINATION({arg})'
if arg == 'FO':
self._funcs[name] = modeling.set_first_order_elimination
elif arg == 'ZO':
self._funcs[name] = modeling.set_zero_order_elimination
elif arg == 'MM':
self._funcs[name] = modeling.set_michaelis_menten_elimination
elif arg == 'MIX-FO-MM':
self._funcs[name] = modeling.set_mixed_mm_fo_elimination
else:
raise ValueError(f'Elimination {arg} not supported')
[docs]class Transits(ModelFeature):
def __init__(self, tree):
self.args = OneArgInterpreter('transits', []).interpret(tree)
self._funcs = dict()
for arg in self.args:
name = f'TRANSITS({arg})'
self._funcs[name] = functools.partial(modeling.set_transit_compartments, n=arg)
[docs]class Peripherals(ModelFeature):
def __init__(self, tree):
self.args = OneArgInterpreter('peripherals', []).interpret(tree)
self._funcs = dict()
for arg in self.args:
name = f'PERIPHERALS({arg})'
self._funcs[name] = functools.partial(modeling.set_peripheral_compartments, n=arg)
[docs]class Lagtime(ModelFeature):
def __init__(self, tree):
self._funcs = {'LAGTIME()': modeling.set_lag_time}
self.args = None
[docs]class OneArgInterpreter(Interpreter):
def __init__(self, name, a):
self.name = name
self.all = a
[docs] def visit_children(self, tree):
a = super().visit_children(tree)
return set().union(*a)
[docs] def feature(self, tree):
return self.visit_children(tree)
[docs] def absorption(self, tree):
if self.name == 'absorption':
return self.visit_children(tree)
else:
return []
[docs] def elimination(self, tree):
if self.name == 'elimination':
return self.visit_children(tree)
else:
return []
[docs] def transits(self, tree):
if self.name == 'transits':
return self.visit_children(tree)
else:
return []
[docs] def peripherals(self, tree):
if self.name == 'peripherals':
return self.visit_children(tree)
else:
return []
[docs] def option(self, tree):
return self.visit_children(tree)
[docs] def value(self, tree):
value = tree.children[0].value
if self.name == 'transits' or self.name == 'peripherals':
return {int(value)}
else:
return {value.upper()}
[docs] def array(self, tree):
return self.visit_children(tree)
[docs] def wildcard(self, tree):
if self.name == 'peripherals' or self.name == 'transits':
raise ValueError(f'Wildcard (*) not supported for {self.name}')
return set(self.all)
[docs] def range(self, tree):
return set(range(int(tree.children[0]), int(tree.children[1]) + 1))
[docs]class ModelFeatures:
def __init__(self, code):
parser = Lark(grammar)
tree = parser.parse(code)
self._all_features = []
if list(tree.find_data('absorption')):
self.absorption = Absorption(tree)
self._all_features.append(self.absorption)
if list(tree.find_data('elimination')):
self.elimination = Elimination(tree)
self._all_features.append(self.elimination)
if list(tree.find_data('transits')):
self.transits = Transits(tree)
self._all_features.append(self.transits)
if list(tree.find_data('peripherals')):
self.peripherals = Peripherals(tree)
self._all_features.append(self.peripherals)
if list(tree.find_data('lagtime')):
self.lagtime = Lagtime(tree)
self._all_features.append(self.lagtime)
[docs] def all_funcs(self):
funcs = dict()
for feat in self._all_features:
funcs.update(feat._funcs)
return funcs
[docs] def next_funcs(self, have):
funcs = dict()
names = [s.split('(')[0].strip().upper() for s in have]
for feat, func in self.all_funcs().items():
curname = feat.split('(')[0].strip().upper()
if curname not in names:
funcs[feat] = func
return funcs
[docs] def all_combinations(self):
feats = []
for feat in self._all_features:
feats.append([None] + list(feat._funcs.keys()))
for t in itertools.product(*feats):
a = [elt for elt in t if elt is not None]
if a:
yield a