diff --git a/ReleaseNotes.txt b/ReleaseNotes.txt
index 8a0de121..8cc4e9b4 100644
--- a/ReleaseNotes.txt
+++ b/ReleaseNotes.txt
@@ -7,6 +7,8 @@
 - Pulse Templates:
     - `MappingPulseTemplate`:
         - Raise a ValueError if more than one inner channel is mapped to the same outer channel
+    - Plotting:
+        - Make `plotting.render` behaviour and return value consistent between calls with `InstructionBlock` and `Loop`. Render now always returns 3 arguments.
 
 ## 0.3 ##
 
diff --git a/qupulse/pulses/plotting.py b/qupulse/pulses/plotting.py
index 5d3e6f4e..6f9355cc 100644
--- a/qupulse/pulses/plotting.py
+++ b/qupulse/pulses/plotting.py
@@ -14,10 +14,10 @@ import warnings
 import operator
 import itertools
 
-from qupulse.utils.types import ChannelID, MeasurementWindow, has_type_interface
+from qupulse.utils.types import ChannelID, MeasurementWindow, has_type_interface, TimeType
 from qupulse.pulses.pulse_template import PulseTemplate
 from qupulse.pulses.parameters import Parameter
-from qupulse._program.waveforms import Waveform
+from qupulse._program.waveforms import Waveform, SequenceWaveform
 from qupulse._program.instructions import EXECInstruction, STOPInstruction, AbstractInstructionBlock, \
     REPJInstruction, MEASInstruction, GOTOInstruction, InstructionPointer
 from qupulse._program._loop import Loop, to_waveform
@@ -50,7 +50,7 @@ def iter_waveforms(instruction_block: AbstractInstructionBlock,
 
 
 def iter_instruction_block(instruction_block: AbstractInstructionBlock,
-                           extract_measurements: bool) -> Tuple[list, list, Real]:
+                           extract_measurements: bool) -> Tuple[list, list, TimeType]:
     """Iterates over the instructions contained in an InstructionBlock (thus simulating execution).
 
     In effect, this function simulates the execution of the control flow represented by the passed InstructionBlock
@@ -72,7 +72,7 @@ def iter_instruction_block(instruction_block: AbstractInstructionBlock,
     block_stack = [(enumerate(instruction_block), None)]
     waveforms = []
     measurements = []
-    time = 0
+    time = TimeType(0)
 
     while block_stack:
         block, expected_return = block_stack.pop()
@@ -107,11 +107,11 @@ def iter_instruction_block(instruction_block: AbstractInstructionBlock,
 
 
 def render(program: Union[AbstractInstructionBlock, Loop],
-           sample_rate: Real=10.0,
-           render_measurements: bool=False,
-           time_slice: Tuple[Real, Real]=None) -> Union[Tuple[np.ndarray, Dict[ChannelID, np.ndarray]],
-                                                        Tuple[np.ndarray, Dict[ChannelID, np.ndarray],
-                                                              List[MeasurementWindow]]]:
+           sample_rate: Real = 10.0,
+           render_measurements: bool = False,
+           time_slice: Tuple[Real, Real] = None,
+           plot_channels: Optional[Set[ChannelID]] = None) -> Tuple[np.ndarray, Dict[ChannelID, np.ndarray],
+                                                                    List[MeasurementWindow]]:
     """'Renders' a pulse program.
 
         Samples all contained waveforms into an array according to the control flow of the program.
@@ -121,7 +121,8 @@ def render(program: Union[AbstractInstructionBlock, Loop],
                 old-fashioned InstructionBlock.
             sample_rate: The sample rate in GHz.
             render_measurements: If True, the third return value is a list of measurement windows.
-            time_slice: The time slice to be plotted. If None, the entire pulse will be shown.
+            time_slice: The time slice to be rendered. If None, the entire pulse will be shown.
+            plot_channels: Only channels in this set are rendered. If None, all will.
 
         Returns:
             A tuple (times, values, measurements). times is a numpy.ndarray of dimensions sample_count where
@@ -131,92 +132,84 @@ def render(program: Union[AbstractInstructionBlock, Loop],
             (name, start_time, duration).
         """
     if has_type_interface(program, Loop):
-        return _render_loop(program, sample_rate=sample_rate,
-                            render_measurements=render_measurements, time_slice=time_slice)
+        waveform, measurements = _render_loop(program, render_measurements=render_measurements)
     elif has_type_interface(program, AbstractInstructionBlock):
         warnings.warn("InstructionBlock API is deprecated", DeprecationWarning)
         if time_slice is not None:
             raise ValueError("Keyword argument time_slice is not supported when rendering instruction blocks")
-        return _render_instruction_block(program, sample_rate=sample_rate, render_measurements=render_measurements)
+        waveform, measurements = _render_instruction_block(program, render_measurements=render_measurements)
     else:
         raise ValueError('Cannot render an object of type %r' % type(program), program)
 
+    if waveform is None:
+        return np.array([]), dict(), measurements
 
-def _render_instruction_block(sequence: AbstractInstructionBlock,
-                              sample_rate: Real=10.0,
-                              render_measurements=False) -> Union[Tuple[np.ndarray, Dict[ChannelID, np.ndarray]],
-                                                                  Tuple[np.ndarray, Dict[ChannelID, np.ndarray],
-                                                                        List[MeasurementWindow]]]:
-    """The specific implementation of render for InstructionBlock arguments."""
+    if plot_channels is None:
+        channels = waveform.defined_channels
+    else:
+        channels = waveform.defined_channels & plot_channels
 
-    waveforms, measurements, total_time = iter_instruction_block(sequence, render_measurements)
-    if not waveforms:
-        return np.empty(0), dict()
+    if time_slice is None:
+        start_time, end_time = 0, waveform.duration
+
+    elif time_slice[1] < time_slice[0] or time_slice[0] < 0 or time_slice[1] < 0:
+        raise ValueError("time_slice is not valid.")
+
+    else:
+        start_time, end_time, *_ = time_slice
 
-    channels = waveforms[0].defined_channels
+        # filter measurement windows
+        measurements = [(name, begin, length)
+                        for name, begin, length in measurements
+                        if begin < end_time and begin + length > start_time]
 
-    # add one sample to see the end of the waveform
-    sample_count = total_time * sample_rate + 1
+    sample_count = (end_time - start_time) * sample_rate + 1
+    if sample_count < 2:
+        raise PlottingNotPossibleException(pulse=None,
+                                           description='cannot render sequence with less than 2 data points')
     if not float(sample_count).is_integer():
-        warnings.warn('Sample count not whole number. Casted to integer.')
-    times = np.linspace(0, float(total_time), num=int(sample_count), dtype=float)
-    # move the last sample inside the waveform
+        warnings.warn("Sample count is not an integer. Will be rounded (this changes the sample rate).")
+
+    times = np.linspace(float(start_time), float(end_time), num=int(sample_count), dtype=float)
     times[-1] = np.nextafter(times[-1], times[-2])
 
-    voltages = dict((ch, np.empty(len(times))) for ch in channels)
-    offset = 0
-    for waveform in waveforms:
-        wf_end = offset + waveform.duration
-        indices = slice(*np.searchsorted(times, (offset, wf_end)))
-        sample_times = times[indices] - float(offset)
-        for channel in channels:
-            output_array = voltages[channel][indices]
-            waveform.get_sampled(channel=channel,
-                                 sample_times=sample_times,
-                                 output_array=output_array)
-        assert(output_array.shape == sample_times.shape)
-        offset = wf_end
+    voltages = {ch: np.empty_like(times)
+                for ch in channels}
+    for ch, ch_voltage in voltages.items():
+        waveform.get_sampled(channel=ch, sample_times=times, output_array=ch_voltage)
+
     return times, voltages, measurements
 
 
-def _render_loop(loop: Loop,
-                 sample_rate: Real,
-                 render_measurements: bool,
-                 time_slice: Tuple[Real, Real] = None) -> Union[Tuple[np.ndarray, Dict[ChannelID, np.ndarray]],
-                                                                Tuple[np.ndarray, Dict[ChannelID, np.ndarray],
-                                                                List[MeasurementWindow]]]:
-    """The specific implementation of render for Loop arguments."""
-    waveform = to_waveform(loop)
-    channels = waveform.defined_channels
+def _render_instruction_block(sequence: AbstractInstructionBlock,
+                              render_measurements=False) -> Tuple[Optional[Waveform], List[MeasurementWindow]]:
+    """Transform program into single waveform and measurement windows. The specific implementation of render for
+    InstructionBlock arguments."""
 
-    if time_slice is None:
-        time_slice = (0, waveform.duration)
-    elif time_slice[1] < time_slice[0] or time_slice[0] < 0 or time_slice[1] < 0:
-        raise ValueError("time_slice is not valid.")
+    waveforms, measurements, total_time = iter_instruction_block(sequence, render_measurements)
+    if not waveforms:
+        return None, measurements
+    sequence_waveform = SequenceWaveform(waveforms)
 
-    sample_count = (time_slice[1] - time_slice[0]) * sample_rate + 1
-    if sample_count<2:
-        raise PlottingNotPossibleException(pulse = None, description = 'cannot render sequence with less than 2 data points')
-    times = np.linspace(float(time_slice[0]), float(time_slice[1]), num=int(sample_count), dtype=float)
-    times[-1] = np.nextafter(times[-1], times[-2])
+    return sequence_waveform, measurements
 
-    voltages = {}
 
-    for ch in channels:
-        voltages[ch] = waveform.get_sampled(ch, times)
+def _render_loop(loop: Loop,
+                 render_measurements: bool,) -> Tuple[Waveform, List[MeasurementWindow]]:
+    """Transform program into single waveform and measurement windows.
+    The specific implementation of render for Loop arguments."""
+    waveform = to_waveform(loop)
 
     if render_measurements:
         measurement_dict = loop.get_measurement_windows()
         measurement_list = []
         for name, (begins, lengths) in measurement_dict.items():
-            measurement_list.extend(m
-                                    for m in zip(itertools.repeat(name), begins, lengths)
-                                    if m[1]+m[2] > time_slice[0] and m[1] < time_slice[1])
+            measurement_list.extend(zip(itertools.repeat(name), begins, lengths))
         measurements = sorted(measurement_list, key=operator.itemgetter(1))
     else:
         measurements = []
 
-    return times, voltages, measurements
+    return waveform, measurements
 
 
 def plot(pulse: PulseTemplate,
@@ -271,7 +264,7 @@ def plot(pulse: PulseTemplate,
     if program is not None:
         times, voltages, measurements = render(program,
                                                sample_rate,
-                                               render_measurements=plot_measurements,
+                                               render_measurements=bool(plot_measurements),
                                                time_slice=time_slice)
     else:
         times, voltages, measurements = np.array([]), dict(), []

Exception when rendering TablePT
`qupulse` generated an exception when trying to render a simple pulse with repetitions. The exception seems to be caused by rounding errors. A minimal example:

``` python
#%% Exception due to rounding error in qupulse
# qupulse.__version__ 0.3
# gmpy2 installed
from qupulse.pulses import TablePT
from qupulse.pulses.sequencing import Sequencer as Sequencing
from qupulse.pulses import SequencePT
from qupulse.pulses.plotting import render

period=8.192004194306148e-05
repetitions=80
sampling_rate=1e7
sec_to_ns=1e9

table_pt= TablePT({'test': [(0, 0), (period * sec_to_ns, 0, 'linear')]})
    
sequencer = Sequencing()
template = SequencePT(*((table_pt,) * repetitions))
channels = template.defined_channels
sequencer.push(template, dict(), channel_mapping={ch: ch for ch in channels},
               window_mapping={w: w for w in template.measurement_names})
instructions = sequencer.build()
(_, voltages, _) = render(instructions, sampling_rate / sec_to_ns)
```

This generated the following error:
```
...
  File "c:\projects\qupulse\qupulse\pulses\plotting.py", line 176, in _render_instruction_block
    output_array=output_array)

  File "c:\projects\qupulse\qupulse\_program\waveforms.py", line 84, in get_sampled
    raise ValueError('The sample times are not in the range [0, duration]')

ValueError: The sample times are not in the range [0, duration]
```
When setting the number of repetitions lower (e.g. 8), the exception does not occur.

Might be related to #176

@terrorfisch 
diff --git a/tests/pulses/plotting_tests.py b/tests/pulses/plotting_tests.py
index 162e3ee9..f43fe26e 100644
--- a/tests/pulses/plotting_tests.py
+++ b/tests/pulses/plotting_tests.py
@@ -25,8 +25,9 @@ class PlotterTests(unittest.TestCase):
             render(block)
 
     def test_render_no_waveforms(self) -> None:
-        time, channel_data = render(InstructionBlock())
+        time, channel_data, measurements = render(InstructionBlock())
         self.assertEqual(channel_data, dict())
+        self.assertEqual(measurements, [])
         numpy.testing.assert_equal(time, numpy.empty(0))
 
     def test_iter_waveforms(self) -> None:
@@ -300,6 +301,29 @@ class PlotterTests(unittest.TestCase):
         with self.assertWarnsRegex(UserWarning, "empty", msg="plot() did not issue a warning for an empty pulse"):
             plot(pt, dict(), show=False)
 
+    def test_bug_447(self):
+        """Code from https://github.com/qutech/qupulse/issues/447"""
+        TablePT = TablePulseTemplate
+        SequencePT = SequencePulseTemplate
+        Sequencing = Sequencer
+
+        period = 8.192004194306148e-05
+        repetitions = 80
+        sampling_rate = 1e7
+        sec_to_ns = 1e9
+
+        table_pt = TablePT({'test': [(0, 0), (period * sec_to_ns, 0, 'linear')]})
+
+        sequencer = Sequencing()
+        template = SequencePT(*((table_pt,) * repetitions))
+        channels = template.defined_channels
+        sequencer.push(template, dict(), channel_mapping={ch: ch for ch in channels},
+                       window_mapping={w: w for w in template.measurement_names})
+        instructions = sequencer.build()
+
+        with self.assertWarns(UserWarning):
+            (_, voltages, _) = render(instructions, sampling_rate / sec_to_ns)
+
 
 class PlottingNotPossibleExceptionTests(unittest.TestCase):
 

pip3 install .[plotting,zurich-instruments,tektronix,Faster-fractions]
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-xdist pytest-mock pytest-asyncio
apt-get update apt-get install -y libgmp-dev libmpfr-dev libmpc-dev
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==22.2.0
cached-property==1.5.2
certifi==2021.5.30
coverage==6.2
cycler==0.11.0
execnet==1.9.0
gmpy2==2.1.5
importlib-metadata==4.8.3
iniconfig==1.1.1
kiwisolver==1.3.1
matplotlib==3.3.4
mpmath==1.3.0
numpy==1.19.5
packaging==21.3
Pillow==8.4.0
pluggy==1.0.0
py==1.11.0
pyparsing==3.1.4
pytest==7.0.1
pytest-asyncio==0.16.0
pytest-cov==4.0.0
pytest-mock==3.6.1
pytest-xdist==3.0.2
python-dateutil==2.9.0.post0
qupulse @ file:///qupulse
six==1.17.0
sympy==1.9
tomli==1.2.3
typing_extensions==4.1.1
zipp==3.6.0

name: qupulse
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==22.2.0
      - cached-property==1.5.2
      - coverage==6.2
      - cycler==0.11.0
      - execnet==1.9.0
      - gmpy2==2.1.5
      - importlib-metadata==4.8.3
      - iniconfig==1.1.1
      - kiwisolver==1.3.1
      - matplotlib==3.3.4
      - mpmath==1.3.0
      - numpy==1.19.5
      - packaging==21.3
      - pillow==8.4.0
      - pluggy==1.0.0
      - py==1.11.0
      - pyparsing==3.1.4
      - pytest==7.0.1
      - pytest-asyncio==0.16.0
      - pytest-cov==4.0.0
      - pytest-mock==3.6.1
      - pytest-xdist==3.0.2
      - python-dateutil==2.9.0.post0
      - qupulse==0.3
      - six==1.17.0
      - sympy==1.9
      - tomli==1.2.3
      - typing-extensions==4.1.1
      - zipp==3.6.0
prefix: /opt/conda/envs/qupulse

tests/pulses/plotting_tests.py::PlotterTests::test_render_no_waveforms
tests/pulses/plotting_tests.py::PlotterTests::test_bug_447 tests/pulses/plotting_tests.py::PlottingIsinstanceTests::test_bug_422_mock
tests/pulses/plotting_tests.py::PlotterTests::test_iter_instruction_block tests/pulses/plotting_tests.py::PlotterTests::test_iter_instruction_block_exceptions tests/pulses/plotting_tests.py::PlotterTests::test_iter_waveform_exceptions tests/pulses/plotting_tests.py::PlotterTests::test_iter_waveforms tests/pulses/plotting_tests.py::PlotterTests::test_plot_empty_pulse tests/pulses/plotting_tests.py::PlotterTests::test_render tests/pulses/plotting_tests.py::PlotterTests::test_render_block_time_slice tests/pulses/plotting_tests.py::PlotterTests::test_render_loop_compare tests/pulses/plotting_tests.py::PlotterTests::test_render_loop_invalid_slice tests/pulses/plotting_tests.py::PlotterTests::test_render_loop_sliced tests/pulses/plotting_tests.py::PlotterTests::test_render_unsupported_instructions tests/pulses/plotting_tests.py::PlotterTests::test_render_warning tests/pulses/plotting_tests.py::PlottingNotPossibleExceptionTests::test
ReleaseNotes.txt qupulse/pulses/plotting.py
ReleaseNotes.txt qupulse/pulses/plotting.py