• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

qutech / qupulse / 21365567346

26 Jan 2026 04:34PM UTC coverage: 88.721%. First build
21365567346

Pull #933

github

web-flow
Merge 78346bca5 into 2d86c016d
Pull Request #933: Refactor context management for program builders

453 of 484 new or added lines in 26 files covered. (93.6%)

19170 of 21607 relevant lines covered (88.72%)

5.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.3
/qupulse/program/waveforms.py
1
# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
2
#
3
# SPDX-License-Identifier: LGPL-3.0-or-later
4

5
"""This module contains all waveform classes
6

7
Classes:
8
    - Waveform: An instantiated pulse which can be sampled to a raw voltage value array.
9
"""
10

11
import itertools
6✔
12
import operator
6✔
13
import warnings
6✔
14
import dataclasses
6✔
15
from abc import ABCMeta, abstractmethod
6✔
16
from numbers import Real
6✔
17
from typing import (
6✔
18
    AbstractSet, Any, FrozenSet, Iterable, Mapping, NamedTuple, Sequence, Set,
19
    Tuple, Union, cast, Optional, List, Hashable)
20
from weakref import WeakValueDictionary, ref
6✔
21

22
import numpy as np
6✔
23

24
from qupulse import ChannelID
6✔
25
from qupulse.utils.performance import is_monotonic
6✔
26
from qupulse.expressions import ExpressionScalar
6✔
27
from qupulse.pulses.interpolation import InterpolationStrategy
6✔
28
from qupulse.utils import checked_int_cast, isclose
6✔
29
from qupulse.utils.types import TimeType, time_from_float
6✔
30
from qupulse.program.transformation import Transformation
6✔
31
from qupulse.utils import pairwise
6✔
32

33

34
class ConstantFunctionPulseTemplateWarning(UserWarning):
6✔
35
    """  This warning indicates a constant waveform is constructed from a FunctionPulseTemplate """
36
    pass
6✔
37

38

39
__all__ = ["Waveform", "TableWaveform", "TableWaveformEntry", "FunctionWaveform", "SequenceWaveform",
6✔
40
           "MultiChannelWaveform", "RepetitionWaveform", "TransformingWaveform", "ArithmeticWaveform",
41
           "ConstantFunctionPulseTemplateWarning", "ConstantWaveform"]
42

43
PULSE_TO_WAVEFORM_ERROR = None  # error margin in pulse template to waveform conversion
6✔
44

45
#  these are private because there probably will be changes here
46
_ALLOCATION_FUNCTION = np.full_like  # pre_allocated = ALLOCATION_FUNCTION(sample_times, **ALLOCATION_FUNCTION_KWARGS)
6✔
47
_ALLOCATION_FUNCTION_KWARGS = dict(fill_value=np.nan, dtype=float)
6✔
48

49

50
def _to_time_type(duration: Real) -> TimeType:
6✔
51
    if isinstance(duration, TimeType):
6✔
52
        return duration
6✔
53
    else:
54
        return time_from_float(float(duration), absolute_error=PULSE_TO_WAVEFORM_ERROR)
6✔
55

56

57
@dataclasses.dataclass(frozen=False, eq=False, repr=False)
6✔
58
class WaveformMetadata:
6✔
59
    """Metadata for a waveform. Does not participate in equality and hashing!"""
60

61
    minimal_sample_rate: Optional[TimeType] = None
6✔
62

63
    def __init__(self, **kwargs):
6✔
NEW
64
        for key, value in kwargs.items():
×
NEW
65
            setattr(self, key, value)
×
66

67
    def as_dict(self):
6✔
NEW
68
        data = vars(self).copy()
×
NEW
69
        for field in dataclasses.fields(self):
×
NEW
70
            if field.default is not dataclasses.MISSING:
×
NEW
71
                if data[field.name] == field.default:
×
NEW
72
                    del data[field.name]
×
NEW
73
        return data
×
74

75

76
    def __repr__(self):
6✔
NEW
77
        args = ",".join(f"{name}={value!r}"
×
78
                        for name, value in self.as_dict().items())
NEW
79
        return f'{self.__class__.__name__}({args})'
×
80

81

82
class Waveform(metaclass=ABCMeta):
6✔
83
    """Represents an instantiated PulseTemplate which can be sampled to retrieve arrays of voltage
84
    values for the hardware."""
85

86
    __sampled_cache = WeakValueDictionary()
6✔
87

88
    __slots__ = (
6✔
89
        '_duration', # included in __hash__ and __eq__
90
        '_metadata', # excluded from __hash__ and __eq__
91
    )
92

93
    def __init__(self, duration: TimeType):
6✔
94
        self._duration = duration
6✔
95

96
    @property
6✔
97
    def duration(self) -> TimeType:
6✔
98
        """The duration of the waveform in time units."""
99
        return self._duration
6✔
100

101
    @property
6✔
102
    def metadata(self):
6✔
NEW
103
        try:
×
NEW
104
            return self._metadata
×
NEW
105
        except AttributeError:
×
NEW
106
            metadata = self._metadata = WaveformMetadata()
×
NEW
107
            return metadata
×
108

109
    @abstractmethod
6✔
110
    def unsafe_sample(self,
6✔
111
                      channel: ChannelID,
112
                      sample_times: np.ndarray,
113
                      output_array: Union[np.ndarray, None] = None) -> np.ndarray:
114
        """Sample the waveform at given sample times.
115

116
        The unsafe means that there are no sanity checks performed. The provided sample times are assumed to be
117
        monotonously increasing and lie in the range of [0, waveform.duration]
118

119
        Args:
120
            sample_times: Times at which this Waveform will be sampled.
121
            output_array: Has to be either None or an array of the same size and type as sample_times. If
122
                not None, the sampled values will be written here and this array will be returned
123
        Result:
124
            The sampled values of this Waveform at the provided sample times. Has the same number of
125
            elements as sample_times.
126
        """
127

128
    def get_sampled(self,
6✔
129
                    channel: ChannelID,
130
                    sample_times: np.ndarray,
131
                    output_array: Union[np.ndarray, None] = None) -> np.ndarray:
132
        """A wrapper to the unsafe_sample method which caches the result. This method enforces the constrains
133
        unsafe_sample expects and caches the result to save memory.
134

135
        Args:
136
            sample_times: Times at which this Waveform will be sampled.
137
            output_array: Has to be either None or an array of the same size and type as sample_times. If an array is
138
                given, the sampled values will be written into the given array and it will be returned. Otherwise, a new
139
                array will be created and cached to save memory.
140

141
        Result:
142
            The sampled values of this Waveform at the provided sample times. Is `output_array` if provided
143
        """
144
        if len(sample_times) == 0:
6✔
145
            if output_array is None:
6✔
146
                return np.zeros_like(sample_times, dtype=float)
6✔
147
            elif len(output_array) == len(sample_times):
6✔
148
                return output_array
6✔
149
            else:
150
                raise ValueError('Output array length and sample time length are different')
6✔
151

152
        if not is_monotonic(sample_times):
6✔
153
            raise ValueError('The sample times are not monotonously increasing')
6✔
154
        if sample_times[0] < 0 or sample_times[-1] > float(self.duration):
6✔
155
            raise ValueError(f'The sample times [{sample_times[0]}, ..., {sample_times[-1]}] are not in the range'
6✔
156
                             f' [0, duration={float(self.duration)}]')
157
        if channel not in self.defined_channels:
6✔
158
            raise KeyError('Channel not defined in this waveform: {}'.format(channel))
6✔
159

160
        constant_value = self.constant_value(channel)
