Repository: Qiskit/qiskit

Issue:
Add pulse builder "macro" decorator
<!-- ⚠️ 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?
I have found a decorator of this form very useful for constructing pulse experiments from functions.

I suggest `macro` to represent a python function with an activated builder context that will be called and evaluated during program construction. I propose that we reserve `function` for support of pulse functions which will be analyzed and compiled as a function in the pulse IR with a signature, to then be loaded and evaluated in the hardware.

```python
import functools
from qiskit.pulse import builder


# decorator for pulse builder function
def macro(func):
    @functools.wraps(func)
    def inst_map_wrapper(*args, **kwargs):
        builder_ = builder._active_builder()
        # activate the pulse builder before calling the function
        with pulse.build(builder_.backend) as built:
            output =  func(*args, **kwargs)
        
        builder_.call_schedule(built)
        return output
        

    return inst_map_wrapper


@macro
def nested(a):
    pulse.play(pulse.Gaussian(100, a, 20), pulse.drive_channel(0))
    return a*2

@macro
def test():
    pulse.play(pulse.Constant(100, 1.0), pulse.drive_channel(0))
    output = nested(0.5)
    print(output)

with pulse.build(backend=backend) as sched:
    print(builder._active_builder())
    test()

sched.draw()
```

We should add this to the standard pulse builder library, document, rename what needs to be to be clearer and test. 


Discussion:
how can i contribute to this?
Hello @Aniket049, to get started you could add the macro decorator to `pulse.builder.py` and add some documentation/tests. We could iterate from there. Would you like to me to assign this to you?
yes.please assigned this project to me.I will be greatful

On Mon, 31 Aug, 2020, 10:53 PM Thomas Alexander, <notifications@github.com>
wrote:

> Hello @Aniket049 <https://github.com/Aniket049>, to get started you could
> add the macro decorator to pulse.builder.py and add some
> documentation/tests. We could iterate from there. Would you like to me to
> assign this to you?
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/Qiskit/qiskit-terra/issues/4960#issuecomment-683917047>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ANEUJ4BARB72C56DXO5SINDSDPMA5ANCNFSM4QGENYAA>
> .
>

Hi @Aniket049, have you made any progress here?
Yes Thomas.I have build the code and will be ready after couple of tests.I
will post it with a pull request within a week.

On Tue, 15 Sep, 2020, 10:57 PM Thomas Alexander, <notifications@github.com>
wrote:

> Hi @Aniket049 <https://github.com/Aniket049>, have you made any progress
> here?
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/Qiskit/qiskit-terra/issues/4960#issuecomment-692861180>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ANEUJ4AX6EYLSRG2V2GKJX3SF6PXZANCNFSM4QGENYAA>
> .
>

Hi @Aniket049, any update here? 😄 
Hello @Aniket049, I've reassigned to myself as I needed this functionality. I hope this isn't too much of an inconvenience.

Resolving patch:
diff --git a/qiskit/pulse/__init__.py b/qiskit/pulse/__init__.py
--- a/qiskit/pulse/__init__.py
+++ b/qiskit/pulse/__init__.py
@@ -421,6 +421,7 @@
     phase_offset,
     transpiler_settings,
     # Macros.
+    macro,
     measure,
     measure_all,
     delay_qubits,
diff --git a/qiskit/pulse/builder.py b/qiskit/pulse/builder.py
--- a/qiskit/pulse/builder.py
+++ b/qiskit/pulse/builder.py
@@ -635,7 +635,7 @@ def build(backend=None,
             circuit to pulse scheduler.
 
     Returns:
-        A new builder context which has the active builder inititalized.
+        A new builder context which has the active builder initialized.
     """
     return _PulseBuilder(
         backend=backend,
@@ -1868,6 +1868,64 @@ def barrier(*channels_or_qubits: Union[chans.Channel, int], name: Optional[str]
 
 
 # Macros
+def macro(func: Callable):
+    """Wrap a Python function and activate the parent builder context at calling time.
+
+    This enables embedding Python functions as builder macros. This generates a new
+    :class:`pulse.Schedule` that is embedded in the parent builder context with
+    every call of the decorated macro function. The decorated macro function will
+    behave as if the function code was embedded inline in the parent builder context
+    after parameter substitution.
+
+
+    Examples:
+
+    .. jupyter-execute::
+
+        from qiskit import pulse
+
+        @pulse.macro
+        def measure(qubit: int):
+            pulse.play(pulse.GaussianSquare(16384, 256, 15872), pulse.measure_channel(qubit))
+            mem_slot = pulse.MemorySlot(qubit)
+            pulse.acquire(16384, pulse.acquire_channel(qubit), mem_slot)
+
+            return mem_slot
+
+        with pulse.build(backend=backend) as sched:
+            mem_slot = measure(0)
+            print(f"Qubit measured into {mem_slot}")
+
+        sched.draw()
+
+
+
+
+
+    Args:
+        func: The Python function to enable as a builder macro. There are no
+            requirements on the signature of the function, any calls to pulse
+            builder methods will be added to builder context the wrapped function
+            is called from.
+
+    Returns:
+        Callable: The wrapped ``func``.
+    """
+    func_name = getattr(func, '__name__', repr(func))
+
+    @functools.wraps(func)
+    def wrapper(*args, **kwargs):
+        _builder = _active_builder()
+        # activate the pulse builder before calling the function
+        with build(backend=_builder.backend, name=func_name) as built:
+            output = func(*args, **kwargs)
+
+        _builder.call_schedule(built)
+        return output
+
+    return wrapper
+
+
 def measure(qubits: Union[List[int], int],
             registers: Union[List[StorageLocation], StorageLocation] = None,
             ) -> Union[List[StorageLocation], StorageLocation]:


Test patch:
diff --git a/test/python/pulse/test_builder.py b/test/python/pulse/test_builder.py
--- a/test/python/pulse/test_builder.py
+++ b/test/python/pulse/test_builder.py
@@ -654,6 +654,29 @@ def test_seconds_to_samples(self):
 class TestMacros(TestBuilder):
     """Test builder macros."""
 
+    def test_macro(self):
+        """Test builder macro decorator."""
+        @pulse.macro
+        def nested(a):
+            pulse.play(pulse.Gaussian(100, a, 20), pulse.drive_channel(0))
+            return a*2
+
+        @pulse.macro
+        def test():
+            pulse.play(pulse.Constant(100, 1.0), pulse.drive_channel(0))
+            output = nested(0.5)
+            return output
+
+        with pulse.build(self.backend) as schedule:
+            output = test()
+            self.assertEqual(output, 0.5*2)
+
+        reference = pulse.Schedule()
+        reference += pulse.Play(pulse.Constant(100, 1.0), pulse.DriveChannel(0))
+        reference += pulse.Play(pulse.Gaussian(100, 0.5, 20), pulse.DriveChannel(0))
+
+        self.assertEqual(schedule, reference)
+
     def test_measure(self):
         """Test utility function - measure."""
         with pulse.build(self.backend) as schedule: