Skip to content
Draft
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
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ monoprop is a high-performance C++/Python hybrid library implementing Majorana a
- Python docstrings use Google style, and are rendered into the docs site by `just gen-api` — keep
them accurate.
- In prose docs (`docs/content/docs/**.mdx`) and Python docstrings, link to API symbols with the mkdocstrings-style `[Symbol][]` reference (or `[Display][fully.qualified.path]`) — never hard-code `/api/...` URLs. Do not backtick the name in the `[Symbol][]` form. See `docs/content/docs/documenting.mdx`.
- Keep classes on the Rule of Zero: declare no destructor, copy or move member. A member that needs a
deep copy because it is heap-owned goes in `indirect<T>` (`cpp/monoprop/Indirect.h`), the project's
allocator-free C++23 subset of C++26 `std::indirect`. Its pointee must be copy-constructible; copying
creates an independent pointee and moving transfers ownership. Use `std::optional<indirect<T>>` when
absence is a domain state. `T` may be incomplete where the member is declared, but any holder special
member that constructs, copies, or destroys it must be defined where `T` is complete.
- C++ comments: bare `//` for one-line comments, `/* */` for block comments.
- Add Doxygen Qt-style documentation on all declarations in header files.
Put documentation after members in enums, structs, classes.
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ All contributions require accepting the Individual CLA through CLA Assistant.
If you are contributing on behalf of your employer, contact
[cla@algorithmiq.fi](mailto:cla@algorithmiq.fi) to arrange a Corporate CLA.