6✔
161
        if constant_value is None:
6✔
162
            if output_array is None:
6✔
163
                # cache the result to save memory
164
                result = self.unsafe_sample(channel, sample_times)
6✔
165
                result.flags.writeable = False
6✔
166
                key = hash(bytes(result))
6✔
167
                if key not in self.__sampled_cache:
6✔
168
                    self.__sampled_cache[key] = result
6✔
169
                return self.__sampled_cache[key]
6✔
170
            else:
171
                if len(output_array) != len(sample_times):
6✔
172
                    raise ValueError('Output array length and sample time length are different')
6✔
173
                # use the user provided memory
174
                return self.unsafe_sample(channel=channel,
6✔
175
                                          sample_times=sample_times,
176
                                          output_array=output_array)
177
        else:
178
            if output_array is None:
6✔
179
                output_array = np.full_like(sample_times, fill_value=constant_value, dtype=float)
6✔
180
            else:
181
                output_array[:] = constant_value
6✔
182
            return output_array
6✔
183

184
    def __hash__(self):
6✔
185
        if self.__class__.__base__ is not Waveform:
6✔
186
            # we require direct inheritance because self.__slots__ are the slots of the subclass
187
            # we manually add self._duration but not self._metadata here
188
            raise NotImplementedError("Waveforms __hash__ and __eq__ implementation requires direct inheritance")
×
189
        return hash(tuple(getattr(self, slot) for slot in self.__slots__)) ^ hash(self._duration)
6✔
190

191
    def __eq__(self, other):
6✔
192
        if self.__class__.__base__ is not Waveform:
6✔
193
            # we require direct inheritance because self.__slots__ are the slots of the subclass
194
            # we manually add self._duration but not self._metadata here
NEW
195
            raise NotImplementedError("Waveforms __hash__ and __eq__ implementation requires direct inheritance")
×
196
        slots = self.__slots__
6✔
197
        if slots is getattr(other, '__slots__', None):
6✔
198
            # the __slots__ attribute of self and other are identical objects -> other is of the same type
199
            return self._duration == other._duration and all(getattr(self, slot) == getattr(other, slot) for slot in slots)
6✔
200
        else:
201
            # The other class might be more lenient
202
            return NotImplemented
6✔
203

204
    @property
6✔
205
    @abstractmethod
6✔
206
    def defined_channels(self) -> AbstractSet[ChannelID]:
6✔
207
        """The channels this waveform should played on. Use
208
            :func:`~qupulse.pulses.instructions.get_measurement_windows` to get a waveform for a subset of these."""
209

210
    @abstractmethod
6✔
211
    def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> 'Waveform':
6✔
212
        """Unsafe version of :func:`~qupulse.pulses.instructions.get_measurement_windows`."""
213

214
    def get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> 'Waveform':
6✔
215
        """Get a waveform that only describes the channels contained in `channels`.
216

217
        Args:
218
            channels: A channel set the return value should confine to.
219

220
        Raises:
221
            KeyError: If `channels` is not a subset of the waveform's defined channels.
222

223
        Returns:
224
            A waveform with waveform.defined_channels == `channels`
225
        """
226
        if not channels <= self.defined_channels:
6✔
227
            raise KeyError('Channels not defined on waveform: {}'.format(channels))
6✔
228
        if channels == self.defined_channels:
6✔
229
            return self
6✔
230
        return self.unsafe_get_subset_for_channels(channels=channels)
6✔
231

232
    def is_constant(self) -> bool:
6✔
233
        """Convenience function to check if all channels are constant. The result is equal to
234
        `all(waveform.constant_value(ch) is not None for ch in waveform.defined_channels)` but might be more performant.
235

236
        Returns:
237
            True if all channels have constant values.
238
        """
239
        return self.constant_value_dict() is not None
6✔
240

241
    def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
6✔
242
        result = {ch: self.constant_value(ch) for ch in self.defined_channels}
6✔
243
        if None in result.values():
6✔
244
            return None
6✔
245
        else:
246
            return result
6✔
247

248
    def constant_value(self, channel: ChannelID) -> Optional[float]:
6✔
249
        """Checks if the requested channel has a constant value and returns it if so.
250

251
        Guarantee that this assertion passes for every t in waveform duration:
252
        >>> assert waveform.constant_value(channel) is None or waveform.constant_value(t) = waveform.get_sampled(channel, t)
253

254
        Args:
255
            channel: The channel to check
256

257
        Returns:
258
            None if there is no guarantee that the channel is constant. The value otherwise.
259
        """
260
        return None
6✔
261

262
    def __neg__(self):
6✔
263
        return FunctorWaveform.from_functor(self, {ch: np.negative for ch in self.defined_channels})
6✔
264

265
    def __pos__(self):
6✔
266
        return self
6✔
267

268
    def _sort_key_for_channels(self) -> Sequence[Tuple[str, int]]:
6✔
269
        """Makes reproducible sorting by defined channels possible"""
270
        return sorted((ch, 0) if isinstance(ch, str) else ('', ch) for ch in self.defined_channels)
6✔
271

272
    def reversed(self) -> 'Waveform':
6✔
273
        """Returns a reversed version of this waveform."""
274
        # We don't check for constness here because const waveforms are supposed to override this method
275
        return ReversedWaveform(self)
6✔
276

277

278
class TableWaveformEntry(NamedTuple('TableWaveformEntry', [('t', Real),
6✔
279
                                                           ('v', float),
280
                                                           ('interp', InterpolationStrategy)])):
281
    def __init__(self, t: float, v: float, interp: InterpolationStrategy):
6✔
282
        if not callable(interp):
6✔
283
            raise TypeError('{} is neither callable nor of type InterpolationStrategy'.format(interp))
6✔
284

285
    def __repr__(self):
6✔
286
        return f'{type(self).__name__}(t={self.t!r}, v={self.v!r}, interp={self.interp!r})'
6✔
287

288

289
class TableWaveform(Waveform):
6✔
290
    EntryInInit = Union[TableWaveformEntry, Tuple[float, float, InterpolationStrategy]]
6✔
291

292
    """Waveform obtained from instantiating a TablePulseTemplate."""
6✔
293

294
    __slots__ = ('_table', '_channel_id')
6✔
295

296
    def __init__(self,
6✔
297
                 channel: ChannelID,
298
                 waveform_table: Tuple[TableWaveformEntry, ...]) -> None:
299
        """Create a new TableWaveform instance.
300

301
        Args:
302
            waveform_table: A tuple of instantiated and validated table entries
303
        """
304
        if not isinstance(waveform_table, tuple):
6✔
305
            warnings.warn("Please use a tuple of TableWaveformEntry to construct TableWaveform directly",
×
306
                          category=DeprecationWarning)
307
            waveform_table = self._validate_input(waveform_table)
×
308

309
        super().__init__(duration=_to_time_type(waveform_table[-1].t))
6✔
310

311
        self._table = waveform_table
6✔
312
        self._channel_id = channel
6✔
313

314
    @staticmethod
6✔
315
    def _validate_input(input_waveform_table: Sequence[EntryInInit]) -> Union[Tuple[Real, Real],
6✔
316
                                                                              List[TableWaveformEntry]]:
317
        """ Checks that:
318
         - the time is increasing,
319
         - there are at least two entries
320

321
        Optimizations:
322
          - removes subsequent entries with same time or voltage values.
323
          - checks if the complete waveform is constant. Returns a (duration, value) tuple if this is the case
324

325
        Raises:
326
            ValueError:
327
              - there are less than two entries
328
              - the entries are not ordered in time
329
              - Any time is negative
330
              - The total length is zero
331

332
        Returns:
333
            A list of de-duplicated table entries
334
            OR
335
            A (duration, value) tuple if the waveform is constant
336
        """
