From 5052cb809474e9e5ca0f92a36a482b25b02c1d4b Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Thu, 28 Aug 2025 10:11:19 +0200 Subject: [PATCH 1/5] avoid string expr in to_next_multiple --- qupulse/utils/__init__.py | 23 ++++++++++++++++++++--- tests/utils/utils_tests.py | 32 +++++++++++++++++--------------- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/qupulse/utils/__init__.py b/qupulse/utils/__init__.py index 8655cd15..3ad983cc 100644 --- a/qupulse/utils/__init__.py +++ b/qupulse/utils/__init__.py @@ -13,6 +13,7 @@ from qupulse.expressions import ExpressionScalar, ExpressionLike import numpy +import sympy as sp try: from math import isclose @@ -151,6 +152,22 @@ def to_next_multiple(sample_rate: ExpressionLike, quantum: int, #double negative for ceil division. return lambda duration: -(-(duration*sample_rate)//quantum) * (quantum/sample_rate) else: - #still return 0 if duration==0 - return lambda duration: ExpressionScalar(f'{quantum}/({sample_rate})*Max({min_quanta},-(-({duration})*{sample_rate}//{quantum}))*Max(0, sign({duration}))') - \ No newline at end of file + qI = sp.Integer(quantum) + k = qI / sample_rate # factor to go from #quanta -> duration + mqI = sp.Integer(min_quanta) + + def _build_sym(d): + u = d*sample_rate/qI # "duration in quanta" (real) + ce = sp.ceiling(u) # number of quanta after rounding up + + # Enforce: 0 if d <= 0; else at least mqI quanta. + # max(mqI, ceil(u)) <=> mqI if u <= mqI, else ceil(u) + # do not evaluate right now because parameters could still be variable, + # then it's just overhead. + return sp.Piecewise( + (0, sp.Le(d, 0)), + (k*mqI, sp.Le(u, mqI)), + (k*ce, True) + , evaluate=False) + + return lambda duration: ExpressionScalar(_build_sym(duration)) \ No newline at end of file diff --git a/tests/utils/utils_tests.py b/tests/utils/utils_tests.py index 3675109a..8c7d5c04 100644 --- a/tests/utils/utils_tests.py +++ b/tests/utils/utils_tests.py @@ -108,6 +108,7 @@ class ToNextMultipleTests(unittest.TestCase): def test_to_next_multiple(self): from qupulse.utils.types import TimeType from qupulse.expressions import ExpressionScalar + precision_digits = 12 duration = TimeType.from_float(47.1415926535) evaluated = to_next_multiple(sample_rate=TimeType.from_float(2.4),quantum=16)(duration) @@ -120,31 +121,32 @@ def test_to_next_multiple(self): self.assertEqual(evaluated, expected) duration = 6185240.0000001 - evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=13)(duration) + evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=13)(duration).evaluate_numeric() expected = 6185248 - self.assertEqual(evaluated, expected) + self.assertAlmostEqual(evaluated, expected, precision_digits) + + duration = 63.99 + evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=4)(duration).evaluate_numeric() + expected = 64 + self.assertAlmostEqual(evaluated, expected, precision_digits) + + duration = 64.01 + evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=4)(duration).evaluate_numeric() + expected = 80 + self.assertAlmostEqual(evaluated, expected, precision_digits) duration = 0. - evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=13)(duration) + evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=13)(duration).evaluate_numeric() expected = 0. - self.assertEqual(evaluated, expected) + self.assertAlmostEqual(evaluated, expected, precision_digits) duration = ExpressionScalar('abc') evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=13)(duration).evaluate_in_scope(dict(abc=0.)) expected = 0. - self.assertEqual(evaluated, expected) + self.assertAlmostEqual(evaluated, expected, precision_digits) duration = ExpressionScalar('q') evaluated = to_next_multiple(sample_rate=ExpressionScalar('w'),quantum=16,min_quanta=1)(duration).evaluate_in_scope( dict(q=3.14159,w=1.0)) expected = 16. - self.assertEqual(evaluated, expected) - - #bracket silent bug - duration = ExpressionScalar('51 + q*51') - evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=1)(duration).evaluate_in_scope( - dict(q=3.14159,)) - expected = 224. - self.assertEqual(evaluated, expected) - - \ No newline at end of file + self.assertAlmostEqual(evaluated, expected, precision_digits) \ No newline at end of file From a07a154ad6d608c020a2120b0daf00ac96294c39 Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Thu, 28 Aug 2025 10:51:41 +0200 Subject: [PATCH 2/5] stricter --- tests/utils/utils_tests.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/utils/utils_tests.py b/tests/utils/utils_tests.py index 8c7d5c04..21d46ccc 100644 --- a/tests/utils/utils_tests.py +++ b/tests/utils/utils_tests.py @@ -108,7 +108,6 @@ class ToNextMultipleTests(unittest.TestCase): def test_to_next_multiple(self): from qupulse.utils.types import TimeType from qupulse.expressions import ExpressionScalar - precision_digits = 12 duration = TimeType.from_float(47.1415926535) evaluated = to_next_multiple(sample_rate=TimeType.from_float(2.4),quantum=16)(duration) @@ -123,30 +122,30 @@ def test_to_next_multiple(self): duration = 6185240.0000001 evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=13)(duration).evaluate_numeric() expected = 6185248 - self.assertAlmostEqual(evaluated, expected, precision_digits) + self.assertEqual(evaluated, expected) duration = 63.99 evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=4)(duration).evaluate_numeric() expected = 64 - self.assertAlmostEqual(evaluated, expected, precision_digits) + self.assertEqual(evaluated, expected) duration = 64.01 evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=4)(duration).evaluate_numeric() expected = 80 - self.assertAlmostEqual(evaluated, expected, precision_digits) + self.assertEqual(evaluated, expected) duration = 0. evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=13)(duration).evaluate_numeric() expected = 0. - self.assertAlmostEqual(evaluated, expected, precision_digits) + self.assertEqual(evaluated, expected) duration = ExpressionScalar('abc') evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=13)(duration).evaluate_in_scope(dict(abc=0.)) expected = 0. - self.assertAlmostEqual(evaluated, expected, precision_digits) + self.assertEqual(evaluated, expected) duration = ExpressionScalar('q') evaluated = to_next_multiple(sample_rate=ExpressionScalar('w'),quantum=16,min_quanta=1)(duration).evaluate_in_scope( dict(q=3.14159,w=1.0)) expected = 16. - self.assertAlmostEqual(evaluated, expected, precision_digits) \ No newline at end of file + self.assertEqual(evaluated, expected) \ No newline at end of file From 0849652c481321cac9db0df320d37f1d007b6eb7 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 28 Aug 2025 11:44:35 +0200 Subject: [PATCH 3/5] Make variable names more verbose and remove comments --- qupulse/utils/__init__.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/qupulse/utils/__init__.py b/qupulse/utils/__init__.py index 3ad983cc..817343d7 100644 --- a/qupulse/utils/__init__.py +++ b/qupulse/utils/__init__.py @@ -152,22 +152,20 @@ def to_next_multiple(sample_rate: ExpressionLike, quantum: int, #double negative for ceil division. return lambda duration: -(-(duration*sample_rate)//quantum) * (quantum/sample_rate) else: - qI = sp.Integer(quantum) - k = qI / sample_rate # factor to go from #quanta -> duration - mqI = sp.Integer(min_quanta) - - def _build_sym(d): - u = d*sample_rate/qI # "duration in quanta" (real) - ce = sp.ceiling(u) # number of quanta after rounding up - - # Enforce: 0 if d <= 0; else at least mqI quanta. - # max(mqI, ceil(u)) <=> mqI if u <= mqI, else ceil(u) - # do not evaluate right now because parameters could still be variable, - # then it's just overhead. - return sp.Piecewise( - (0, sp.Le(d, 0)), - (k*mqI, sp.Le(u, mqI)), - (k*ce, True) - , evaluate=False) - - return lambda duration: ExpressionScalar(_build_sym(duration)) \ No newline at end of file + # work with sympy + sample_rate = sample_rate.sympified_expression + duration_per_quantum = sp.Integer(quantum) / sample_rate + minimal_duration = duration_per_quantum * min_quanta + + def build_next_multiple(duration: ExpressionLike) -> ExpressionScalar: + duration = sp.sympify(duration) + rounded_up_duration = sp.ceiling(duration / duration_per_quantum) * duration_per_quantum + + next_multiple_sp = sp.Piecewise( + (0, sp.Le(duration, 0)), + (minimal_duration, sp.Le(duration, minimal_duration)), + (rounded_up_duration, True) + ) + return ExpressionScalar(next_multiple_sp) + + return build_next_multiple From b22a33b30c5b4f22426caac200911bd8d222b182 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 28 Aug 2025 13:43:43 +0200 Subject: [PATCH 4/5] Add application oriented pytest benchmark --- tests/utils/utils_tests.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/utils/utils_tests.py b/tests/utils/utils_tests.py index 21d46ccc..a6d880e8 100644 --- a/tests/utils/utils_tests.py +++ b/tests/utils/utils_tests.py @@ -148,4 +148,19 @@ def test_to_next_multiple(self): evaluated = to_next_multiple(sample_rate=ExpressionScalar('w'),quantum=16,min_quanta=1)(duration).evaluate_in_scope( dict(q=3.14159,w=1.0)) expected = 16. - self.assertEqual(evaluated, expected) \ No newline at end of file + self.assertEqual(evaluated, expected) + + +def test_to_next_multiple_padding_duration_evaluation(benchmark): + # reminder how to manually run pytest tests: + # use pytest -k test_to_next_multiple_padding_duration_evaluation + # or for faster collection phase + # pytest -k test_to_next_multiple_padding_duration_evaluation tests/utils/utils_tests.py + + from qupulse.pulses import FunctionPT + pt = FunctionPT('start+t/t_gate*(end-start)', 't_gate', 'a') + + def padding(): + pt.pad_to(to_next_multiple(2.4, 16, 4)).duration.evaluate_in_scope({'t_gate': 10.}) + + benchmark(padding) From 896b1ae5dcc624e152175c6e683f2995660acb41 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 3 Sep 2025 16:20:48 +0200 Subject: [PATCH 5/5] Adjust type hints to pad_to requirements and do piecewise evaluation based on the quanta --- qupulse/utils/__init__.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/qupulse/utils/__init__.py b/qupulse/utils/__init__.py index 817343d7..fa571537 100644 --- a/qupulse/utils/__init__.py +++ b/qupulse/utils/__init__.py @@ -131,7 +131,7 @@ def forced_hash(obj) -> int: def to_next_multiple(sample_rate: ExpressionLike, quantum: int, - min_quanta: Optional[int] = None) -> Callable[[ExpressionLike],ExpressionScalar]: + min_quanta: Optional[int] = None) -> Callable[[ExpressionScalar], ExpressionLike]: """Construct a helper function to expand a duration to one corresponding to valid sample multiples according to the arguments given. Useful e.g. for PulseTemplate.pad_to's 'to_new_duration'-argument. @@ -141,12 +141,13 @@ def to_next_multiple(sample_rate: ExpressionLike, quantum: int, quantum: number of samples to whose next integer multiple the duration shall be rounded up to. min_quanta: number of multiples of quantum not to fall short of. Returns: - A function that takes a duration (ExpressionLike) as input, and returns + A function that takes a duration as input, and returns a duration rounded up to the next valid samples count in given sample rate. The function returns 0 if duration==0, <0 is not checked if min_quanta is None. """ sample_rate = ExpressionScalar(sample_rate) + #is it more efficient to omit the Max call if not necessary? if min_quanta is None: #double negative for ceil division. @@ -157,14 +158,16 @@ def to_next_multiple(sample_rate: ExpressionLike, quantum: int, duration_per_quantum = sp.Integer(quantum) / sample_rate minimal_duration = duration_per_quantum * min_quanta - def build_next_multiple(duration: ExpressionLike) -> ExpressionScalar: + def build_next_multiple(duration: ExpressionScalar) -> ExpressionLike: duration = sp.sympify(duration) - rounded_up_duration = sp.ceiling(duration / duration_per_quantum) * duration_per_quantum + n_quanta = sp.ceiling(duration / duration_per_quantum) + rounded_up_duration = n_quanta * duration_per_quantum next_multiple_sp = sp.Piecewise( - (0, sp.Le(duration, 0)), - (minimal_duration, sp.Le(duration, minimal_duration)), - (rounded_up_duration, True) + (0, sp.Le(n_quanta, 0)), + (minimal_duration, sp.Le(n_quanta, min_quanta)), + (rounded_up_duration, True), + evaluate=False, ) return ExpressionScalar(next_multiple_sp)