diff --git a/AGENTS.md b/AGENTS.md index d33c3dc5..2d1d5e0d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +44,8 @@ Key files: user-facing front-ends are `src/monoprop/majorana_propagator.py` (`MajoranaPropagator`) and `src/monoprop/pauli_propagator.py` (`PauliPropagator`). - `cpp/include/monoprop/MonomialPropagator.h`: the single templated C++ engine `MonomialPropagator` - (the Majorana/Pauli choice is a runtime `Basis`, not a separate class). + (the Majorana/Pauli choice is a runtime `Basis`, not a separate class). Its `only_rotate_len_k` + arguments use `std::optional`; `std::nullopt` means no gate-application length cap. - `src/monoprop/bindings/binder.h`: hand-written binding template; `tools/generate-*.py` generate the per-mode-width `bindings.cpp` and `_dispatch.py` from it (do not hand-edit the generated files). Both generators take the 32-mode storage-block rule from `tools/_binding_layout.py` — they must diff --git a/cpp/include/monoprop/MonomialPropagator.h b/cpp/include/monoprop/MonomialPropagator.h index 1110a6f8..2b09f350 100644 --- a/cpp/include/monoprop/MonomialPropagator.h +++ b/cpp/include/monoprop/MonomialPropagator.h @@ -233,22 +233,23 @@ class MonomialPropagator { /// Build the propagation graph, one layer per generator, recording each layer's gate info /// (angle = parameters[mapping[i]] * gen_coeffs[i]). Accumulates across calls. `gate_indices` is /// 0-based per call, offset internally by the gate count already in the graph. Pass `parameters` to - /// seed atol truncation while extending a non-empty graph. `only_rotate_len_k` > 0 applies gates to - /// monomials of length <= k even if they anticommute. Heisenberg consumes each call's sequence in + /// seed atol truncation while extending a non-empty graph. `only_rotate_len_k` applies gates to + /// monomials of length <= k even if they anticommute; nullopt applies them without a length cap. + /// Heisenberg consumes each call's sequence in /// reverse, so a forward split across calls is not equivalent; Schrodinger is front-to-back, so it is. auto build_graph(const std::vector &majoranas, const VecZ ¶meter_mapping, const VecD &gen_coeffs, std::optional gate_indices = std::nullopt, std::optional parameters = std::nullopt, - int only_rotate_len_k = 0) -> void; + std::optional only_rotate_len_k = std::nullopt) -> void; /// Evolve and contract immediately, without storing a graph. auto propagate(const std::vector &majoranas, const VecZ ¶meter_mapping, const VecD &gen_coeffs, const VecD ¶meters, - int only_rotate_len_k = 0) -> void; + std::optional only_rotate_len_k = std::nullopt) -> void; auto expectation_value(const VecD ¶meters) -> double; @@ -409,7 +410,7 @@ class MonomialPropagator { const VecZ ¶meter_mapping, const VecD &gen_coeffs, const VecZ &gate_indices, - int only_rotate_len_k) -> void; + std::optional only_rotate_len_k) -> void; // Returns {build_angle, apply_angle}; apply is the build angle, negated in Schrödinger. auto gate_angle_(const VecD &mapped_params, size_t i, size_t majoranas_size) const -> std::pair { @@ -424,30 +425,31 @@ class MonomialPropagator { const VecZ &gate_indices, const VecD ¶meters, const VecD &operator_coeffs, - int only_rotate_len_k) -> void; + std::optional only_rotate_len_k) -> void; auto evolve_mode_contract_immediately_(const std::vector &majoranas, const VecZ ¶meter_mapping, const VecD &gen_coeffs, const VecD ¶meters, - int only_rotate_len_k) -> void; + std::optional only_rotate_len_k) -> void; template - auto run_gate_loop_(const std::vector &majoranas, int only_rotate_len_k, EvolutionFunc evolution_func) - -> void; + auto run_gate_loop_(const std::vector &majoranas, + std::optional only_rotate_len_k, + EvolutionFunc evolution_func) -> void; auto propagate_one_(const VecZ &gen_vec, - int only_rotate_len_k, + std::optional only_rotate_len_k, std::optional> coeffs = std::nullopt, std::optional param = std::nullopt, size_t param_index = 0, double gen_coeff = 0.0, size_t gate_index = 0) -> void; - // fused_scale_coeffs (ContractImmediately only): the picture's mutable coeff vector for the k==0 fused - // cos sweep; the taken decision is reported via fused_scale so the apply matches. See build_layer. + // fused_scale_coeffs (ContractImmediately only): the picture's mutable coeff vector for the uncapped + // fused cos sweep; the taken decision is reported via fused_scale so the apply matches. See build_layer. auto build_evolve_result_(const VecZ &gen_vec, - int only_rotate_len_k, + std::optional only_rotate_len_k, std::optional> coeffs = std::nullopt, std::optional param = std::nullopt, CosMask *out_cos = nullptr, diff --git a/cpp/monoprop/Validation.cpp b/cpp/monoprop/Validation.cpp index 29c8b504..f702072d 100644 --- a/cpp/monoprop/Validation.cpp +++ b/cpp/monoprop/Validation.cpp @@ -107,6 +107,17 @@ auto validate_expected_graph_layers(size_t current_layers, size_t expected_layer } } +auto validate_only_rotate_len_k_(std::optional only_rotate_len_k, size_t max_k) -> void { + if (!only_rotate_len_k.has_value()) { + return; + } + + const auto k = *only_rotate_len_k; + if (k == 0 || static_cast(k) > max_k) { + throw ValidationError(std::format("only_rotate_len_k={} is out of range; must be 0 < k <= 2*num_qubits", k)); + } +} + // NOLINTEND(misc-use-internal-linkage) } // namespace monoprop diff --git a/cpp/monoprop/Validation.h b/cpp/monoprop/Validation.h index 436c5444..7124c06b 100644 --- a/cpp/monoprop/Validation.h +++ b/cpp/monoprop/Validation.h @@ -14,6 +14,7 @@ #pragma once +#include #include #include "monoprop/TypeAliases.h" @@ -39,4 +40,7 @@ monoprop_EXPORT auto validate_functional_call(const VecD ¶meters, size_t exp // The graph must still have the layer count the functional was built against. monoprop_EXPORT auto validate_expected_graph_layers(size_t current_layers, size_t expected_layers) -> void; +// only_rotate_len_k is optional; when set it must satisfy 0 < k <= max_k. +monoprop_EXPORT auto validate_only_rotate_len_k_(std::optional only_rotate_len_k, size_t max_k) -> void; + } // namespace monoprop diff --git a/cpp/monoprop/detail/evolution/layer_build/Engine.h b/cpp/monoprop/detail/evolution/layer_build/Engine.h index 63335a35..9be43c80 100644 --- a/cpp/monoprop/detail/evolution/layer_build/Engine.h +++ b/cpp/monoprop/detail/evolution/layer_build/Engine.h @@ -25,6 +25,7 @@ #include #include +#include "monoprop/Validation.h" #include "monoprop/algebra/Algebra.h" #include "monoprop/detail/evolution/EvolutionHelpers.h" #include "monoprop/detail/evolution/layer_build/Common.h" @@ -521,7 +522,7 @@ auto build_layer(MPOperator &local_op, std::optional> local_coeffs, const std::optional &upper_atol, const std::optional ¶m, - int only_rotate_len_k, + std::optional only_rotate_len_k, MatchedEpochSet &matched_scratch, mpi::Comm comm, CosMask *out_cos = nullptr, @@ -530,6 +531,7 @@ auto build_layer(MPOperator &local_op, VecD *fused_scale_coeffs = nullptr, bool *fused_scale_out = nullptr, Basis basis = Basis::Majorana) -> std::shared_ptr { + validate_only_rotate_len_k_(only_rotate_len_k, 2 * NumModes); const size_t my_rank = static_cast(mpi::rank(comm)); const size_t R = static_cast(mpi::size(comm)); // Fused contraction runs at all rank counts (R>1 via the cross-rank half-rotation exchange). @@ -538,13 +540,13 @@ auto build_layer(MPOperator &local_op, const auto &coeffs = local_coeffs ? local_coeffs->get() : empty_coeffs(); const CutoffEvaluator cut_eval{cutoff_fn}; - // Fused cos sweep: fold the per-gate cosine scale into the scan's own coefficient pass. k==0 only (a + // Fused cos sweep: fold the per-gate cosine scale into the scan's own coefficient pass. No length cap only (a // popcount>k hit is outside the per-index cos set, so 1/cos recovery would be wrong) and cos!=0 (else // recovery is impossible; two-pass fallback). cos is even, so the sweep's cos(2·build_angle) matches // the apply's cos(2·apply_angle) bit-for-bit. const double cos_build = (use_fused && param.has_value()) ? std::cos(2.0 * param.value()) : 1.0; - const bool fused_scale = - use_fused && only_rotate_len_k == 0 && fused_scale_coeffs != nullptr && param.has_value() && cos_build != 0.0; + const bool fused_scale = use_fused && !only_rotate_len_k.has_value() && fused_scale_coeffs != nullptr + && param.has_value() && cos_build != 0.0; // build_layer is the single authority for this decision; the fused caller must drive its apply from it. if (fused_scale_out != nullptr) { *fused_scale_out = fused_scale; diff --git a/cpp/monoprop/detail/evolution/layer_build/FusedApply.h b/cpp/monoprop/detail/evolution/layer_build/FusedApply.h index 2e5fb04e..fdd002de 100644 --- a/cpp/monoprop/detail/evolution/layer_build/FusedApply.h +++ b/cpp/monoprop/detail/evolution/layer_build/FusedApply.h @@ -25,9 +25,9 @@ namespace monoprop::detail { // The drain paired with build_layer's fused emission: complete each rotation by adding its sine term // directly to op_coeffs (the ContractImmediately forward path at all rank counts). The gate's cosine // scale reaches the coefficients two ways: -// • fused_scale (k==0, default): the scan already scaled every anticommuting coeff, so no cos pass runs +// • fused_scale (no length cap, default): the scan already scaled every anticommuting coeff, so no cos pass runs // here; slots born after that sweep (fresh inserts) fold cos in via their apply arm below. -// • two-pass (k>0 / cos==0 fallback): scale_cos_mask runs here, then every arm is a plain add. +// • two-pass (length cap / cos==0 fallback): scale_cos_mask runs here, then every arm is a plain add. // At R>1 each rank applies only the add to the slot it owns (half rotations in fc.cross_half). inline auto apply_fused_contract(FusedContract &fc, VecD &op_coeffs, diff --git a/cpp/monoprop/detail/evolution/layer_build/Scan.h b/cpp/monoprop/detail/evolution/layer_build/Scan.h index cd21e073..c75f80d7 100644 --- a/cpp/monoprop/detail/evolution/layer_build/Scan.h +++ b/cpp/monoprop/detail/evolution/layer_build/Scan.h @@ -19,10 +19,12 @@ #include #include #include +#include #include #include #include "monoprop/TypeAliases.h" +#include "monoprop/Validation.h" #include "monoprop/algebra/Algebra.h" #include "monoprop/detail/evolution/EvolutionHelpers.h" #include "monoprop/detail/evolution/layer_build/Common.h" @@ -141,9 +143,11 @@ inline auto even_parity_scan_pass1(const InvertedIndex &sc, // The per-term rotation gate splits into a dynamic part (orbital pop cap, lower-atol sine cutoff) and a // static part (the structural cutoff on M'=M⊕G, applied in emit). -inline auto rotation_dynamic_gate(int only_rotate_len_k, size_t mono_pop, const CutoffContext &ctx, double abs_c) - -> bool { - if (only_rotate_len_k > 0 && mono_pop > static_cast(only_rotate_len_k)) { +inline auto rotation_dynamic_gate(std::optional only_rotate_len_k, + size_t mono_pop, + const CutoffContext &ctx, + double abs_c) -> bool { + if (only_rotate_len_k && mono_pop > static_cast(*only_rotate_len_k)) { return false; } if (ctx.is_below_sin(abs_c)) { @@ -183,7 +187,7 @@ struct FusedScanResult { // Classify, cut off and emit in one pass over the anticommuting terms. Queries go to the owner of // M'=M⊕G (hash%R; self at R==1) in ascending source-index order, so resolve and index assignment are -// deterministic. `fused_scale_coeffs` (k==0 only; must alias coeffs.data()) scales every anticommuting +// deterministic. `fused_scale_coeffs` (no length cap only; must alias coeffs.data()) scales every anticommuting // coeff in place by `fused_scale_cos`=cos(2·build_angle), so no cosine set is built and a hit's stored // value is post-cos (resolve recovers it via 1/cos). template @@ -192,12 +196,13 @@ auto fused_find_and_collect(const MPOperator &op, const CutoffEvaluator &cutoff_eval, const CutoffContext &cut_st, const VecD &coeffs, - int only_rotate_len_k, + std::optional only_rotate_len_k, size_t rank_count, size_t my_rank, bool capture_values = false, double *fused_scale_coeffs = nullptr, double fused_scale_cos = 1.0) -> FusedScanResult { + validate_only_rotate_len_k_(only_rotate_len_k, 2 * NumModes); const size_t gen_pop = gen.count(); const auto ectx = A::make_gen_context(gen); @@ -333,7 +338,7 @@ auto fused_find_and_collect(const MPOperator &op, } return {0.0, cut_st.abs_coeff_for(i, coeffs)}; }; - const bool word_aligned_cos = only_rotate_len_k == 0; + const bool word_aligned_cos = !only_rotate_len_k.has_value(); CosineWordBuilder cos_b; for (const auto &w : nz) { if (word_aligned_cos && fused_scale_coeffs != nullptr) { @@ -375,7 +380,7 @@ auto fused_find_and_collect(const MPOperator &op, const size_t tz = static_cast(std::countr_zero(m)); const size_t i = w.base + tz; const size_t mono_pop = op.store->popcount(i); - if (mono_pop > static_cast(only_rotate_len_k)) { + if (mono_pop > static_cast(*only_rotate_len_k)) { continue; } cos_b.push_index(i); diff --git a/cpp/monoprop/detail/monomial_propagator/MonomialPropagatorImpl.h b/cpp/monoprop/detail/monomial_propagator/MonomialPropagatorImpl.h index d00e128c..7d0722f3 100644 --- a/cpp/monoprop/detail/monomial_propagator/MonomialPropagatorImpl.h +++ b/cpp/monoprop/detail/monomial_propagator/MonomialPropagatorImpl.h @@ -535,12 +535,12 @@ auto MonomialPropagator::evolve_mode_build_graph_(const std::vector void { + std::optional only_rotate_len_k) -> void { const auto majoranas_size = majoranas.size(); run_gate_loop_(majoranas, only_rotate_len_k, [this, ¶meter_mapping, &gen_coeffs, &gate_indices, majoranas_size](const VecZ &mono, - int rot_len, + std::optional rot_len, size_t i) { const auto idx = !schrodinger_ ? majoranas_size - 1 - i : i; propagate_one_(mono, @@ -560,7 +560,7 @@ auto MonomialPropagator::evolve_mode_graph_with_coeffs_(const std::vec const VecZ &gate_indices, const VecD ¶meters, const VecD &operator_coeffs, - int only_rotate_len_k) -> void { + std::optional only_rotate_len_k) -> void { auto mapped_params = map_params(parameters, parameter_mapping, gen_coeffs, 1.0); auto coeffs = operator_coeffs; const auto majoranas_size = majoranas.size(); @@ -569,7 +569,7 @@ auto MonomialPropagator::evolve_mode_graph_with_coeffs_(const std::vec only_rotate_len_k, [this, ¶meter_mapping, &gen_coeffs, &gate_indices, &mapped_params, &coeffs, majoranas_size]( const VecZ &mono, - int rot_len, + std::optional rot_len, size_t i) { const auto idx = !schrodinger_ ? majoranas_size - 1 - i : i; const auto [build_angle, apply_angle] = gate_angle_(mapped_params, i, majoranas_size); @@ -593,7 +593,7 @@ auto MonomialPropagator::evolve_mode_contract_immediately_(const std:: const VecZ ¶meter_mapping, const VecD &gen_coeffs, const VecD ¶meters, - int only_rotate_len_k) -> void { + std::optional only_rotate_len_k) -> void { auto mapped_params = map_params(parameters, parameter_mapping, gen_coeffs, 1.0); // Called for the side effect alone: it returns a reference to the very vector selected below. (void)current_picture_coeffs_(); @@ -602,7 +602,7 @@ auto MonomialPropagator::evolve_mode_contract_immediately_(const std:: run_gate_loop_( majoranas, only_rotate_len_k, - [this, &mapped_params, op_coeffs, majoranas_size](const VecZ &mono, int rot_len, size_t i) { + [this, &mapped_params, op_coeffs, majoranas_size](const VecZ &mono, std::optional rot_len, size_t i) { const auto [build_angle, apply_angle] = gate_angle_(mapped_params, i, majoranas_size); // extend_coeffs must run after build_evolve_result_'s self-rank grow and before the apply. CosMask cos; @@ -620,7 +620,8 @@ auto MonomialPropagator::build_graph(const std::vector &majorana const VecD &gen_coeffs, std::optional gate_indices, std::optional parameters, - int only_rotate_len_k) -> void { + std::optional only_rotate_len_k) -> void { + validate_only_rotate_len_k_(only_rotate_len_k, 2 * logical_num_modes_); if (partition_group_) { for_each_partition_([&](MonomialPropagator &s) { s.build_graph(majoranas, parameter_mapping, gen_coeffs, gate_indices, parameters, only_rotate_len_k); @@ -690,7 +691,8 @@ auto MonomialPropagator::propagate(const std::vector &majoranas, const VecZ ¶meter_mapping, const VecD &gen_coeffs, const VecD ¶meters, - int only_rotate_len_k) -> void { + std::optional only_rotate_len_k) -> void { + validate_only_rotate_len_k_(only_rotate_len_k, 2 * logical_num_modes_); if (partition_group_) { for_each_partition_([&](MonomialPropagator &s) { s.propagate(majoranas, parameter_mapping, gen_coeffs, parameters, only_rotate_len_k); @@ -715,7 +717,7 @@ auto MonomialPropagator::propagate(const std::vector &majoranas, template template auto MonomialPropagator::run_gate_loop_(const std::vector &majoranas, - int only_rotate_len_k, + std::optional only_rotate_len_k, EvolutionFunc evolution_func) -> void { // Serial per partition; parallelism comes from partitioning the operator across cores. for (size_t i = 0; i < majoranas.size(); ++i) { @@ -729,7 +731,7 @@ auto MonomialPropagator::run_gate_loop_(const std::vector &major template auto MonomialPropagator::build_evolve_result_(const VecZ &gen_vec, - int only_rotate_len_k, + std::optional only_rotate_len_k, std::optional> coeffs, std::optional param, CosMask *out_cos, @@ -761,7 +763,7 @@ auto MonomialPropagator::build_evolve_result_(const VecZ &gen_vec, template auto MonomialPropagator::propagate_one_(const VecZ &gen_vec, - int only_rotate_len_k, + std::optional only_rotate_len_k, std::optional> coeffs, std::optional param, size_t param_index, diff --git a/cpp/tests/ctor_validation_tests.cpp b/cpp/tests/ctor_validation_tests.cpp index 8e3e5084..2eddcd19 100644 --- a/cpp/tests/ctor_validation_tests.cpp +++ b/cpp/tests/ctor_validation_tests.cpp @@ -126,6 +126,30 @@ BOOST_AUTO_TEST_CASE(generator_index_bound_is_logical_not_storage) { BOOST_CHECK_THROW(sim.build_graph({VecZ{9}}, VecZ{0}, VecD{1.0}), std::runtime_error); } +BOOST_AUTO_TEST_CASE(only_rotate_len_k_build_graph_validation_matches_python_contract) { + auto sim = make(OperatorDict{}); + + BOOST_CHECK_THROW(sim.build_graph({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, std::nullopt, std::nullopt, /*k=*/0u), + std::runtime_error); + BOOST_CHECK_NO_THROW(sim.build_graph({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, std::nullopt, std::nullopt, std::nullopt)); + + auto logical_bound = make(OperatorDict{}, 2 * N, std::nullopt, std::nullopt, CutoffType::Length, std::nullopt, 4); + BOOST_CHECK_THROW(logical_bound.build_graph({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, std::nullopt, std::nullopt, 9u), + std::runtime_error); + BOOST_CHECK_NO_THROW(logical_bound.build_graph({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, std::nullopt, std::nullopt, 8u)); +} + +BOOST_AUTO_TEST_CASE(only_rotate_len_k_propagate_validation_matches_python_contract) { + auto sim = make(OperatorDict{}); + + BOOST_CHECK_THROW(sim.propagate({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, VecD{0.5}, /*k=*/0u), std::runtime_error); + BOOST_CHECK_NO_THROW(sim.propagate({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, VecD{0.5}, std::nullopt)); + + auto logical_bound = make(OperatorDict{}, 2 * N, std::nullopt, std::nullopt, CutoffType::Length, std::nullopt, 4); + BOOST_CHECK_THROW(logical_bound.propagate({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, VecD{0.5}, 9u), std::runtime_error); + BOOST_CHECK_NO_THROW(logical_bound.propagate({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, VecD{0.5}, 8u)); +} + // The update_* setters must not write straight through to regenerate_cutoff_fn_(). BOOST_AUTO_TEST_CASE(setters_enforce_the_constructor_invariants) { auto pauli = diff --git a/cpp/tests/validation_tests.cpp b/cpp/tests/validation_tests.cpp index 1aeeb75a..173c3a2d 100644 --- a/cpp/tests/validation_tests.cpp +++ b/cpp/tests/validation_tests.cpp @@ -60,3 +60,10 @@ BOOST_AUTO_TEST_CASE(validation_expected_graph_layers) { BOOST_CHECK_NO_THROW(validate_expected_graph_layers(3, 3)); BOOST_CHECK_THROW(validate_expected_graph_layers(4, 3), std::runtime_error); } + +BOOST_AUTO_TEST_CASE(validation_only_rotate_len_k) { + BOOST_CHECK_NO_THROW(validate_only_rotate_len_k_(std::nullopt, 8)); + BOOST_CHECK_NO_THROW(validate_only_rotate_len_k_(8u, 8)); + BOOST_CHECK_THROW(validate_only_rotate_len_k_(0u, 8), std::runtime_error); + BOOST_CHECK_THROW(validate_only_rotate_len_k_(9u, 8), std::runtime_error); +} diff --git a/docs/content/docs/features/cutoff.mdx b/docs/content/docs/features/cutoff.mdx index e530dc68..9063892f 100644 --- a/docs/content/docs/features/cutoff.mdx +++ b/docs/content/docs/features/cutoff.mdx @@ -50,6 +50,13 @@ $M_\nu = m_1 m_2 m_5$ touches only mode 1 (both $m_1$ and $m_2$) and mode 3 term by its **Pauli weight** — the number of qubits it touches. This is the only structural measure: `cutoff` bounds the Pauli weight. +## Gate-application length cap + +[MonomialPropagator.build_graph][] and [MonomialPropagator.propagate][] accept +`only_rotate_len_k` to apply gates only to terms whose length is at most `k`. +This is independent of the structural cutoff. Omitting the argument or passing +`None` imposes no additional gate-application cap. + ## Coefficient tolerance filtering Two absolute-tolerance thresholds prune terms by coefficient magnitude, diff --git a/src/monoprop/bindings/binder.h b/src/monoprop/bindings/binder.h index 2603725b..5728aca5 100644 --- a/src/monoprop/bindings/binder.h +++ b/src/monoprop/bindings/binder.h @@ -106,7 +106,7 @@ auto bind_monomial_propagator(nb::module_ &mod) -> void { "gen_coeffs"_a, "gate_indices"_a = std::nullopt, "parameters"_a = std::nullopt, - "only_rotate_len_k"_a = 0, + "only_rotate_len_k"_a = std::nullopt, "Build the propagation graph, recording per-layer gate information"); // Deep copy: the operator store is cloned, the immutable graph layer cores are shared. @@ -121,7 +121,7 @@ auto bind_monomial_propagator(nb::module_ &mod) -> void { "parameter_mapping"_a, "gen_coeffs"_a, "parameters"_a, - "only_rotate_len_k"_a = 0, + "only_rotate_len_k"_a = std::nullopt, "Evolve and contract immediately without storing a graph"); cls.def("expectation_value", diff --git a/src/monoprop/monomial_propagator.py b/src/monoprop/monomial_propagator.py index eac92956..27b88b79 100644 --- a/src/monoprop/monomial_propagator.py +++ b/src/monoprop/monomial_propagator.py @@ -178,10 +178,8 @@ def _check_circuit_width(self, circuit: Circuit) -> None: f"{self._system_size}." ) - def _validate_and_correct_only_rotate_len_k( - self, only_rotate_len_k: int | None - ) -> int: - """Validate ``only_rotate_len_k``; ``None`` becomes the ``0`` the engine reads as "all". + def _validate_only_rotate_len_k(self, only_rotate_len_k: int | None) -> None: + """Validate ``only_rotate_len_k``. Must be positive, and at most ``2 * num_qubits`` when the propagator knows its qubit count (i.e. on a [PauliPropagator][monoprop.pauli_propagator.PauliPropagator]). @@ -190,16 +188,15 @@ def _validate_and_correct_only_rotate_len_k( only_rotate_len_k: Optional length cutoff for gate application. Returns: - The validated cutoff. The engine reads ``0`` as "apply all gates to all monomials", - so ``None`` maps onto it and callers need not special-case "no restriction". + The validated optional cutoff. """ - if only_rotate_len_k is None: - return 0 - if only_rotate_len_k <= 0 or only_rotate_len_k > 2 * self._system_size: + if ( + only_rotate_len_k is not None + and not 0 < only_rotate_len_k <= 2 * self._system_size + ): raise ValueError( f"only_rotate_len_k={only_rotate_len_k} is out of range; must be 0 < k <= 2*num_qubits " ) - return only_rotate_len_k def build_graph( self, @@ -228,9 +225,7 @@ def build_graph( """ self._check_initial_state(circuit) self._check_circuit_width(circuit) - only_rotate_len_k = self._validate_and_correct_only_rotate_len_k( - only_rotate_len_k - ) + self._validate_only_rotate_len_k(only_rotate_len_k) if seed_parameters is not None: seed = seed_parameters @@ -271,9 +266,7 @@ def propagate( circuit: Gates to apply, and the angle values to apply them at. only_rotate_len_k: See [build_graph][]. """ - only_rotate_len_k = self._validate_and_correct_only_rotate_len_k( - only_rotate_len_k - ) + self._validate_only_rotate_len_k(only_rotate_len_k) self._check_initial_state(circuit) self._check_circuit_width(circuit) gates = self._circuit_gates(circuit) diff --git a/tests/test_only_rotate_k.py b/tests/test_only_rotate_k.py index 4c5708ad..cf4e4757 100644 --- a/tests/test_only_rotate_k.py +++ b/tests/test_only_rotate_k.py @@ -45,6 +45,66 @@ def is_orbital(gate): return gates, [] +@pytest.mark.parametrize( + ("propagator_cls", "operator", "gate_generator", "system_size"), + [ + ( + MajoranaPropagator, + MajoranaOperator({(0,): 1.0}, num_modes=1), + MajoranaOperator({(0, 1): 1.0j}, num_modes=1), + 1, + ), + ( + PauliPropagator, + PauliOperator({"Z": 1.0}, num_qubits=1), + PauliOperator({"X": 1.0}, num_qubits=1), + 1, + ), + ], +) +@pytest.mark.parametrize( + ("builder_method", "result_method"), + [ + ("build_graph", "expval_functional"), + ("propagate", "expval"), + ], +) +def test_only_rotate_len_k_none_is_uncapped( + propagator_cls, + operator, + gate_generator, + system_size, + serial_mp_kwargs, + builder_method, + result_method, +): + circuit = Circuit( + (ExpGate(gate_generator, index=0),), + initial_state=(), + system_size=system_size, + parameters=(0.321,), + ) + parameters = circuit.parameters + + def get_expval(mp): + result = getattr(mp, result_method) + return ( + result()(parameters) if result_method == "expval_functional" else result() + ) + + mp_default = propagator_cls(operator, circuit.initial_state, **serial_mp_kwargs) + getattr(mp_default, builder_method)(circuit) + expval_default = get_expval(mp_default) + + mp_none = propagator_cls(operator, circuit.initial_state, **serial_mp_kwargs) + getattr(mp_none, builder_method)(circuit, only_rotate_len_k=None) + expval_none = get_expval(mp_none) + + assert mp_none.graph_size() == mp_default.graph_size() + assert mp_none.size() == mp_default.size() + assert np.isclose(expval_none, expval_default, atol=1e-12) + + def test_basic_orbital_rotation(serial_comm): n_modes = 4 @@ -148,6 +208,7 @@ def test_only_rotate_len_k(problem, inplace, serial_mp_kwargs): match=r"only_rotate_len_k=0 is out of range; must be 0 < k <= 2\*num_qubits", ), ), + (None, does_not_raise()), ( 9, pytest.raises( @@ -186,6 +247,7 @@ def test_only_rotate_len_k_errors_majorana(only_rotate_len_k, err, method_name): match=r"only_rotate_len_k=0 is out of range; must be 0 < k <= 2\*num_qubits", ), ), + (None, does_not_raise()), ( 9, pytest.raises(