337
        # we use an iterator here to avoid duplicate work and be maximally efficient for short tables
338
        # We never use StopIteration to abort iteration. It always signifies an error.
339
        input_iter = iter(input_waveform_table)
6✔
340
        try:
6✔
341
            first_t, first_v, first_interp = next(input_iter)
6✔
342
        except StopIteration:
6✔
343
            raise ValueError("Waveform table mut not be empty")
6✔
344

345
        if first_t != 0.0:
6✔
346
            raise ValueError('First time entry is not zero.')
6✔
347

348
        previous_t = 0.0
6✔
349
        previous_v = first_v
6✔
350
        output_waveform_table = [TableWaveformEntry(0.0, first_v, first_interp)]
6✔
351

352
        try:
6✔
353
            t, v, interp = next(input_iter)
6✔
354
        except StopIteration:
6✔
355
            raise ValueError("Waveform table has less than two entries.")
6✔
356
        if t < 0:
6✔
357
            raise ValueError('Negative time values are not allowed.')
6✔
358

359
        # constant_v is None <=> the waveform is constant until up to the current entry
360
        constant_v = interp.constant_value((previous_t, previous_v), (t, v))
6✔
361

362
        for next_t, next_v, next_interp in input_iter:
6✔
363
            if next_t < t:
6✔
364
                if next_t < 0:
6✔
365
                    raise ValueError('Negative time values are not allowed.')
×
366
                else:
367
                    raise ValueError('Times are not increasing.')
6✔
368

369
            if constant_v is not None and interp.constant_value((t, v), (next_t, next_v)) != constant_v:
6✔
370
                constant_v = None
6✔
371

372
            if (previous_t != t or t != next_t) and (previous_v != v or v != next_v):
6✔
373
                # the time and the value differ both either from the next or the previous
374
                # otherwise we skip the entry
375
                previous_t = t
6✔
376
                previous_v = v
6✔
377
                output_waveform_table.append(TableWaveformEntry(t, v, interp))
6✔
378

379
            t, v, interp = next_t, next_v, next_interp
6✔
380

381
        # Until now, we only checked that the time does not decrease. We require an increase because duration == 0
382
        # waveforms are ill-formed. t is now the time of the last entry.
383
        if t == 0:
6✔
384
            raise ValueError('Last time entry is zero.')
6✔
385

386
        if constant_v is not None:
6✔
387
            # the waveform is constant
388
            return t, constant_v
6✔
389
        else:
390
            # we must still add the last entry to the table
391
            output_waveform_table.append(TableWaveformEntry(t, v, interp))
6✔
392
            return output_waveform_table
6✔
393

394
    def is_constant(self) -> bool:
6✔
395
        # only correct if `from_table` is used
396
        return False
6✔
397

398
    def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
6✔
399
        # only correct if `from_table` is used
400
        return None
6✔
401

402
    @classmethod
6✔
403
    def from_table(cls, channel: ChannelID, table: Sequence[EntryInInit]) -> Union['TableWaveform', 'ConstantWaveform']:
6✔
404
        table = cls._validate_input(table)
6✔
405
        if isinstance(table, tuple):
6✔
406
            duration, amplitude = table
6✔
407
            return ConstantWaveform(duration=duration, amplitude=amplitude, channel=channel)
6✔
408
        else:
409
            return TableWaveform(channel, tuple(table))
6✔
410

411
    @property
6✔
412
    def compare_key(self) -> Any:
6✔
413
        warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
×
414
                      DeprecationWarning, stacklevel=2)
415
        return self._channel_id, self._table
×
416

417
    def unsafe_sample(self,
6✔
418
                      channel: ChannelID,
419
                      sample_times: np.ndarray,
420
                      output_array: Union[np.ndarray, None] = None) -> np.ndarray:
421
        if output_array is None:
6✔
422
            output_array = _ALLOCATION_FUNCTION(sample_times, **_ALLOCATION_FUNCTION_KWARGS)
6✔
423

424
        if PULSE_TO_WAVEFORM_ERROR:
6✔
425
            # we need to replace the last entry's t with self.duration
426
            *entries, last = self._table
×
427
            entries.append(TableWaveformEntry(float(self.duration), last.v, last.interp))
×
428
        else:
429
            entries = self._table
6✔
430

431
        for entry1, entry2 in pairwise(entries):
6✔
432
            indices = slice(sample_times.searchsorted(entry1.t, 'left'),
6✔
433
                            sample_times.searchsorted(entry2.t, 'right'))
434
            output_array[indices] = \
6✔
435
                entry2.interp((float(entry1.t), entry1.v),
436
                              (float(entry2.t), entry2.v),
437
                              sample_times[indices])
438
        return output_array
6✔
439

440
    @property
6✔
441
    def defined_channels(self) -> AbstractSet[ChannelID]:
6✔
442
        return {self._channel_id}
6✔
443

444
    def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> 'Waveform':
6✔
445
        return self
6✔
446

447
    def __repr__(self):
6✔
448
        return f'{type(self).__name__}(channel={self._channel_id!r}, waveform_table={self._table!r})'
6✔
449

450

451
class ConstantWaveform(Waveform):
6✔
452

453
    # TODO: remove
454
    _is_constant_waveform = True
6✔
455

456
    __slots__ = ('_amplitude', '_channel')
6✔
457

458
    def __init__(self, duration: Real, amplitude: Any, channel: ChannelID):
6✔
459
        """ Create a qupulse waveform corresponding to a ConstantPulseTemplate """
460
        super().__init__(duration=_to_time_type(duration))
6✔
461
        if hasattr(amplitude, 'shape'):
6✔
462
            amplitude = amplitude[()]
6✔
463
            hash(amplitude)
6✔
464
        self._amplitude = amplitude
6✔
465
        self._channel = channel
6✔
466

467
    @classmethod
6✔
468
    def from_mapping(cls, duration: Real, constant_values: Mapping[ChannelID, float]) -> Union['ConstantWaveform',
6✔
469
                                                                                               'MultiChannelWaveform']:
470
        """Construct a ConstantWaveform or a MultiChannelWaveform of ConstantWaveforms with given duration and values"""
471
        assert constant_values
6✔
472
        duration = _to_time_type(duration)
6✔
473
        if len(constant_values) == 1:
6✔
474
            (channel, amplitude), = constant_values.items()
6✔
475
            return cls(duration, amplitude=amplitude, channel=channel)
6✔
476
        else:
477
            return MultiChannelWaveform([cls(duration, amplitude=amplitude, channel=channel)
6✔
478
                                         for channel, amplitude in constant_values.items()])
479

480
    def is_constant(self) -> bool:
6✔
481
        return True
6✔
482

483
    def constant_value(self, channel: ChannelID) -> Optional[float]:
6✔
484
        assert channel == self._channel
6✔
485
        return self._amplitude
6✔
486

487
    def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
6✔
488
        return {self._channel: self._amplitude}
6✔
489

490
    @property
6✔
491
    def defined_channels(self) -> AbstractSet[ChannelID]:
6✔
492
        """The channels this waveform should played on. Use
493
            :func:`~qupulse.pulses.instructions.get_measurement_windows` to get a waveform for a subset of these."""
494

495
        return {self._channel}
6✔
496

497
    @property
6✔
498
    def compare_key(self) -> Tuple[Any, ...]:
6✔
499
        warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
×
500
                      DeprecationWarning, stacklevel=2)
