diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 1fb0f42caf3..8e904494c8c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -373,36 +373,6 @@ tutorials-samples-no-gpu:
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
-rtx4090:
- <<: *global_job_definition
- stage: build
- variables:
- CC: 'gcc-12'
- CXX: 'g++-12'
- GCOV: 'gcov-12'
- myconfig: 'maxset'
- with_cuda: 'true'
- with_coverage: 'false'
- with_scafacos: 'true'
- with_walberla: 'true'
- with_walberla_avx: 'true'
- with_stokesian_dynamics: 'true'
- with_caliper: 'true'
- build_type: 'Release'
- script:
- - bash maintainer/CI/build_cmake.sh
- - cd build
- - make check_samples
- - make check_tutorials
- tags:
- - espresso
- - cuda
- - avx2
- - sfb1313
- rules:
- - if: $CI_COMMIT_BRANCH == "python"
- when: manual
-
installation:
<<: *global_job_definition
stage: build
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index e32e9a689cc..81ecf4138c9 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -22,6 +22,7 @@
add_library(
espresso_core SHARED
bond_error.cpp
+ bonds.cpp
cells.cpp
communication.cpp
dpd.cpp
diff --git a/src/core/bonds.cpp b/src/core/bonds.cpp
new file mode 100644
index 00000000000..4c49fe627ff
--- /dev/null
+++ b/src/core/bonds.cpp
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2025 The ESPResSo project
+ *
+ * This file is part of ESPResSo.
+ *
+ * ESPResSo is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * ESPResSo is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "cell_system/CellStructure.hpp"
+#include "system/System.hpp"
+
+#include
+
+bool add_bond(System::System &system, int bond_id,
+ std::vector const &particle_ids) {
+ Particle *p = system.cell_structure->get_local_particle(particle_ids[0]);
+ if (p) {
+ // The bond view is stored in the bond list of the primary particle.
+ // Thus the bond views's partner list only contains the other particle id.
+ BondView bond(bond_id, {particle_ids.data() + 1, particle_ids.size() - 1});
+ p->bonds().insert(bond);
+ return true;
+ }
+ return false;
+}
diff --git a/src/core/bonds.hpp b/src/core/bonds.hpp
new file mode 100644
index 00000000000..fbc1ca9627d
--- /dev/null
+++ b/src/core/bonds.hpp
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2025 The ESPResSo project
+ *
+ * This file is part of ESPResSo.
+ *
+ * ESPResSo is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * ESPResSo is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#pragma once
+
+#include "system/System.hpp"
+
+#include
+
+constexpr bool use_one_sided_bond_storage = true;
+
+/**
+ * @brief Add a bond to a particle.
+ *
+ * The caller is responsible for calling
+ * @ref System::System::on_particle_change().
+ */
+bool add_bond(System::System &system, int bond_id,
+ std::vector const &particle_ids);
diff --git a/src/core/collision_detection/BindAtPointOfCollision.cpp b/src/core/collision_detection/BindAtPointOfCollision.cpp
index da9bea7c55d..a2d8515b2b0 100644
--- a/src/core/collision_detection/BindAtPointOfCollision.cpp
+++ b/src/core/collision_detection/BindAtPointOfCollision.cpp
@@ -86,7 +86,7 @@ void BindAtPointOfCollision::handle_collisions(
auto const min_global_cut = system.get_min_global_cut();
auto const &box_geo = *system.box_geo;
- add_bind_centers(local_collision_queue, cell_structure, bond_centers);
+ add_bind_centers(local_collision_queue, system, bond_centers);
// Gather the global collision queue, because only one node has a collision
// across node boundaries in its queue.
diff --git a/src/core/collision_detection/BindCenters.cpp b/src/core/collision_detection/BindCenters.cpp
index d648895e76f..9314e4dea76 100644
--- a/src/core/collision_detection/BindCenters.cpp
+++ b/src/core/collision_detection/BindCenters.cpp
@@ -58,7 +58,7 @@ void BindCenters::initialize(System::System &system) {
void BindCenters::handle_collisions(
System::System &system, std::vector &local_collision_queue) {
- add_bind_centers(local_collision_queue, *system.cell_structure, bond_centers);
+ add_bind_centers(local_collision_queue, system, bond_centers);
}
} // namespace CollisionDetection
diff --git a/src/core/collision_detection/utils.hpp b/src/core/collision_detection/utils.hpp
index 6c9577a1403..71b0231e3b6 100644
--- a/src/core/collision_detection/utils.hpp
+++ b/src/core/collision_detection/utils.hpp
@@ -27,6 +27,7 @@
#include "BoxGeometry.hpp"
#include "Particle.hpp"
+#include "bonds.hpp"
#include "cell_system/CellStructure.hpp"
#include "communication.hpp"
#include "virtual_sites.hpp"
@@ -80,17 +81,19 @@ inline auto gather_collision_queue(std::vector const &local) {
}
inline void add_bind_centers(std::vector &collision_queue,
- CellStructure &cell_structure, int bond_centers) {
+ System::System &system, int bond_id) {
for (auto &c : collision_queue) {
// Ensure that the bond is associated with the non-ghost particle
- if (cell_structure.get_local_particle(c.first)->is_ghost()) {
+ if (system.cell_structure->get_local_particle(c.first)->is_ghost()) {
std::swap(c.first, c.second);
}
- const int bondG[] = {c.second};
-
- // Insert the bond for the non-ghost particle
- get_part(cell_structure, c.first).bonds().insert({bond_centers, bondG});
+ // Because MPI rank 1's queue containing (@c p1_on_rank_1, @c p2_on_rank_2)
+ // doesn't guarantee that the same pair (with or without swapped order) is
+ // also queued on the MPI rank 2.
+ // Once we change bond storage, some syncing has to be done.
+ assert(use_one_sided_bond_storage);
+ ::add_bond(system, bond_id, {c.first, c.second});
}
}
diff --git a/src/script_interface/particle_data/ParticleHandle.cpp b/src/script_interface/particle_data/ParticleHandle.cpp
index a6b3089a9d6..7ac67762151 100644
--- a/src/script_interface/particle_data/ParticleHandle.cpp
+++ b/src/script_interface/particle_data/ParticleHandle.cpp
@@ -30,6 +30,7 @@
#include "core/BoxGeometry.hpp"
#include "core/PropagationMode.hpp"
#include "core/bonded_interactions/bonded_interaction_data.hpp"
+#include "core/bonds.hpp"
#include "core/cell_system/CellStructure.hpp"
#include "core/exclusions.hpp"
#include "core/nonbonded_interactions/nonbonded_interaction_data.hpp"
@@ -580,13 +581,12 @@ Variant ParticleHandle::do_call_method(std::string const &name,
return make_vector_of_variants(bonds_flat);
}
if (name == "add_bond") {
- set_particle_property([¶ms](Particle &p) {
- auto const bond_id = get_value(params, "bond_id");
- auto const part_id = get_value>(params, "part_id");
- auto const bond_view =
- BondView(bond_id, {part_id.data(), part_id.size()});
- p.bonds().insert(bond_view);
- });
+ auto const bond_id = get_value(params, "bond_id");
+ auto const partner_ids = get_value>(params, "part_id");
+ std::vector particle_ids = {m_pid};
+ std::ranges::copy(partner_ids, std::back_inserter(particle_ids));
+ ::add_bond(*get_system(), bond_id, particle_ids);
+ get_system()->on_particle_change();
} else if (name == "del_bond") {
set_particle_property([¶ms](Particle &p) {
auto const bond_id = get_value(params, "bond_id");