C++ classes use Rule-of-Zero ownership. A heap-owned value member should use the
project's C++23 `monoprop::indirect<T>` vocabulary type; use
`std::optional<monoprop::indirect<T>>` when the member can be absent. See the
[contributor guide](https://docs.monoprop.algorithmiq.tech/how-to-contribute#c-ownership)
for its opaque-type and completeness rules.

## Documentation

The documentation is built with [Fumadocs](https://fumadocs.dev/) and hosted at
Expand Down
18 changes: 5 additions & 13 deletions cpp/include/monoprop/MonomialPropagator.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include <vector>

#include "monoprop/Evolution.h"
#include "monoprop/Indirect.h"
#include "monoprop/MPFunctions.h"
#include "monoprop/MPGraph.h"
#include "monoprop/TypeAliases.h"
Expand All @@ -54,8 +55,7 @@ class PartitionGroup;
} // namespace detail

/// A propagator setting is out of range, or inconsistent with another setting.
// Covers a crossed atol pair and a logical width outside [1, NumModes]; also thrown from
// MonomialPropagatorImpl.h
// Covers a crossed atol pair and a logical width outside [1, NumModes].
class PropagatorConfigError : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
Expand Down Expand Up @@ -83,14 +83,6 @@ class MonomialPropagator {
Basis basis = Basis::Majorana,
size_t partitions = 0);

/// Out-of-line because partition_group_ is a unique_ptr to an incomplete type here.
virtual ~MonomialPropagator();

/// Deep copy: clones the operator store, shares the immutable graph cores, and clones the whole
/// partition group on a facade. The virtual destructor suppresses implicit moves, so a "move" deep-copies.
MonomialPropagator(const MonomialPropagator &other);
auto operator=(const MonomialPropagator &) -> MonomialPropagator & = delete;

static constexpr auto num_modes{NumModes};
static constexpr auto storage_num_modes{NumModes};

Expand Down Expand Up @@ -274,7 +266,7 @@ class MonomialPropagator {
auto evolved_operator_terms(const VecD &parameters, double atol)
-> std::vector<std::pair<VecZ, std::complex<double>>>;

virtual auto update_initial_operator(const OperatorDict &op_dict) -> void { apply_initial_operator_(op_dict); }
auto update_initial_operator(const OperatorDict &op_dict) -> void { apply_initial_operator_(op_dict); }
Comment thread
robertodr marked this conversation as resolved.

protected:
static inline const auto ev_fn = [](const EvalRequest &request,
Expand Down Expand Up @@ -322,8 +314,8 @@ class MonomialPropagator {

// Intra-process partition runtime. Null ⇒ ordinary single-partition propagator; non-null ⇒ a partition facade
// whose own mp_op_/graph_ are unused and every method fans out to the S partition propagators.
std::unique_ptr<detail::partition::PartitionGroup<NumModes>> partition_group_;
// PartitionGroup rebinds a cloned partition's comm_ to its own transport during a deep copy.
std::optional<indirect<detail::partition::PartitionGroup<NumModes>>> partition_group_;
// PartitionGroup rebinds a copied partition's comm_ to its own transport during a deep copy.
friend class detail::partition::PartitionGroup<NumModes>;

// A facade's own graph_/mp_op_ are never populated, so handing them out would return plausible-looking
Expand Down
1 change: 1 addition & 0 deletions cpp/monoprop/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ target_sources(
TYPE HEADERS
FILES
"${PROJECT_SOURCE_DIR}/cpp/monoprop/Bitset.h"
"${PROJECT_SOURCE_DIR}/cpp/monoprop/Indirect.h"
"${PROJECT_SOURCE_DIR}/cpp/monoprop/MPGraphEncoding.h"
"${PROJECT_SOURCE_DIR}/cpp/monoprop/TypeAliases.h"
"${PROJECT_SOURCE_DIR}/cpp/monoprop/Utilities.h"
Expand Down
84 changes: 84 additions & 0 deletions cpp/monoprop/Indirect.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright 2026 Algorithmiq
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once

#include <memory>
#include <utility>

namespace monoprop {

/// An allocator-free C++23 subset of C++26 `std::indirect`.
//
// The pointee has value semantics: copying copy-constructs a distinct T, while moving transfers the
// allocation and leaves the source valueless. T may be incomplete where indirect<T> is named, but must
// be complete wherever an operation constructs, copies, or destroys the pointee.
//
// This subset deliberately omits allocators, comparisons, hashing, value assignment, and constructors
// that adopt an existing pointer. Copy assignment replaces the allocation rather than preserving its
// address, so T need only be copy-constructible, not copy-assignable.
template <typename T>
class indirect {
public:
/// The owned value type.
using value_type = T;

/// Construct a value-initialized `T`.
explicit indirect() : ptr_(std::make_unique<T>()) {}

/// Construct `T` directly from `args`.
template <typename... Args>
explicit indirect(std::in_place_t, Args &&...args) : ptr_(std::make_unique<T>(std::forward<Args>(args)...)) {}

/// Copy-construct an independent `T`, or preserve a moved-from state.
indirect(const indirect &other) : ptr_(other.ptr_ ? std::make_unique<T>(*other.ptr_) : nullptr) {}

/// Replace the owned value with an independent copy of `other`.
auto operator=(const indirect &other) -> indirect & {
if (this != std::addressof(other)) {
indirect replacement(other);
ptr_.swap(replacement.ptr_);
}
return *this;
}

/// Transfer ownership and leave `other` valueless.
indirect(indirect &&) noexcept = default;

/// Transfer ownership and leave `other` valueless.
auto operator=(indirect &&) noexcept -> indirect & = default;

/// Destroy the owned value, if present.
~indirect() = default;

/// Access the owned value; the object must not be valueless.
auto operator*() noexcept -> T & { return *ptr_; }

/// Access the owned value; the object must not be valueless.
auto operator*() const noexcept -> const T & { return *ptr_; }

/// Access the owned value; the object must not be valueless.
auto operator->() noexcept -> T * { return ptr_.get(); }

/// Access the owned value; the object must not be valueless.
auto operator->() const noexcept -> const T * { return ptr_.get(); }

/// Return whether ownership was transferred from this object.
[[nodiscard]] auto valueless_after_move() const noexcept -> bool { return ptr_ == nullptr; }

private:
std::unique_ptr<T> ptr_;
};

} // namespace monoprop
44 changes: 9 additions & 35 deletions cpp/monoprop/detail/monomial_propagator/MonomialPropagator.inl
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,7 @@ MonomialPropagator<NumModes>::MonomialPropagator(const OperatorDict &initial_ope
basis,
/*partitions=*/1);
};
partition_group_ = std::make_unique<detail::partition::PartitionGroup<NumModes>>(static_cast<int>(n_partitions),
factory,
comm);
partition_group_.emplace(std::in_place, static_cast<int>(n_partitions), factory, comm);
return;
}

Expand Down Expand Up @@ -166,7 +164,7 @@ MonomialPropagator<NumModes>::MonomialPropagator(const OperatorDict &initial_ope
const size_t expected_local_terms = std::max<size_t>(1, op.size() / std::max<size_t>(1, num_ranks));
// Must run before the store: packed_inline_width_() derives the packed-row width from cutoff_fn_.
regenerate_cutoff_fn_();
mp_op_.store = std::make_unique<detail::OperatorIndex<NumModes>>(packed_inline_width_());
mp_op_.store = indirect<detail::OperatorIndex<NumModes>>(std::in_place, packed_inline_width_());
mp_op_.store->reserve(expected_local_terms);
// Store replaced: drop the stale lazy inverted index so it rebuilds against the new store.
mp_op_.inverted_index_.reset();
Expand All @@ -187,30 +185,6 @@ MonomialPropagator<NumModes>::MonomialPropagator(const OperatorDict &initial_ope
initialize_operator_caches_();
}

template <size_t NumModes>
MonomialPropagator<NumModes>::~MonomialPropagator() = default;

template <size_t NumModes>
MonomialPropagator<NumModes>::MonomialPropagator(const MonomialPropagator &other)
: schrodinger_(other.schrodinger_),
comm_(other.comm_),
cutoff_fn_(other.cutoff_fn_),
mp_op_(other.mp_op_),
graph_(other.graph_),
matched_scratch_(other.matched_scratch_),
cutoff_(other.cutoff_),
lower_atol_(other.lower_atol_),
upper_atol_(other.upper_atol_),
core_term_(other.core_term_),
initial_operator_epoch_(other.initial_operator_epoch_),
logical_num_modes_(other.logical_num_modes_),
cutoff_type_(other.cutoff_type_),
basis_change_(other.basis_change_),
basis_(other.basis_),
partition_group_(other.partition_group_
? std::make_unique<detail::partition::PartitionGroup<NumModes>>(*other.partition_group_)
: nullptr) {}

template <size_t NumModes>
auto MonomialPropagator<NumModes>::resolve_partition_count_(size_t requested, mpi::Comm comm) -> size_t {
if (requested >= 1) {
Expand Down Expand Up @@ -251,13 +225,13 @@ auto MonomialPropagator<NumModes>::resolve_partition_count_(size_t requested, mp

template <size_t NumModes>
auto MonomialPropagator<NumModes>::for_each_partition_(const std::function<void(MonomialPropagator &)> &fn) -> void {
partition_group_->run_on_all([&](int r) { fn(partition_group_->partition(r)); });
(**partition_group_).run_on_all([&](int r) { fn((**partition_group_).partition(r)); });
}

template <size_t NumModes>
template <typename Fn, typename R>
auto MonomialPropagator<NumModes>::map_partitions_(Fn fn) -> std::vector<R> {
return detail::partition::map_partitions(*partition_group_, fn);
return detail::partition::map_partitions(**partition_group_, fn);
}

template <size_t NumModes>
Expand All @@ -280,8 +254,8 @@ template <size_t NumModes>
template <typename Proj, typename Accumulate, typename R>
auto MonomialPropagator<NumModes>::fold_partitions_(Proj proj, Accumulate accumulate) const -> R {
R total{};
for (int r = 0; r < partition_group_->partition_count(); ++r) {
accumulate(total, proj(partition_group_->partition(r)));
for (int r = 0; r < (**partition_group_).partition_count(); ++r) {
accumulate(total, proj((**partition_group_).partition(r)));
}
return total;
}
Expand All @@ -294,7 +268,7 @@ auto MonomialPropagator<NumModes>::sum_partitions_(Proj proj) const -> R {

template <size_t NumModes>
auto MonomialPropagator<NumModes>::first_partition_() const -> const MonomialPropagator & {
return partition_group_->partition(0);
return (**partition_group_).partition(0);
}

template <size_t NumModes>
Expand Down Expand Up @@ -1007,7 +981,7 @@ auto MonomialPropagator<NumModes>::expectation_value_functional(std::optional<do
// raw pointer, so the returned callable must not outlive this propagator.
auto fns = std::make_shared<std::vector<std::function<double(const VecD &)>>>(
map_partitions_([&](MonomialPropagator &s) { return s.expectation_value_functional(pare_threshold); }));
auto *grp = partition_group_.get();
auto *grp = std::addressof(**partition_group_);
return [grp, fns](const VecD &params) -> double {
return detail::partition::collect_on_all(*grp,
[&](int r) { return (*fns)[static_cast<size_t>(r)](params); })[0];
Expand All @@ -1022,7 +996,7 @@ auto MonomialPropagator<NumModes>::expectation_value_and_gradient_functional(std
if (partition_group_) {
auto fns = std::make_shared<std::vector<std::function<std::pair<double, VecD>(const VecD &)>>>(map_partitions_(
[&](MonomialPropagator &s) { return s.expectation_value_and_gradient_functional(pare_threshold); }));
auto *grp = partition_group_.get();
auto *grp = std::addressof(**partition_group_);
return [grp, fns](const VecD &params) -> std::pair<double, VecD> {
return detail::partition::collect_on_all(*grp,
[&](int r) { return (*fns)[static_cast<size_t>(r)](params); })[0];
Expand Down
24 changes: 6 additions & 18 deletions cpp/monoprop/detail/operator/MPOperator.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include <format>
#include <print>

#include "monoprop/Indirect.h"
#include "monoprop/TypeAliases.h"
#include "monoprop/Utilities.h"
#include "monoprop/detail/operator/InvertedIndex.h"
Expand Down Expand Up @@ -60,9 +61,9 @@ class OperatorTermNotFound : public std::runtime_error {

template <size_t NumModes>
struct MPOperator {
// The store is non-copyable/non-movable, so it is heap-owned by unique_ptr (keeping MPOperator
// itself cheaply movable). Always non-null.
std::unique_ptr<OperatorIndex<NumModes>> store = std::make_unique<OperatorIndex<NumModes>>();
// The store is non-movable, so it is heap-owned (keeping MPOperator itself cheaply movable).
// indirect copy-constructs an independent index, letting MPOperator declare no special members.
indirect<OperatorIndex<NumModes>> store{std::in_place};
VecD op_coeffs = {};
// Only fully-paired terms score nonzero (see score_new_state_rows_), which on production models is
// ~0.07% of the rows -- a dense vector here is 99.9% zeros. state_rows_ is strictly ascending: rows are
Expand All @@ -79,21 +80,8 @@ struct MPOperator {
Basis basis = Basis::Majorana;
mutable std::optional<InvertedIndex<NumModes>> inverted_index_ = std::nullopt;

MPOperator() noexcept = default;
MPOperator(MPOperator &&) noexcept = default;
MPOperator &operator=(MPOperator &&) noexcept = default;

MPOperator(const MPOperator &other)
: store(other.store->clone()),
op_coeffs(other.op_coeffs),
state_rows_(other.state_rows_),
state_vals_(other.state_vals_),
state_scored_rows_(other.state_scored_rows_),
state_coeffs(other.state_coeffs),
init_op_map(other.init_op_map),
initial_state(other.initial_state),
basis(other.basis),
inverted_index_(other.inverted_index_) {}
// Rule of Zero: every special member is implicit, so a member added above is copied and moved without
// an edit here. Copying deep-copies the store; moving steals it.

auto size() const -> size_t { return store->size(); }

Expand Down
30 changes: 14 additions & 16 deletions cpp/monoprop/detail/operator/OperatorIndex.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,25 +73,23 @@ class OperatorIndex {
explicit OperatorIndex(size_t inline_width = kDefaultInlinePositions)
: inline_width_(std::clamp<size_t>(inline_width, 1, kMaxInlinePositions)),
stride_(1 + inline_width_) {}
OperatorIndex(const OperatorIndex &) = delete;
OperatorIndex &operator=(const OperatorIndex &) = delete;
OperatorIndex(OperatorIndex &&) = delete;
OperatorIndex &operator=(OperatorIndex &&) = delete;

// Called only on an idle store, so it needs no synchronization.
[[nodiscard]] auto clone() const -> std::unique_ptr<OperatorIndex> {
auto out = std::make_unique<OperatorIndex>(inline_width_);
out->rows_ = rows_;
out->size_ = size_;
out->overflow_ = overflow_;
out->reserve_index(table_.count);
for (const Slot &e : table_.slots) {
// Copying rebuilds the keyless index so every slot resolves against this object's row storage.
OperatorIndex(const OperatorIndex &src)
: rows_(src.rows_),
size_(src.size_),
inline_width_(src.inline_width_),
stride_(src.stride_),
overflow_(src.overflow_) {
reserve_index(src.table_.count);
for (const Slot &e : src.table_.slots) {
if (e.idx != kEmptySlot) {
out->insert_slot_(e.idx, e.h);
insert_slot_(e.idx, e.h);
}
}
return out;
}
OperatorIndex &operator=(const OperatorIndex &) = delete;
OperatorIndex(OperatorIndex &&) = delete;
OperatorIndex &operator=(OperatorIndex &&) = delete;

[[nodiscard]] auto size() const -> size_t { return size_; }

Expand Down Expand Up @@ -355,7 +353,7 @@ class OperatorIndex {
auto reserve_index(size_t n) -> void { table_.rehash_to(slots_for_(n + 1)); }

// Insert (idx, h) into the table with no duplicate probe — callers on this path insert provably distinct
// keys (⊕G-injective miss batches, clone re-insertion).
// keys (⊕G-injective miss batches, copy-constructor re-insertion).
auto insert_slot_(TermIndex idx, uint32_t h) -> void {
table_.rehash_if_needed();
size_t s = spread(h) & table_.mask;
Expand Down
2 changes: 1 addition & 1 deletion cpp/monoprop/detail/partition/PartitionGroup.h
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ class PartitionGroup {
}

// Under an MPI parent, find how many ranks share this host and which we are, so each co-located rank
// pins to a disjoint core block (see partition_cpusets). Collective over `parent`; clones copy the result.
// pins to a disjoint core block (see partition_cpusets). Collective over `parent`; copies reuse the result.
auto discover_node_peers_() -> void {
#ifdef monoprop_ENABLE_MPI
if (parent_.kind == mpi::Comm::Kind::Mpi && mpi::size(parent_) > 1) {
Expand Down
Loading