501
        return self._duration, self._amplitude, self._channel
×
502

503
    def unsafe_sample(self,
6✔
504
                      channel: ChannelID,
505
                      sample_times: np.ndarray,
506
                      output_array: Union[np.ndarray, None] = None) -> np.ndarray:
507
        if output_array is None:
6✔
508
            return np.full_like(sample_times, fill_value=self._amplitude, dtype=float)
6✔
509
        else:
510
            output_array[:] = self._amplitude
6✔
511
            return output_array
6✔
512

513
    def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> Waveform:
6✔
514
        """Unsafe version of :func:`~qupulse.pulses.instructions.get_measurement_windows`."""
515
        return self
6✔
516

517
    def __repr__(self):
6✔
518
        return f"{type(self).__name__}(duration={self.duration!r}, "\
6✔
519
               f"amplitude={self._amplitude!r}, channel={self._channel!r})"
520

521
    def reversed(self) -> 'Waveform':
6✔
522
        return self
6✔
523

524

525
class FunctionWaveform(Waveform):
6✔
526
    """Waveform obtained from instantiating a FunctionPulseTemplate."""
527

528
    __slots__ = ('_expression', '_channel_id')
6✔
529

530
    def __init__(self, expression: ExpressionScalar,
6✔
531
                 duration: float,
532
                 channel: ChannelID) -> None:
533
        """Creates a new FunctionWaveform instance.
534

535
        Args:
536
            expression: The function represented by this FunctionWaveform
537
                as a mathematical expression where 't' denotes the time variable. It must not have other variables
538
            duration: The duration of the waveform
539
            measurement_windows: A list of measurement windows
540
            channel: The channel this waveform is played on
541
        """
542

543
        if set(expression.variables) - set('t'):
6✔
544
            raise ValueError('FunctionWaveforms may not depend on anything but "t"')
6✔
545
        elif not expression.variables:
6✔
546
            warnings.warn("Constant FunctionWaveform is not recommended as the constant propagation will be suboptimal",
6✔
547
                          category=ConstantFunctionPulseTemplateWarning)
548
        super().__init__(duration=_to_time_type(duration))
6✔
549
        self._expression = expression
6✔
550
        self._channel_id = channel
6✔
551

552
    @classmethod
6✔
553
    def from_expression(cls, expression: ExpressionScalar, duration: float, channel: ChannelID) -> Union['FunctionWaveform', ConstantWaveform]:
6✔
554
        if expression.variables:
6✔
555
            return cls(expression, duration, channel)
6✔
556
        else:
557
            return ConstantWaveform(amplitude=expression.evaluate_numeric(), duration=duration, channel=channel)
6✔
558

559
    def is_constant(self) -> bool:
6✔
560
        # only correct if `from_expression` is used
561
        return False
6✔
562

563
    def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
6✔
564
        # only correct if `from_expression` is used
565
        return None
6✔
566

567
    @property
6✔
568
    def defined_channels(self) -> AbstractSet[ChannelID]:
6✔
569
        return {self._channel_id}
6✔
570

571
    @property
6✔
572
    def compare_key(self) -> Any:
6✔
573
        warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
×
574
                      DeprecationWarning, stacklevel=2)
575
        return self._channel_id, self._expression, self._duration
×
576

577
    @property
6✔
578
    def duration(self) -> TimeType:
6✔
579
        return self._duration
6✔
580

581
    def unsafe_sample(self,
6✔
582
                      channel: ChannelID,
583
                      sample_times: np.ndarray,
584
                      output_array: Union[np.ndarray, None] = None) -> np.ndarray:
585
        evaluated = self._expression.evaluate_numeric(t=sample_times)
6✔
586
        if output_array is None:
6✔
587
            if self._expression.variables:
6✔
588
                return evaluated.astype(float)
6✔
589
            else:
590
                return np.full_like(sample_times, fill_value=float(evaluated), dtype=float)
6✔
591
        else:
592
            output_array[:] = evaluated
6✔
593
            return output_array
6✔
594

595
    def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> Waveform:
6✔
596
        return self
6✔
597

598
    def __repr__(self):
6✔
599
        return f"{type(self).__name__}(duration={self.duration!r}, "\
6✔
600
               f"expression={self._expression!r}, channel={self._channel_id!r})"
601

602

603
class SequenceWaveform(Waveform):
6✔
604
    """This class allows putting multiple PulseTemplate together in one waveform on the hardware."""
605

606
    __slots__ = ('_sequenced_waveforms', )
6✔
607

608
    def __init__(self, sub_waveforms: Iterable[Waveform]):
6✔
609
        """Use Waveform.from_sequence for optimal construction
610

611
        :param subwaveforms: All waveforms must have the same defined channels
612
        """
613
        if not sub_waveforms:
6✔
614
            raise ValueError(
6✔
615
                "SequenceWaveform cannot be constructed without channel waveforms."
616
            )
617

618
        # do not fail on iterators although we do not allow them as an argument
619
        sequenced_waveforms = tuple(sub_waveforms)
6✔
620

621
        super().__init__(duration=sum(waveform.duration for waveform in sequenced_waveforms))
6✔
622
        self._sequenced_waveforms = sequenced_waveforms
6✔
623

624
        defined_channels = self._sequenced_waveforms[0].defined_channels
6✔
625
        if not all(waveform.defined_channels == defined_channels
6✔
626
                   for waveform in itertools.islice(self._sequenced_waveforms, 1, None)):
627
            for waveform in self._sequenced_waveforms[1:]:
6✔
628
                 if not waveform.defined_channels == self.defined_channels:
6✔
629
                     print(f"SequenceWaveform: defined channels {self.defined_channels} do not match {waveform.defined_channels} ")
6✔
630
            raise ValueError(
6✔
631
                "SequenceWaveform cannot be constructed from waveforms of different"
632
                "defined channels."
633
            )
634

635
    @classmethod
6✔
636
    def from_sequence(cls, waveforms: Sequence['Waveform']) -> 'Waveform':
6✔
637
        """Returns a waveform the represents the given sequence of waveforms. Applies some optimizations."""
638
        assert waveforms, "Sequence must not be empty"
6✔
639
        if len(waveforms) == 1:
6✔
640
            return waveforms[0]
6✔
641

642
        flattened = []
6✔
643
        constant_values = waveforms[0].constant_value_dict()
6✔
644
        for wf in waveforms:
6✔
645
            if constant_values and constant_values != wf.constant_value_dict():
6✔
646
                constant_values = None
6✔
647
            if isinstance(wf, cls):
6✔
648
                flattened.extend(wf.sequenced_waveforms)
6✔
649
            else:
650
                flattened.append(wf)
6✔
651
        if constant_values is None:
6✔
652
            return cls(sub_waveforms=flattened)
6✔
653
        else:
654
            duration = sum(wf.duration for wf in flattened)
6✔
655
            return ConstantWaveform.from_mapping(duration, constant_values)
6✔
656

657
    def is_constant(self) -> bool:
6✔
658
        # only correct if from_sequence is used for construction
659
        return False
6✔
660

661
    def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
6✔
662
        # only correct if from_sequence is used for construction
663
        return None
6✔
664

665
    def constant_value(self, channel: ChannelID) -> Optional[float]:
6✔
666
        v = None
6✔
667
        for wf in self._sequenced_waveforms:
6✔
668
            wf_cv = wf.constant_value(channel)
6✔
669
            if wf_cv is None:
6✔
670
                return None
6✔
671
            elif wf_cv == v:
6✔
672
                continue
×
673
            elif v is None:
