Repository: Qiskit/qiskit

Issue:
cmd_def expression extraction should be replaced with math expression parser 
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues to confirm this idea does not exist. -->

### What is the expected enhancement?
Pulse commands supplied by the backend may be parameterized with some expression that includes math performed on parameters of the form `P#`. Examples of valid strings are
```
P1
-(P1)
cos(pi)*P1
1+1+P1
P1/P2
...
```
Currently, parameters are extracted by first sanitizing the untrusted string supplied by backends to only math and then passing to Sympy parser for parameter extraction as [Sympy parsing is known to be unsafe due to its use of `eval`](https://docs.sympy.org/latest/modules/core.html). Sanitization is notoriously hard to implement correctly, and while it currently is mostly sufficient as the majority of backends are assumed to be trusted. 

Rather than using a sanitization + Sympy, a parser for the limited subset of math and expressions required should be implemented. This parser should output a Qiskit expression with parameters.




Discussion:
I found the `cmd_def` returns some fc phases in the format such as `np.pi*0.5`, and it returns `ParametrizedSchedule` because the value is classified as a parameter. Also, parameters such as `-(P#)` returns unsafe expression error. New parser should fix these bugs.

I have implemented shunting yard based math parser in pulse simulator. Maybe I can build some parser for `cmd_def` as well.
The first bug you note has already been fixed in master.

It would be great if you could implement the math parser for terra. In general, the parser should parse math and export a qiskit terra `Expression`, which contains `Parameters` in this manner both circuits and schedules can use. There is no need to implement a parser from scratch, there are several examples of math parsing with preexisting python parsing libraries, we should have a discussion about introducing dependencies and if they are needed, @1ucian0? 



Resolving patch:
diff --git a/qiskit/pulse/__init__.py b/qiskit/pulse/__init__.py
--- a/qiskit/pulse/__init__.py
+++ b/qiskit/pulse/__init__.py
@@ -16,10 +16,11 @@
 
 from .channels import (DeviceSpecification, DriveChannel, MeasureChannel, AcquireChannel,
                        ControlChannel, RegisterSlot, MemorySlot)
+from .cmd_def import CmdDef
 from .commands import (Instruction, Acquire, FrameChange, PersistentValue,
                        SamplePulse, Snapshot, Kernel, Discriminator, functional_pulse)
 from .configuration import LoConfig, LoRange
-from .schedule import Schedule
-from .cmd_def import CmdDef
 from .exceptions import PulseError
 from .interfaces import ScheduleComponent
+# from .parser import parse_string_expr
+from .schedule import Schedule
diff --git a/qiskit/pulse/parser.py b/qiskit/pulse/parser.py
new file mode 100644
--- /dev/null
+++ b/qiskit/pulse/parser.py
@@ -0,0 +1,292 @@
+# -*- coding: utf-8 -*-
+
+# This code is part of Qiskit.
+#
+# (C) Copyright IBM 2017, 2019.
+#
+# This code is licensed under the Apache License, Version 2.0. You may
+# obtain a copy of this license in the LICENSE.txt file in the root directory
+# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
+#
+# Any modifications or derivative works of this code must retain this
+# copyright notice, and modified files need to carry a notice indicating
+# that they have been altered from the originals.
+
+# pylint: disable=invalid-name
+
+"""Helper function to parse string expression given by backends."""
+
+import ast
+import copy
+import operator
+
+import cmath
+
+from qiskit.pulse.exceptions import PulseError
+
+
+class PulseExpression(ast.NodeTransformer):
+    """Expression parser to evaluate parameter values.
+    """
+    # valid functions
+    _math_ops = {
+        'acos': cmath.acos,
+        'acosh': cmath.acosh,
+        'asin': cmath.asin,
+        'asinh': cmath.asinh,
+        'atan': cmath.atan,
+        'atanh': cmath.atanh,
+        'cos': cmath.cos,
+        'cosh': cmath.cosh,
+        'exp': cmath.exp,
+        'log': cmath.log,
+        'log10': cmath.log10,
+        'sin': cmath.sin,
+        'sinh': cmath.sinh,
+        'sqrt': cmath.sqrt,
+        'tan': cmath.tan,
+        'tanh': cmath.tanh,
+        'pi': cmath.pi,
+        'e': cmath.e
+    }
+
+    # valid binary operations
+    _binary_ops = {
+        ast.Add: operator.add,
+        ast.Sub: operator.sub,
+        ast.Mult: operator.mul,
+        ast.Div: operator.truediv,
+        ast.Pow: operator.pow
+    }
+
+    # valid unary operations
+    _unary_ops = {
+        ast.UAdd: operator.pos,
+        ast.USub: operator.neg
+    }
+
+    def __init__(self, source, partial_binding=False):
+        """Create new evaluator.
+
+        Args:
+            source (str or ast.Expression): Expression of equation to evaluate.
+            partial_binding (bool): Allow partial bind of parameters.
+
+        Raises:
+            PulseError: When invalid string is specified.
+        """
+        self._partial_binding = partial_binding
+        self._locals_dict = {}
+        self._params = set()
+
+        if isinstance(source, ast.Expression):
+            self._tree = source
+        else:
+            try:
+                self._tree = ast.parse(source, mode='eval')
+            except SyntaxError:
+                raise PulseError('%s is invalid expression.' % source)
+
+        # parse parameters
+        self.visit(self._tree)
+
+    @property
+    def params(self):
+        """Get parameters."""
+        return sorted(self._params.copy())
+
+    def __call__(self, *args, **kwargs):
+        """Get evaluated value with given parameters.
+
+        Args:
+            *args: Variable length parameter list.
+            **kwargs: Arbitrary parameters.
+
+        Returns:
+            float or complex or ast: Evaluated value.
+
+        Raises:
+            PulseError: When parameters are not bound.
+        """
+        if isinstance(self._tree.body, ast.Num):
+            return self._tree.body.n
+
+        self._locals_dict.clear()
+        if args:
+            for key, val in zip(self.params, args):
+                self._locals_dict[key] = val
+        if kwargs:
+            for key, val in kwargs.items():
+                if key in self.params:
+                    if key not in self._locals_dict.keys():
+                        self._locals_dict[key] = val
+                    else:
+                        raise PulseError("%s got multiple values for argument '%s'"
+                                         % (self.__class__.__name__, key))
+                else:
+                    raise PulseError("%s got an unexpected keyword argument '%s'"
+                                     % (self.__class__.__name__, key))
+
+        expr = self.visit(self._tree)
+
+        if not isinstance(expr.body, ast.Num):
+            if self._partial_binding:
+                return PulseExpression(expr, self._partial_binding)
+            else:
+                raise PulseError('Parameters %s are not all bound.' % self.params)
+        return expr.body.n
+
+    @staticmethod
+    def _match_ops(opr, opr_dict, *args):
+        """Helper method to apply operators.
+
+        Args:
+            opr (ast.AST): Operator of node.
+            opr_dict (dict): Mapper from ast to operator.
+            *args: Arguments supplied to operator.
+
+        Returns:
+            float or complex: Evaluated value.
+
+        Raises:
+            PulseError: When unsupported operation is specified.
+        """
+        for op_type, op_func in opr_dict.items():
+            if isinstance(opr, op_type):
+                return op_func(*args)
+        raise PulseError('Operator %s is not supported.' % opr.__class__.__name__)
+
+    def visit_Expression(self, node):
+        """Evaluate children nodes of expression.
+
+        Args:
+            node (ast.Expression): Expression to evaluate.
+
+        Returns:
+            ast.Expression: Evaluated value.
+        """
+        tmp_node = copy.deepcopy(node)
+        tmp_node.body = self.visit(tmp_node.body)
+
+        return tmp_node
+
+    def visit_Num(self, node):
+        """Return number as it is.
+
+        Args:
+            node (ast.Num): Number.
+
+        Returns:
+            ast.Num: Number to return.
+        """
+        return node
+
+    def visit_Name(self, node):
+        """Evaluate name and return ast.Num if it is bound.
+
+        Args:
+            node (ast.Name): Name to evaluate.
+
+        Returns:
+            ast.Name or ast.Num: Evaluated value.
+
+        Raises:
+            PulseError: When parameter value is not a number.
+        """
+        if node.id in self._math_ops.keys():
+            val = ast.Num(n=self._math_ops[node.id])
+            return ast.copy_location(val, node)
+        elif node.id in self._locals_dict.keys():
+            _val = self._locals_dict[node.id]
+            try:
+                _val = complex(_val)
+                if not _val.imag:
+                    _val = _val.real
+            except ValueError:
+                raise PulseError('Invalid parameter value %s = %s is specified.'
+                                 % (node.id, self._locals_dict[node.id]))
+            val = ast.Num(n=_val)
+            return ast.copy_location(val, node)
+        self._params.add(node.id)
+        return node
+
+    def visit_UnaryOp(self, node):
+        """Evaluate unary operation and return ast.Num if operand is bound.
+
+        Args:
+            node (ast.UnaryOp): Unary operation to evaluate.
+
+        Returns:
+            ast.UnaryOp or ast.Num: Evaluated value.
+        """
+        node.operand = self.visit(node.operand)
+        if isinstance(node.operand, ast.Num):
+            val = ast.Num(n=self._match_ops(node.op, self._unary_ops,
+                                            node.operand.n))
+            return ast.copy_location(val, node)
+        return node
+
+    def visit_BinOp(self, node):
+        """Evaluate binary operation and return ast.Num if operands are bound.
+
+        Args:
+            node (ast.BinOp): Binary operation to evaluate.
+
+        Returns:
+            ast.BinOp or ast.Num: Evaluated value.
+        """
+        node.left = self.visit(node.left)
+        node.right = self.visit(node.right)
+        if isinstance(node.left, ast.Num) and isinstance(node.right, ast.Num):
+            val = ast.Num(n=self._match_ops(node.op, self._binary_ops,
+                                            node.left.n, node.right.n))
+            return ast.copy_location(val, node)
+        return node
+
+    def visit_Call(self, node):
+        """Evaluate function and return ast.Num if all arguments are bound.
+
+        Args:
+            node (ast.Call): Function to evaluate.
+
+        Returns:
+            ast.Call or ast.Num: Evaluated value.
+
+        Raises:
+            PulseError: When unsupported or unsafe function is specified.
+        """
+        if not isinstance(node.func, ast.Name):
+            raise PulseError('Unsafe expression is detected.')
+        node.args = [self.visit(arg) for arg in node.args]
+        if all(isinstance(arg, ast.Num) for arg in node.args):
+            if node.func.id not in self._math_ops.keys():
+                raise PulseError('Function %s is not supported.' % node.func.id)
+            _args = [arg.n for arg in node.args]
+            _val = self._math_ops[node.func.id](*_args)
+            if not _val.imag:
+                _val = _val.real
+            val = ast.Num(n=_val)
+            return ast.copy_location(val, node)
+        return node
+
+    def generic_visit(self, node):
+        raise PulseError('Unsupported node: %s' % node.__class__.__name__)
+
+
+def parse_string_expr(source, partial_binding=False):
+    """Safe parsing of string expression.
+
+    Args:
+        source (str): String expression to parse.
+        partial_binding (bool): Allow partial bind of parameters.
+
+    Returns:
+        PulseExpression: Returns a expression object.
+    """
+    subs = [('numpy.', ''), ('np.', ''), ('math.', ''), ('cmath.', '')]
+    for match, sub in subs:
+        source = source.replace(match, sub)
+
+    expression = PulseExpression(source, partial_binding)
+
+    return expression
diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py
--- a/qiskit/qobj/converters/pulse_instruction.py
+++ b/qiskit/qobj/converters/pulse_instruction.py
@@ -15,19 +15,13 @@
 """Helper class used to convert a pulse instruction into PulseQobjInstruction."""
 
 import re
-import math
 import warnings
 
-from sympy.parsing.sympy_parser import (parse_expr, standard_transformations,
-                                        implicit_multiplication_application,
-                                        function_exponentiation)
-from sympy import Symbol
-
 from qiskit.pulse import commands, channels
-from qiskit.pulse.schedule import ParameterizedSchedule, Schedule
 from qiskit.pulse.exceptions import PulseError
+from qiskit.pulse.parser import parse_string_expr
+from qiskit.pulse.schedule import ParameterizedSchedule, Schedule
 from qiskit.qobj import QobjMeasurementOption
-from qiskit.exceptions import QiskitError
 
 
 class ConversionMethodBinder:
@@ -228,103 +222,6 @@ def convert_snapshot(self, shift, instruction):
         return self._qobj_model(**command_dict)
 
 
-# pylint: disable=invalid-name
-
-# get math operations valid in python. Presumably these are valid in sympy
-_math_ops = [math_op for math_op in math.__dict__ if not math_op.startswith('__')]
-# only allow valid math ops
-_math_ops_regex = r"(" + ")|(".join(_math_ops) + ")"
-# match consecutive alphanumeric, and single consecutive math ops +-/.()
-# and multiple * for exponentiation
-_allowedchars = re.compile(r'(([+\/\-]?|\*{0,2})?[\(\)\s]*'  # allow to start with math/bracket
-                           r'([a-zA-Z][a-zA-Z\d]*|'  # match word
-                           r'[\d]+(\.\d*)?)[\(\)\s]*)*')  # match decimal and bracket
-# match any sequence of chars and numbers
-_expr_regex = r'([a-zA-Z]+\d*)'
-# and valid params
-_param_regex = r'(P\d+)'
-# only valid sequences are P# for parameters and valid math operations above
-_valid_sub_expr = re.compile(_param_regex+'|'+_math_ops_regex)
-# pylint: enable=invalid-name
-
-
-def _is_math_expr_safe(expr):
-    r"""Verify mathematical expression is sanitized.
-
-    Only allow strings of form 'P\d+' and operations from `math`.
-    Allowed chars are [a-zA-Z]. Allowed math operators are '+*/().'
-    where only '*' are allowed to be consecutive.
-
-    Args:
-        expr (str): Expression to sanitize
-
-    Returns:
-        bool: Whether the string is safe to parse math from
-
-    Raise:
-        QiskitError: If math expression is not sanitized
-    """
-
-    only_allowed_chars = _allowedchars.match(expr)
-    if not only_allowed_chars:
-        return False
-    elif not only_allowed_chars.group(0) == expr:
-        return False
-
-    sub_expressions = re.findall(_expr_regex, expr)
-    if not all([_valid_sub_expr.match(sub_exp) for sub_exp in sub_expressions]):
-        return False
-
-    return True
-
-
-def _parse_string_expr(expr):  # pylint: disable=missing-return-type-doc
-    """Parse a mathematical string expression and extract free parameters.
-
-    Args:
-        expr (str): String expression to parse
-
-    Returns:
-        (Callable, Tuple[str]): Returns a callable function and tuple of string symbols
-
-    Raises:
-        QiskitError: If expression is not safe
-    """
-    # remove these strings from expression and hope sympy knows these math expressions
-    # these are effectively reserved keywords
-    subs = [('numpy.', ''), ('np.', ''), ('math.', '')]
-    for match, sub in subs:
-        expr = expr.replace(match, sub)
-    if not _is_math_expr_safe(expr):
-        raise QiskitError('Expression: "%s" is not safe to evaluate.' % expr)
-    params = sorted(re.findall(_param_regex, expr))
-    local_dict = {param: Symbol(param) for param in params}
-    symbols = sorted(list(local_dict.keys()))
-    transformations = (standard_transformations + (implicit_multiplication_application,) +
-                       (function_exponentiation,))
-
-    parsed_expr = parse_expr(expr, local_dict=local_dict, transformations=transformations)
-
-    def parsed_fun(*args, **kwargs):
-        subs = {}
-        matched_params = []
-        if args:
-            subs.update({Symbol(symbols[i]): arg for i, arg in enumerate(args)})
-            matched_params += list(params[i] for i in range(len(args)))
-        if kwargs:
-            subs.update({local_dict[key]: value for key, value in kwargs.items()
-                         if key in local_dict})
-            matched_params += list(key for key in kwargs if key in params)
-
-        if not set(matched_params).issuperset(set(params)) or\
-                not set(subs.keys()).issuperset(set(local_dict.values())):
-            raise PulseError('Supplied params ({args}, {kwargs}) do not match '
-                             '{params}'.format(args=args, kwargs=kwargs, params=params))
-
-        return complex(parsed_expr.evalf(subs=subs))
-    return parsed_fun, params
-
-
 class QobjToInstructionConverter:
     """Converts Qobj models to pulse Instructions
     """
@@ -445,13 +342,13 @@ def convert_frame_change(self, instruction):
 
         # This is parameterized
         if isinstance(phase, str):
-            phase_expr, params = _parse_string_expr(phase)
+            phase_expr = parse_string_expr(phase, partial_binding=False)
 
             def gen_fc_sched(*args, **kwargs):
                 phase = abs(phase_expr(*args, **kwargs))
                 return commands.FrameChange(phase)(channel) << t0
 
-            return ParameterizedSchedule(gen_fc_sched, parameters=params)
+            return ParameterizedSchedule(gen_fc_sched, parameters=phase_expr.params)
 
         return commands.FrameChange(phase)(channel) << t0
 
@@ -470,13 +367,13 @@ def convert_persistent_value(self, instruction):
 
         # This is parameterized
         if isinstance(val, str):
-            val_expr, params = _parse_string_expr(val)
+            val_expr = parse_string_expr(val, partial_binding=False)
 
-            def gen_fc_sched(*args, **kwargs):
+            def gen_pv_sched(*args, **kwargs):
                 val = complex(val_expr(*args, **kwargs))
                 return commands.PersistentValue(val)(channel) << t0
 
-            return ParameterizedSchedule(gen_fc_sched, parameters=params)
+            return ParameterizedSchedule(gen_pv_sched, parameters=val_expr.params)
 
         return commands.PersistentValue(val)(channel) << t0
 


Test patch:
diff --git a/test/python/pulse/test_parser.py b/test/python/pulse/test_parser.py
new file mode 100644
--- /dev/null
+++ b/test/python/pulse/test_parser.py
@@ -0,0 +1,234 @@
+# -*- coding: utf-8 -*-
+
+# This code is part of Qiskit.
+#
+# (C) Copyright IBM 2017, 2019.
+#
+# This code is licensed under the Apache License, Version 2.0. You may
+# obtain a copy of this license in the LICENSE.txt file in the root directory
+# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
+#
+# Any modifications or derivative works of this code must retain this
+# copyright notice, and modified files need to carry a notice indicating
+# that they have been altered from the originals.
+
+"""Parser Test."""
+
+from qiskit.test import QiskitTestCase
+from qiskit.pulse.parser import parse_string_expr
+from qiskit.pulse.exceptions import PulseError
+
+
+class TestInstructionToQobjConverter(QiskitTestCase):
+    """Expression parser test."""
+
+    def test_valid_expression1(self):
+        """Parsing valid expression."""
+
+        expr = '1+1*2*3.2+8*cos(0)**2'
+        parsed_expr = parse_string_expr(expr)
+
+        self.assertEqual(parsed_expr.params, [])
+        self.assertEqual(parsed_expr(), 15.4+0j)
+
+    def test_valid_expression2(self):
+        """Parsing valid expression."""
+
+        expr = 'pi*2'
+        parsed_expr = parse_string_expr(expr)
+
+        self.assertEqual(parsed_expr.params, [])
+        self.assertEqual(parsed_expr(), 6.283185307179586+0j)
+
+    def test_valid_expression3(self):
+        """Parsing valid expression."""
+
+        expr = '-P1*cos(P2)'
+        parsed_expr = parse_string_expr(expr)
+
+        self.assertEqual(parsed_expr.params, ['P1', 'P2'])
+        self.assertEqual(parsed_expr(P1=1.0, P2=2.0), 0.4161468365471424 + 0j)
+
+    def test_valid_expression4(self):
+        """Parsing valid expression."""
+
+        expr = '-P1*P2*P3'
+        parsed_expr = parse_string_expr(expr)
+
+        self.assertEqual(parsed_expr.params, ['P1', 'P2', 'P3'])
+        self.assertEqual(parsed_expr(P1=1.0, P2=2.0, P3=3.0), -6.0 + 0j)
+
+    def test_valid_expression5(self):
+        """Parsing valid expression."""
+
+        expr = '-(P1)'
+        parsed_expr = parse_string_expr(expr)
+
+        self.assertEqual(parsed_expr.params, ['P1'])
+        self.assertEqual(parsed_expr(P1=1.0), -1.0 + 0j)
+
+    def test_valid_expression6(self):
+        """Parsing valid expression."""
+
+        expr = '-1.*P1'
+        parsed_expr = parse_string_expr(expr)
+
+        self.assertEqual(parsed_expr.params, ['P1'])
+        self.assertEqual(parsed_expr(P1=1.0), -1.0 + 0j)
+
+    def test_valid_expression7(self):
+        """Parsing valid expression."""
+
+        expr = '-1.*P1*P2'
+        parsed_expr = parse_string_expr(expr)
+
+        self.assertEqual(parsed_expr.params, ['P1', 'P2'])
+        self.assertEqual(parsed_expr(P1=1.0, P2=2.0), -2.0 + 0j)
+
+    def test_valid_expression8(self):
+        """Parsing valid expression."""
+
+        expr = 'P3-P2*(4+P1)'
+        parsed_expr = parse_string_expr(expr)
+
+        self.assertEqual(parsed_expr.params, ['P1', 'P2', 'P3'])
+        self.assertEqual(parsed_expr(P1=1, P2=2, P3=3), -7.0 + 0j)
+
+    def test_invalid_expressions1(self):
+        """Parsing invalid expressions."""
+
+        expr = '2***2'
+        with self.assertRaises(PulseError):
+            parsed_expr = parse_string_expr(expr)
+            parsed_expr()
+
+    def test_invalid_expressions2(self):
+        """Parsing invalid expressions."""
+
+        expr = 'avdfd*3'
+        with self.assertRaises(PulseError):
+            parsed_expr = parse_string_expr(expr)
+            parsed_expr()
+
+    def test_invalid_expressions3(self):
+        """Parsing invalid expressions."""
+
+        expr = 'Cos(1+2)'
+        with self.assertRaises(PulseError):
+            parsed_expr = parse_string_expr(expr)
+            parsed_expr()
+
+    def test_invalid_expressions4(self):
+        """Parsing invalid expressions."""
+
+        expr = 'hello_world'
+        with self.assertRaises(PulseError):
+            parsed_expr = parse_string_expr(expr)
+            parsed_expr()
+
+    def test_invalid_expressions5(self):
+        """Parsing invalid expressions."""
+
+        expr = '1.1.1.1'
+        with self.assertRaises(PulseError):
+            parsed_expr = parse_string_expr(expr)
+            parsed_expr()
+
+    def test_invalid_expressions6(self):
+        """Parsing invalid expressions."""
+
+        expr = 'abc.1'
+        with self.assertRaises(PulseError):
+            parsed_expr = parse_string_expr(expr)
+            parsed_expr()
+
+    def test_malicious_expressions1(self):
+        """Parsing malicious expressions."""
+
+        expr = '__import__("sys").stdout.write("unsafe input.")'
+        with self.assertRaises(PulseError):
+            parsed_expr = parse_string_expr(expr)
+            parsed_expr()
+
+    def test_malicious_expressions2(self):
+        """Parsing malicious expressions."""
+
+        expr = 'INSERT INTO students VALUES (?,?)'
+        with self.assertRaises(PulseError):
+            parsed_expr = parse_string_expr(expr)
+            parsed_expr()
+
+    def test_malicious_expressions3(self):
+        """Parsing malicious expressions."""
+
+        expr = 'import math'
+        with self.assertRaises(PulseError):
+            parsed_expr = parse_string_expr(expr)
+            parsed_expr()
+
+    def test_malicious_expressions4(self):
+        """Parsing malicious expressions."""
+
+        expr = 'complex'
+        with self.assertRaises(PulseError):
+            parsed_expr = parse_string_expr(expr)
+            parsed_expr()
+
+    def test_malicious_expressions5(self):
+        """Parsing malicious expressions."""
+
+        expr = 'print(1.0)'
+        with self.assertRaises(PulseError):
+            parsed_expr = parse_string_expr(expr)
+            parsed_expr()
+
+    def test_malicious_expressions6(self):
+        """Parsing malicious expressions."""
+
+        expr = 'eval("()._" + "_class_" + "_._" +  "_bases_" + "_[0]")'
+        with self.assertRaises(PulseError):
+            parsed_expr = parse_string_expr(expr)
+            parsed_expr()
+
+    def test_partial_binding(self):
+        """Test partial binding of parameters."""
+
+        expr = 'P1 * P2 + P3 / P4 - P5'
+
+        parsed_expr = parse_string_expr(expr, partial_binding=True)
+        self.assertEqual(parsed_expr.params, ['P1', 'P2', 'P3', 'P4', 'P5'])
+
+        bound_three = parsed_expr(P1=1, P2=2, P3=3)
+        self.assertEqual(bound_three.params, ['P4', 'P5'])
+
+        self.assertEqual(bound_three(P4=4, P5=5), -2.25)
+        self.assertEqual(bound_three(4, 5), -2.25)
+
+        bound_four = bound_three(P4=4)
+        self.assertEqual(bound_four.params, ['P5'])
+        self.assertEqual(bound_four(P5=5), -2.25)
+        self.assertEqual(bound_four(5), -2.25)
+
+        bound_four_new = bound_three(P4=40)
+        self.assertEqual(bound_four_new.params, ['P5'])
+        self.assertEqual(bound_four_new(P5=5), -2.925)
+        self.assertEqual(bound_four_new(5), -2.925)
+
+    def test_argument_duplication(self):
+        """Test duplication of *args and **kwargs."""
+
+        expr = 'P1+P2'
+        parsed_expr = parse_string_expr(expr, partial_binding=True)
+
+        with self.assertRaises(PulseError):
+            parsed_expr(1, P1=1)
+
+        self.assertEqual(parsed_expr(1, P2=2), 3.0)
+
+    def test_unexpected_argument(self):
+        """Test unexpected argument error."""
+        expr = 'P1+P2'
+        parsed_expr = parse_string_expr(expr, partial_binding=True)
+
+        with self.assertRaises(PulseError):
+            parsed_expr(1, 2, P3=3)
diff --git a/test/python/qobj/test_pulse_converter.py b/test/python/qobj/test_pulse_converter.py
--- a/test/python/qobj/test_pulse_converter.py
+++ b/test/python/qobj/test_pulse_converter.py
@@ -21,7 +21,6 @@
                          QobjMeasurementOption)
 from qiskit.qobj.converters import (InstructionToQobjConverter, QobjToInstructionConverter,
                                     LoConfigConverter)
