Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<NumModes>`
(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<size_t>`; `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
Expand Down
28 changes: 15 additions & 13 deletions cpp/include/monoprop/MonomialPropagator.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<VecZ> &majoranas,
const VecZ &parameter_mapping,
const VecD &gen_coeffs,
std::optional<VecZ> gate_indices = std::nullopt,
std::optional<VecD> parameters = std::nullopt,
int only_rotate_len_k = 0) -> void;
std::optional<size_t> only_rotate_len_k = std::nullopt) -> void;

/// Evolve and contract immediately, without storing a graph.
auto propagate(const std::vector<VecZ> &majoranas,
const VecZ &parameter_mapping,
const VecD &gen_coeffs,
const VecD &parameters,
int only_rotate_len_k = 0) -> void;
std::optional<size_t> only_rotate_len_k = std::nullopt) -> void;

auto expectation_value(const VecD &parameters) -> double;

Expand Down Expand Up @@ -409,7 +410,7 @@ class MonomialPropagator {
const VecZ &parameter_mapping,
const VecD &gen_coeffs,
const VecZ &gate_indices,
int only_rotate_len_k) -> void;
std::optional<size_t> 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<double, double> {
Expand All @@ -424,30 +425,31 @@ class MonomialPropagator {
const VecZ &gate_indices,
const VecD &parameters,
const VecD &operator_coeffs,
int only_rotate_len_k) -> void;
std::optional<size_t> only_rotate_len_k) -> void;

auto evolve_mode_contract_immediately_(const std::vector<VecZ> &majoranas,
const VecZ &parameter_mapping,
const VecD &gen_coeffs,
const VecD &parameters,
int only_rotate_len_k) -> void;
std::optional<size_t> only_rotate_len_k) -> void;

template <typename EvolutionFunc>
auto run_gate_loop_(const std::vector<VecZ> &majoranas, int only_rotate_len_k, EvolutionFunc evolution_func)
-> void;
auto run_gate_loop_(const std::vector<VecZ> &majoranas,
std::optional<size_t> only_rotate_len_k,
EvolutionFunc evolution_func) -> void;

auto propagate_one_(const VecZ &gen_vec,
int only_rotate_len_k,
std::optional<size_t> only_rotate_len_k,
std::optional<std::reference_wrapper<const VecD>> coeffs = std::nullopt,
std::optional<double> 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<size_t> only_rotate_len_k,
std::optional<std::reference_wrapper<const VecD>> coeffs = std::nullopt,
std::optional<double> param = std::nullopt,
CosMask *out_cos = nullptr,
Expand Down
11 changes: 11 additions & 0 deletions cpp/monoprop/Validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<size_t> 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<size_t>(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
4 changes: 4 additions & 0 deletions cpp/monoprop/Validation.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

#pragma once

#include <optional>
#include <vector>

#include "monoprop/TypeAliases.h"
Expand All @@ -39,4 +40,7 @@ monoprop_EXPORT auto validate_functional_call(const VecD &parameters, 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<size_t> only_rotate_len_k, size_t max_k) -> void;

} // namespace monoprop
10 changes: 6 additions & 4 deletions cpp/monoprop/detail/evolution/layer_build/Engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <utility>
#include <vector>

#include "monoprop/Validation.h"
#include "monoprop/algebra/Algebra.h"
#include "monoprop/detail/evolution/EvolutionHelpers.h"
#include "monoprop/detail/evolution/layer_build/Common.h"
Expand Down Expand Up @@ -521,7 +522,7 @@ auto build_layer(MPOperator<NumModes> &local_op,
std::optional<std::reference_wrapper<const VecD>> local_coeffs,
const std::optional<double> &upper_atol,
const std::optional<double> &param,
int only_rotate_len_k,
std::optional<size_t> only_rotate_len_k,
MatchedEpochSet &matched_scratch,
mpi::Comm comm,
CosMask *out_cos = nullptr,
Expand All @@ -530,6 +531,7 @@ auto build_layer(MPOperator<NumModes> &local_op,
VecD *fused_scale_coeffs = nullptr,
bool *fused_scale_out = nullptr,
Basis basis = Basis::Majorana) -> std::shared_ptr<LayerCore> {
validate_only_rotate_len_k_(only_rotate_len_k, 2 * NumModes);
const size_t my_rank = static_cast<size_t>(mpi::rank(comm));
const size_t R = static_cast<size_t>(mpi::size(comm));
// Fused contraction runs at all rank counts (R>1 via the cross-rank half-rotation exchange).
Expand All @@ -538,13 +540,13 @@ auto build_layer(MPOperator<NumModes> &local_op,
const auto &coeffs = local_coeffs ? local_coeffs->get() : empty_coeffs();
const CutoffEvaluator<NumModes> 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;
Expand Down
4 changes: 2 additions & 2 deletions cpp/monoprop/detail/evolution/layer_build/FusedApply.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
19 changes: 12 additions & 7 deletions cpp/monoprop/detail/evolution/layer_build/Scan.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
#include <cassert>
#include <cmath>
#include <cstddef>
#include <optional>
#include <span>
#include <vector>

#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"
Expand Down Expand Up @@ -141,9 +143,11 @@ inline auto even_parity_scan_pass1(const InvertedIndex<NumModes> &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<size_t>(only_rotate_len_k)) {
inline auto rotation_dynamic_gate(std::optional<size_t> only_rotate_len_k,
size_t mono_pop,
const CutoffContext &ctx,
double abs_c) -> bool {
if (only_rotate_len_k && mono_pop > static_cast<size_t>(*only_rotate_len_k)) {
return false;
}
if (ctx.is_below_sin(abs_c)) {
Expand Down Expand Up @@ -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 <size_t NumModes, Algebra A>
Expand All @@ -192,12 +196,13 @@ auto fused_find_and_collect(const MPOperator<NumModes> &op,
const CutoffEvaluator<NumModes> &cutoff_eval,
const CutoffContext &cut_st,
const VecD &coeffs,
int only_rotate_len_k,
std::optional<size_t> 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);

Expand Down Expand Up @@ -333,7 +338,7 @@ auto fused_find_and_collect(const MPOperator<NumModes> &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) {
Expand Down Expand Up @@ -375,7 +380,7 @@ auto fused_find_and_collect(const MPOperator<NumModes> &op,
const size_t tz = static_cast<size_t>(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<size_t>(only_rotate_len_k)) {
if (mono_pop > static_cast<size_t>(*only_rotate_len_k)) {
Comment thread
robertodr marked this conversation as resolved.
continue;
}
cos_b.push_index(i);
Expand Down
Loading
Loading