6✔
674
                v = wf_cv
6✔
675
            else:
676
                assert v != wf_cv
6✔
677
                return None
6✔
678
        return v
×
679

680
    @property
6✔
681
    def defined_channels(self) -> AbstractSet[ChannelID]:
6✔
682
        return self._sequenced_waveforms[0].defined_channels
6✔
683

684
    def unsafe_sample(self,
6✔
685
                      channel: ChannelID,
686
                      sample_times: np.ndarray,
687
                      output_array: Union[np.ndarray, None] = None) -> np.ndarray:
688
        if output_array is None:
6✔
689
            output_array = _ALLOCATION_FUNCTION(sample_times, **_ALLOCATION_FUNCTION_KWARGS)
6✔
690
        time = 0
6✔
691
        for subwaveform in self._sequenced_waveforms:
6✔
692
            # before you change anything here, make sure to understand the difference between basic and advanced
693
            # indexing in numpy and their copy/reference behaviour
694
            end = time + subwaveform.duration
6✔
695

696
            indices = slice(*sample_times.searchsorted((float(time), float(end)), 'left'))
6✔
697
            subwaveform.unsafe_sample(channel=channel,
6✔
698
                                      sample_times=sample_times[indices]-np.float64(time),
699
                                      output_array=output_array[indices])
700
            time = end
6✔
701
        return output_array
6✔
702

703
    @property
6✔
704
    def compare_key(self) -> Tuple[Waveform]:
6✔
705
        warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
×
706
                      DeprecationWarning, stacklevel=2)
707
        return self._sequenced_waveforms
×
708

709
    @property
6✔
710
    def duration(self) -> TimeType:
6✔
711
        return self._duration
6✔
712

713
    def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> 'Waveform':
6✔
714
        return SequenceWaveform.from_sequence([
6✔
715
            sub_waveform.unsafe_get_subset_for_channels(channels & sub_waveform.defined_channels)
716
            for sub_waveform in self._sequenced_waveforms if sub_waveform.defined_channels & channels])
717

718
    @property
6✔
719
    def sequenced_waveforms(self) -> Sequence[Waveform]:
6✔
720
        return self._sequenced_waveforms
6✔
721

722
    def __repr__(self):
6✔
723
        return f"{type(self).__name__}({self._sequenced_waveforms})"
6✔
724

725

726
class MultiChannelWaveform(Waveform):
6✔
727
    """A MultiChannelWaveform is a Waveform object that allows combining arbitrary Waveform objects
728
    to into a single waveform defined for several channels.
729

730
    The number of channels used by the MultiChannelWaveform object is the sum of the channels used
731
    by the Waveform objects it consists of.
732

733
    MultiChannelWaveform allows an arbitrary mapping of channels defined by the Waveforms it
734
    consists of and the channels it defines. For example, if the MultiChannelWaveform consists
735
    of a two Waveform objects A and B which define two channels each, then the channels of the
736
    MultiChannelWaveform may be 0: A.1, 1: B.0, 2: B.1, 3: A.0 where A.0 means channel 0 of Waveform
737
    object A.
738

739
    The following constraints must hold:
740
     - The durations of all Waveform objects must be equal.
741
     - The channel mapping must be sane, i.e., no channel of the MultiChannelWaveform must be
742
        assigned more than one channel of any Waveform object it consists of
743
    """
744

745
    __slots__ = ('_sub_waveforms', '_defined_channels')
6✔
746

747
    def __init__(self, sub_waveforms: List[Waveform]) -> None:
6✔
748
        """Create a new MultiChannelWaveform instance.
749
        Use `MultiChannelWaveform.from_parallel` for optimal construction.
750

751
        Requires a list of subwaveforms in the form (Waveform, List(int)) where the list defines
752
        the channel mapping, i.e., a value y at index x in the list means that channel x of the
753
        subwaveform will be mapped to channel y of this MultiChannelWaveform object.
754

755
        Args:
756
            sub_waveforms: The list of sub waveforms of this
757
                MultiChannelWaveform. List might get sorted!
758
        Raises:
759
            ValueError, if a channel mapping is out of bounds of the channels defined by this
760
                MultiChannelWaveform
761
            ValueError, if several subwaveform channels are assigned to a single channel of this
762
                MultiChannelWaveform
763
            ValueError, if subwaveforms have inconsistent durations
764
        """
765

766
        if not sub_waveforms:
6✔
767
            raise ValueError(
6✔
768
                "MultiChannelWaveform cannot be constructed without channel waveforms."
769
            )
770

771
        # sort the waveforms with their defined channels to make compare key reproducible
772
        if not isinstance(sub_waveforms, list):
6✔
773
            sub_waveforms = list(sub_waveforms)
6✔
774
        sub_waveforms.sort(key=lambda wf: wf._sort_key_for_channels())
6✔
775

776
        super().__init__(duration=sub_waveforms[0].duration)
6✔
777
        self._sub_waveforms = tuple(sub_waveforms)
6✔
778

779
        defined_channels = set()
6✔
780
        for waveform in self._sub_waveforms:
6✔
781
            if waveform.defined_channels & defined_channels:
6✔
782
                raise ValueError('Channel may not be defined in multiple waveforms',
6✔
783
                                 waveform.defined_channels & defined_channels)
784
            defined_channels |= waveform.defined_channels
6✔
785
        self._defined_channels = frozenset(defined_channels)
6✔
786

787
        if not all(isclose(waveform.duration, self.duration) for waveform in self._sub_waveforms[1:]):
6✔
788
            # meaningful error message:
789
            durations = {}
6✔
790

791
            for waveform in self._sub_waveforms:
6✔
792
                for duration, channels in durations.items():
6✔
793
                    if isclose(waveform.duration, duration):
6✔
794
                        channels.update(waveform.defined_channels)
6✔
795
                        break
6✔
796
                else:
797
                    durations[waveform.duration] = set(waveform.defined_channels)
6✔
798

799
            raise ValueError(
6✔
800
                "MultiChannelWaveform cannot be constructed from channel waveforms of different durations.",
801
                durations
802
            )
803

804
    @staticmethod
6✔
805
    def from_parallel(waveforms: Sequence[Waveform]) -> Waveform:
6✔
806
        assert waveforms, "ARgument must not be empty"
6✔
807
        if len(waveforms) == 1:
6✔
808
            return waveforms[0]
6✔
809

810
        # we do not look at constant values here because there is no benefit. We would need to construct a new
811
        # MultiChannelWaveform anyways
812

813
        # avoid unnecessary multi channel nesting
814
        flattened = []
6✔
815
        for waveform in waveforms:
6✔
816
            if isinstance(waveform, MultiChannelWaveform):
6✔
817
                flattened.extend(waveform._sub_waveforms)
6✔
818
            else:
819
                flattened.append(waveform)
6✔
820

821
        return MultiChannelWaveform(flattened)
6✔
822

823
    def is_constant(self) -> bool:
6✔
824
        return all(wf.is_constant() for wf in self._sub_waveforms)
6✔
825

826
    def constant_value(self, channel: ChannelID) -> Optional[float]:
6✔
827
        return self[channel].constant_value(channel)
6✔
828

829
    def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
6✔
830
        d = {}
6✔
831
        for wf in self._sub_waveforms:
6✔
832
            wf_d = wf.constant_value_dict()
6✔
833
            if wf_d is None:
6✔
834
                return None
6✔
835
            else:
836
                d.update(wf_d)
6✔
837
        return d
6✔
838

839
    @property
6✔
840
    def duration(self) -> TimeType:
6✔
841
        return self._sub_waveforms[0].duration