-from qiskit.qobj.converters.pulse_instruction import _is_math_expr_safe
 from qiskit.pulse.commands import (SamplePulse, FrameChange, PersistentValue, Snapshot, Acquire,
                                    Discriminator, Kernel)
 from qiskit.pulse.channels import (DeviceSpecification, Qubit, AcquireChannel, DriveChannel,
@@ -251,35 +250,6 @@ def test_parameterized_persistent_value(self):
         self.assertEqual(evaluated_instruction.timeslots, instruction.timeslots)
         self.assertEqual(evaluated_instruction.instructions[0][-1].command, cmd)
 
-    def test_expression_sanitizer(self):
-        """Test math expression sanitization."""
-
-        self.assertFalse(_is_math_expr_safe('INSERT INTO students VALUES (?,?)'))
-        self.assertFalse(_is_math_expr_safe('import math'))
-        self.assertFalse(_is_math_expr_safe('complex'))
-        self.assertFalse(_is_math_expr_safe('__import__("os").system("clear")'))
-        self.assertFalse(_is_math_expr_safe('eval("()._" + "_class_" + "_._" +'
-                                            ' "_bases_" + "_[0]")'))
-        self.assertFalse(_is_math_expr_safe('2***2'))
-        self.assertFalse(_is_math_expr_safe('avdfd*3'))
-        self.assertFalse(_is_math_expr_safe('Cos(1+2)'))
-        self.assertFalse(_is_math_expr_safe('hello'))
-        self.assertFalse(_is_math_expr_safe('hello_world'))
-        self.assertFalse(_is_math_expr_safe('1_2'))
-        self.assertFalse(_is_math_expr_safe('2+-2'))
-        self.assertFalse(_is_math_expr_safe('print(1.0)'))
-        self.assertFalse(_is_math_expr_safe('1.1.1.1'))
-        self.assertFalse(_is_math_expr_safe('abc.1'))
-
-        self.assertTrue(_is_math_expr_safe('1+1*2*3.2+8*cos(1)**2'))
-        self.assertTrue(_is_math_expr_safe('pi*2'))
-        self.assertTrue(_is_math_expr_safe('-P1*cos(P2)'))
-        self.assertTrue(_is_math_expr_safe('-P1*P2*P3'))
-        self.assertTrue(_is_math_expr_safe('-P1'))
-        self.assertTrue(_is_math_expr_safe('-1.*P1'))
-        self.assertTrue(_is_math_expr_safe('-1.*P1*P2'))
-        self.assertTrue(_is_math_expr_safe('-(P1)'))
-
 
 class TestLoConverter(QiskitTestCase):
     """LO converter tests."""