6✔
842

843
    def __getitem__(self, key: ChannelID) -> Waveform:
6✔
844
        for waveform in self._sub_waveforms:
6✔
845
            if key in waveform.defined_channels:
6✔
846
                return waveform
6✔
847
        raise KeyError('Unknown channel ID: {}'.format(key), key)
6✔
848

849
    @property
6✔
850
    def defined_channels(self) -> AbstractSet[ChannelID]:
6✔
851
        return self._defined_channels
6✔
852

853
    @property
6✔
854
    def compare_key(self) -> Any:
6✔
855
        warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
×
856
                      DeprecationWarning, stacklevel=2)
857
        return self._sub_waveforms
×
858

859
    def unsafe_sample(self,
6✔
860
                      channel: ChannelID,
861
                      sample_times: np.ndarray,
862
                      output_array: Union[np.ndarray, None] = None) -> np.ndarray:
863
        return self[channel].unsafe_sample(channel, sample_times, output_array)
6✔
864

865
    def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> 'Waveform':
6✔
866
        relevant_sub_waveforms = [swf for swf in self._sub_waveforms if swf.defined_channels & channels]
6✔
867
        if len(relevant_sub_waveforms) == 1:
6✔
868
            return relevant_sub_waveforms[0].get_subset_for_channels(channels)
6✔
869
        elif len(relevant_sub_waveforms) > 1:
6✔
870
            return MultiChannelWaveform.from_parallel(
6✔
871
                [sub_waveform.get_subset_for_channels(channels & sub_waveform.defined_channels)
872
                 for sub_waveform in relevant_sub_waveforms])
873
        else:
874
            raise KeyError('Unknown channels: {}'.format(channels))
6✔
875

876
    def __repr__(self):
6✔
877
        return f"{type(self).__name__}({self._sub_waveforms!r})"
6✔
878

879

880
class RepetitionWaveform(Waveform):
6✔
881
    """This class allows putting multiple PulseTemplate together in one waveform on the hardware."""
882

883
    __slots__ = ('_body', '_repetition_count')
6✔
884

885
    def __init__(self, body: Waveform, repetition_count: int):
6✔
886
        repetition_count = checked_int_cast(repetition_count)
6✔
887
        if repetition_count < 1 or not isinstance(repetition_count, int):
6✔
888
            raise ValueError('Repetition count must be an integer >0')
6✔
889

890
        super().__init__(duration=body.duration * repetition_count)
6✔
891
        self._body = body
6✔
892
        self._repetition_count = repetition_count
6✔
893

894
    @classmethod
6✔
895
    def from_repetition_count(cls, body: Waveform, repetition_count: int) -> Waveform:
6✔
896
        constant_values = body.constant_value_dict()
6✔
897
        if constant_values is None:
6✔
898
            return RepetitionWaveform(body, repetition_count)
6✔
899
        else:
900
            return ConstantWaveform.from_mapping(body.duration * repetition_count, constant_values)
6✔
901

902
    @property
6✔
903
    def defined_channels(self) -> AbstractSet[ChannelID]:
6✔
904
        return self._body.defined_channels
6✔
905

906
    def unsafe_sample(self,
6✔
907
                      channel: ChannelID,
908
                      sample_times: np.ndarray,
909
                      output_array: Union[np.ndarray, None] = None) -> np.ndarray:
910
        if output_array is None:
6✔
911
            output_array = _ALLOCATION_FUNCTION(sample_times, **_ALLOCATION_FUNCTION_KWARGS)
6✔
912
        body_duration = self._body.duration
6✔
913
        time = 0
6✔
914
        for _ in range(self._repetition_count):
6✔
915
            end = time + body_duration
6✔
916
            indices = slice(*sample_times.searchsorted((float(time), float(end)), 'left'))
6✔
917
            self._body.unsafe_sample(channel=channel,
6✔
918
                                     sample_times=sample_times[indices] - float(time),
919
                                     output_array=output_array[indices])
920
            time = end
6✔
921
        return output_array
6✔
922

923
    @property
6✔
924
    def compare_key(self) -> Tuple[Any, int]:
6✔
925
        warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
×
926
                      DeprecationWarning, stacklevel=2)
927
        return self._body.compare_key, self._repetition_count
×
928

929
    def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> Waveform:
6✔
930
        return RepetitionWaveform.from_repetition_count(
6✔
931
            body=self._body.unsafe_get_subset_for_channels(channels),
932
            repetition_count=self._repetition_count)
933

934
    def is_constant(self) -> bool:
6✔
935
        return self._body.is_constant()
6✔
936

937
    def constant_value(self, channel: ChannelID) -> Optional[float]:
6✔
938
        return self._body.constant_value(channel)
6✔
939

940
    def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
6✔
941
        return self._body.constant_value_dict()
6✔
942

943
    def __repr__(self):
6✔
944
        return f"{type(self).__name__}(body={self._body!r}, repetition_count={self._repetition_count!r})"
6✔
945

946

947
class TransformingWaveform(Waveform):
6✔
948
    __slots__ = ('_inner_waveform', '_transformation', '_cached_data', '_cached_times')
6✔
949

950
    def __init__(self, inner_waveform: Waveform, transformation: Transformation):
6✔
951
        """"""
952
        super(TransformingWaveform, self).__init__(duration=inner_waveform.duration)
6✔
953
        self._inner_waveform = inner_waveform
6✔
954
        self._transformation = transformation
6✔
955

956
        # cache data of inner channels based identified and invalidated by the sample times
957
        self._cached_data = None
6✔
958
        self._cached_times = lambda: None
6✔
959

960
    def __hash__(self):
6✔
961
        return hash((self._inner_waveform, self._transformation))
×
962

963
    def __eq__(self, other):
6✔
964
        if getattr(other, '__slots__', None) is self.__slots__:
6✔
965
            return self._inner_waveform == other._inner_waveform and self._transformation == other._transformation
6✔
966
        return NotImplemented
×
967

968
    @classmethod
6✔
969
    def from_transformation(cls, inner_waveform: Waveform, transformation: Transformation) -> Waveform:
6✔
970
        constant_values = inner_waveform.constant_value_dict()
6✔
971

972
        if constant_values is None or not transformation.is_constant_invariant():
6✔
973
            return cls(inner_waveform, transformation)
6✔
974

975
        transformed_constant_values = {key: value for key, value in transformation(0., constant_values).items()}
6✔
976
        return ConstantWaveform.from_mapping(inner_waveform.duration, transformed_constant_values)
6✔
977

978
    def is_constant(self) -> bool:
6✔
979
        # only true if `from_transformation` was used
980
        return False
6✔
981

982
    def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
6✔
983
        # only true if `from_transformation` was used
984
        return None
6✔
985

986
    def constant_value(self, channel: ChannelID) -> Optional[float]:
6✔
987
        if not self._transformation.is_constant_invariant():
6✔
988
            return None
6✔
989
        in_channels = self._transformation.get_input_channels({channel})
6✔
990
        in_values = {ch: self._inner_waveform.constant_value(ch) for ch in in_channels}
6✔
991
        if any(val is None for val in in_values.values()):
6✔
992
            return None
6✔
993
        else:
994
            return self._transformation(0., in_values)[channel]
6✔
995

996
    @property
6✔
997
    def inner_waveform(self) -> Waveform:
6✔
998
        return self._inner_waveform
6✔
999

1000
    @property
6✔
1001
    def transformation(self) -> Transformation:
6✔
1002
        return self._transformation
6✔
1003

1004
    @property
6✔
1005
    def defined_channels(self) -> AbstractSet[ChannelID]:
6✔
1006
        return self.transformation.get_output_channels(self.inner_waveform.defined_channels)
6✔
1007

1008
    @property
6✔
1009
    def compare_key(self) -> Tuple[Waveform, Transformation]:
6✔
1010
        warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
6✔
1011
                      DeprecationWarning, stacklevel=2)
1012
        return self.inner_waveform, self.transformation
6✔
1013

1014
    def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> 'SubsetWaveform':
6✔
1015
        return SubsetWaveform(self, channel_subset=channels)
6✔
1016

1017
    def unsafe_sample(self,
6✔
1018
                      channel: ChannelID,
1019
                      sample_times: np.ndarray,
1020
                      output_array: Union[np.ndarray, None] = None) -> np.ndarray:
1021
        if self._cached_times() is not sample_times:
6✔
1022
            self._cached_data = dict()
6✔
1023
            self._cached_times = ref(sample_times)
6✔
1024

1025
        if channel not in self._cached_data:
6✔
1026

1027
            inner_channels = self.transformation.get_input_channels({channel})
6✔
1028

1029
            inner_data = {inner_channel: self.inner_waveform.unsafe_sample(inner_channel, sample_times)
6✔
1030
                          for inner_channel in inner_channels}
1031

1032
            outer_data = self.transformation(sample_times, inner_data)
6✔
1033
            self._cached_data.update(outer_data)
6✔
1034

1035
        if output_array is None:
6✔
1036
            output_array = self._cached_data[channel]
6✔
1037
        else:
1038
            output_array[:] = self._cached_data[channel]
6✔
1039

1040
        return output_array
6✔
1041

1042

1043
class SubsetWaveform(Waveform):
6✔
1044
    __slots__ = ('_inner_waveform', '_channel_subset')
6✔
1045

1046
    def __init__(self, inner_waveform: Waveform, channel_subset: Set[ChannelID]):
6✔
1047
        super().__init__(duration=inner_waveform.duration)
6✔
1048
        self._inner_waveform = inner_waveform
6✔
1049
        self._channel_subset = frozenset(channel_subset)
6✔
1050

1051
    @property
6✔
1052
    def inner_waveform(self) -> Waveform:
6✔
1053
        return self._inner_waveform
6✔
1054

1055
    @property
6✔
1056
    def defined_channels(self) -> FrozenSet[ChannelID]:
6✔
1057
        return self._channel_subset
6✔
1058

1059
    @property
6✔
1060
    def compare_key(self) -> Tuple[frozenset, Waveform]:
6✔
1061
        warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
6✔
1062
                      DeprecationWarning, stacklevel=2)
1063
        return self.defined_channels, self.inner_waveform
6✔
1064

1065
    def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> Waveform:
6✔
1066
        return self.inner_waveform.get_subset_for_channels(channels)
6✔
1067

1068
    def unsafe_sample(self,
6✔
1069
                      channel: ChannelID,
1070
                      sample_times: np.ndarray,
1071
                      output_array: Union[np.ndarray, None] = None) -> np.ndarray:
1072
        return self.inner_waveform.unsafe_sample(channel, sample_times, output_array)
6✔
1073

1074
    def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
6✔
1075
        d = self._inner_waveform.constant_value_dict()
×
1076
        if d is not None:
×
1077
            return {ch: d[ch] for ch in self._channel_subset}
×
1078

1079
    def constant_value(self, channel: ChannelID) -> Optional[float]:
6✔
1080
        if channel not in self._channel_subset:
6✔
1081
            raise KeyError(channel)
×
1082
        return self._inner_waveform.constant_value(channel)
6✔
1083

1084

1085
class ArithmeticWaveform(Waveform):
6✔
1086
    """Channels only present in one waveform have the operations neutral element on the other."""
1087

1088
    numpy_operator_map = {'+': np.add,
6✔
1089
                          '-': np.subtract}
1090
    operator_map = {'+': operator.add,
6✔
1091
                    '-': operator.sub}
1092

1093
    rhs_only_map = {'+': operator.pos,
6✔
1094
                    '-': operator.neg}
1095
    numpy_rhs_only_map = {'+': np.positive,
6✔
1096
                          '-': np.negative}
1097

1098
    __slots__ = ('_lhs', '_rhs', '_arithmetic_operator')
6✔
1099

1100
    def __init__(self,
6✔
1101
                 lhs: Waveform,
1102
                 arithmetic_operator: str,
1103
                 rhs: Waveform):
1104
        super().__init__(duration=lhs.duration)
6✔
1105
        self._lhs = lhs
6✔
1106
        self._rhs = rhs
6✔
1107
        self._arithmetic_operator = arithmetic_operator
6✔
1108

1109
        assert np.isclose(float(self._lhs.duration), float(self._rhs.duration))
6✔
1110
        assert arithmetic_operator in self.operator_map
6✔
1111

1112
    @classmethod
6✔
1113
    def from_operator(cls, lhs: Waveform, arithmetic_operator: str, rhs: Waveform):
6✔
1114
        # one could optimize rhs_cv to being only created if lhs_cv is not None but this makes the code harder to read
1115
        lhs_cv = lhs.constant_value_dict()
6✔
1116
        rhs_cv = rhs.constant_value_dict()
6✔
1117
        if lhs_cv is None or rhs_cv is None:
6✔
1118
            return cls(lhs, arithmetic_operator, rhs)
6✔
1119

1120
        else:
1121
            constant_values = dict(lhs_cv)
6✔
1122
            op = cls.operator_map[arithmetic_operator]
6✔
1123
            rhs_op = cls.rhs_only_map[arithmetic_operator]
6✔
1124

1125
            for ch, rhs_val in rhs_cv.items():
6✔
1126
                if ch in constant_values:
6✔
1127
                    constant_values[ch] = op(constant_values[ch], rhs_val)
6✔
1128
                else:
1129
                    constant_values[ch] = rhs_op(rhs_val)
6✔
1130

1131
            duration = lhs.duration
6✔
1132
            assert isclose(duration, rhs.duration)
6✔
1133

1134
            return ConstantWaveform.from_mapping(duration, constant_values)
6✔
1135

1136
    def constant_value(self, channel: ChannelID) -> Optional[float]:
6✔
1137
        if channel not in self._rhs.defined_channels:
6✔
1138
            return self._lhs.constant_value(channel)
6✔
1139
        rhs = self._rhs.constant_value(channel)
6✔
1140
        if rhs is None:
6✔
1141
            return None
6✔
1142

1143
        if channel in self._lhs.defined_channels:
6✔
1144
            lhs = self._lhs.constant_value(channel)
6✔
1145
            if lhs is None:
6✔
1146
                return None
6✔
1147

1148
            return self.operator_map[self._arithmetic_operator](lhs, rhs)
6✔
1149
        else:
1150
            return self.rhs_only_map[self._arithmetic_operator](rhs)
6✔
1151

1152
    def is_constant(self) -> bool:
6✔
1153
        # only correct if from_operator is used
1154
        return False
6✔
1155

1156
    def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
6✔
1157
        # only correct if from_operator is used
1158
        return None
6✔
1159

1160
    @property
6✔
1161
    def lhs(self) -> Waveform:
6✔
1162
        return self._lhs
6✔
1163

1164
    @property
6✔
1165
    def rhs(self) -> Waveform:
6✔
1166
        return self._rhs
6✔
1167

1168
    @property
6✔
1169
    def arithmetic_operator(self) -> str:
6✔
1170
        return self._arithmetic_operator
6✔
1171

1172
    @property
6✔
1173
    def duration(self) -> TimeType:
6✔
1174
        return self._lhs.duration
6✔
1175

1176
    @property
6✔
1177
    def defined_channels(self) -> AbstractSet[ChannelID]:
6✔
1178
        return self._lhs.defined_channels | self._rhs.defined_channels
6✔
1179

1180
    def unsafe_sample(self,
6✔
1181
                      channel: ChannelID,
1182
                      sample_times: np.ndarray,
1183
                      output_array: Union[np.ndarray, None] = None) -> np.ndarray:
1184
        if channel in self._lhs.defined_channels:
6✔
1185
            lhs = self._lhs.unsafe_sample(channel=channel, sample_times=sample_times, output_array=output_array)
6✔
1186
        else:
1187
            lhs = None
6✔
1188

1189
        if channel in self._rhs.defined_channels:
6✔
1190
            rhs = self._rhs.unsafe_sample(channel=channel, sample_times=sample_times,
6✔
1191
                                          output_array=None if lhs is not None else output_array)
1192
        else:
1193
            rhs = None
6✔
1194

1195
        if rhs is not None and lhs is not None:
6✔
1196
            arithmetic_operator = self.numpy_operator_map[self._arithmetic_operator]
6✔
1197
            if output_array is None:
6✔
1198
                output_array = lhs
6✔
1199
            return arithmetic_operator(lhs, rhs, out=output_array)
6✔
1200

1201
        else:
1202
            if lhs is None:
6✔
1203
                assert rhs is not None, "channel %r not in defined channels (internal bug)" % channel
6✔
1204
                return self.numpy_rhs_only_map[self._arithmetic_operator](rhs, out=output_array)
6✔
1205
            else:
1206
                return lhs
6✔
1207

1208
    def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> Waveform:
6✔
1209
        # TODO: optimization possible
1210
        return SubsetWaveform(self, channels)
6✔
1211

1212
    @property
6✔
1213
    def compare_key(self) -> Tuple[str, Waveform, Waveform]:
6✔
1214
        warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
6✔
1215
                      DeprecationWarning, stacklevel=2)
1216
        return self._arithmetic_operator, self._lhs, self._rhs
6✔
1217

1218

1219
class FunctorWaveform(Waveform):
6✔
1220
    # TODO: Use Protocol to enforce that it accepts second argument has the keyword out
1221
    Functor = callable
6✔
1222

1223
    __slots__ = ('_inner_waveform', '_functor')
6✔
1224

1225
    """Apply a channel wise functor that works inplace to all results. The functor must accept two arguments"""
6✔
1226
    def __init__(self, inner_waveform: Waveform, functor: Mapping[ChannelID, Functor]):
6✔
1227
        super(FunctorWaveform, self).__init__(duration=inner_waveform.duration)
6✔
1228
        self._inner_waveform = inner_waveform
6✔
1229
        self._functor = dict(functor.items())
6✔
1230

1231
        assert set(functor.keys()) == inner_waveform.defined_channels, ("There is no default identity mapping (yet)."
6✔
1232
                                                                        "File an issue on github if you need it.")
1233

1234
    @classmethod
6✔
1235
    def from_functor(cls, inner_waveform: Waveform, functor: Mapping[ChannelID, Functor]):
6✔
1236
        constant_values = inner_waveform.constant_value_dict()
6✔
1237
        if constant_values is None:
6✔
1238
            return FunctorWaveform(inner_waveform, functor)
6✔
1239

1240
        funced_constant_values = {ch: functor[ch](val) for ch, val in constant_values.items()}
6✔
1241
        return ConstantWaveform.from_mapping(inner_waveform.duration, funced_constant_values)
6✔
1242

1243
    def is_constant(self) -> bool:
6✔
1244
        # only correct if `from_functor` was used
1245
        return False
6✔
1246

1247
    def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
6✔
1248
        # only correct if `from_functor` was used
1249
        return None
6✔
1250

1251
    def constant_value(self, channel: ChannelID) -> Optional[float]:
6✔
1252
        inner = self._inner_waveform.constant_value(channel)
6✔
1253
        if inner is None:
6✔
1254
            return None
6✔
1255
        else:
1256
            return self._functor[channel](inner)
6✔
1257

1258
    @property
6✔
1259
    def defined_channels(self) -> AbstractSet[ChannelID]:
6✔
1260
        return self._inner_waveform.defined_channels
6✔
1261

1262
    def unsafe_sample(self,
6✔
1263
                      channel: ChannelID,
1264
                      sample_times: np.ndarray,
1265
                      output_array: Union[np.ndarray, None] = None) -> np.ndarray:
1266
        inner_output = self._inner_waveform.unsafe_sample(channel, sample_times, output_array)
6✔
1267
        return self._functor[channel](inner_output, out=inner_output)
6✔
1268

1269
    def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> Waveform:
6✔
1270
        return FunctorWaveform.from_functor(
6✔
1271
            self._inner_waveform.unsafe_get_subset_for_channels(channels),
1272
            {ch: self._functor[ch] for ch in channels})
1273

1274
    @property
6✔
1275
    def compare_key(self) -> Tuple[Waveform, FrozenSet]:
6✔
1276
        warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
6✔
1277
                      DeprecationWarning, stacklevel=2)
1278
        return self._inner_waveform, frozenset(self._functor.items())
6✔
1279

1280

1281
class ReversedWaveform(Waveform):
6✔
1282
    """Reverses the inner waveform in time."""
1283

1284
    __slots__ = ('_inner',)
6✔
1285

1286
    def __init__(self, inner: Waveform):
6✔
1287
        super().__init__(duration=inner.duration)
6✔
1288
        self._inner = inner
6✔
1289

1290
    @classmethod
6✔
1291
    def from_to_reverse(cls, inner: Waveform) -> Waveform:
6✔
1292
        if inner.constant_value_dict():
×
1293
            return inner
×
1294
        else:
1295
            return cls(inner)
×
1296

1297
    def unsafe_sample(self, channel: ChannelID, sample_times: np.ndarray,
6✔
1298
                      output_array: Union[np.ndarray, None] = None) -> np.ndarray:
1299
        inner_sample_times = (float(self.duration) - sample_times)[::-1]
6✔
1300
        if output_array is None:
6✔
1301
            return self._inner.unsafe_sample(channel, inner_sample_times, None)[::-1]
6✔
1302
        else:
1303
            inner_output_array = output_array[::-1]
6✔
1304
            inner_output_array = self._inner.unsafe_sample(channel, inner_sample_times, output_array=inner_output_array)
6✔
1305
            if id(inner_output_array.base) not in (id(output_array), id(output_array.base)):
6✔
1306
                # TODO: is there a guarantee by numpy we never end up here?
1307
                output_array[:] = inner_output_array[::-1]
×
1308
            return output_array
6✔
1309

1310
    @property
6✔
1311
    def defined_channels(self) -> AbstractSet[ChannelID]:
6✔
1312
        return self._inner.defined_channels
6✔
1313

1314
    def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> 'Waveform':
6✔
1315
        return ReversedWaveform.from_to_reverse(self._inner.unsafe_get_subset_for_channels(channels))
×
1316

1317
    @property
6✔
1318
    def compare_key(self) -> Hashable:
6✔
1319
        warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
6✔
1320
                      DeprecationWarning, stacklevel=2)
1321
        return self._inner.compare_key
6✔
1322

1323
    def reversed(self) -> 'Waveform':
6✔
1324
        return self._inner
×
1325

1326
    def __repr__(self):
6✔
1327
        return f"ReversedWaveform(inner={self._inner!r})"
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc