Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Added timeout for QuickSim #654

Open
wants to merge 44 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
9477cf8
:memo: Update pyfiction docstrings
actions-user Dec 12, 2024
35fed45
Merge branch 'cda-tum:main' into main
Drewniok Dec 26, 2024
bc905b7
Merge branch 'cda-tum:main' into main
Drewniok Jan 2, 2025
f188a69
Merge branch 'cda-tum:main' into main
Drewniok Jan 7, 2025
65ec0a5
Merge branch 'cda-tum:main' into main
Drewniok Jan 8, 2025
24c2421
Merge branch 'cda-tum:main' into main
Drewniok Jan 9, 2025
4113659
:memo: Update pyfiction docstrings
actions-user Jan 9, 2025
72e08f4
Merge branch 'cda-tum:main' into main
Drewniok Jan 11, 2025
dfb71fb
Merge branch 'cda-tum:main' into main
Drewniok Jan 12, 2025
d5c5ff5
Merge branch 'cda-tum:main' into main
Drewniok Jan 13, 2025
9d45592
Merge branch 'cda-tum:main' into main
Drewniok Jan 13, 2025
dfaf039
Merge branch 'cda-tum:main' into main
Drewniok Jan 14, 2025
af15694
Merge branch 'cda-tum:main' into main
Drewniok Jan 15, 2025
74dfaab
Merge branch 'cda-tum:main' into main
Drewniok Jan 19, 2025
5598bce
Merge branch 'cda-tum:main' into main
Drewniok Jan 23, 2025
7a1cde8
Merge branch 'cda-tum:main' into main
Drewniok Jan 24, 2025
ecb1fbb
Merge branch 'cda-tum:main' into main
Drewniok Jan 25, 2025
8c6bd0a
Merge branch 'cda-tum:main' into main
Drewniok Jan 28, 2025
fc8e96d
Merge branch 'cda-tum:main' into main
Drewniok Jan 30, 2025
9f1d8a1
Merge branch 'cda-tum:main' into main
Drewniok Jan 31, 2025
34fb825
Merge branch 'cda-tum:main' into main
Drewniok Feb 1, 2025
bf2b216
:sparkles: add timeout feature to quicksim.
Drewniok Feb 1, 2025
aeeb5a0
:memo: Update pyfiction docstrings
actions-user Feb 1, 2025
7f3dd43
:memo: update docu.
Drewniok Feb 1, 2025
1a019ee
:memo: small fix.
Drewniok Feb 1, 2025
143a3f6
:art: fix clang-tidy warnings.
Drewniok Feb 1, 2025
16efd53
:art: fix clang-tidy warnings.
Drewniok Feb 1, 2025
0945b45
:art: small fix.
Drewniok Feb 1, 2025
1690cea
:white_check_mark: small fix.
Drewniok Feb 3, 2025
39fa9c4
:memo: small fix.
Drewniok Feb 3, 2025
2adc077
Merge branch 'main' into add_timout_for_quicksim
Drewniok Feb 3, 2025
88028b2
:art: small fix.
Drewniok Feb 3, 2025
eed8faa
:memo: Update pyfiction docstrings
actions-user Feb 3, 2025
8165114
:art: small fix.
Drewniok Feb 3, 2025
568780f
:white_check_mark: use more margin.
Drewniok Feb 3, 2025
d6a3b35
Merge branch 'main' into add_timout_for_quicksim
Drewniok Feb 4, 2025
27a0b28
:white_check_mark: increase time margin.
Drewniok Feb 5, 2025
afe1338
Merge branch 'main' into add_timout_for_quicksim
Drewniok Feb 5, 2025
9281c53
:art: implement Marcel's feedback.
Drewniok Feb 9, 2025
db26065
:art: use optional as return for quicksim.
Drewniok Feb 11, 2025
10ac019
Merge branch 'main' into add_timout_for_quicksim
Drewniok Feb 11, 2025
2da2e2d
:memo: Update pyfiction docstrings
actions-user Feb 11, 2025
7f9fda3
:art: small fix.
Drewniok Feb 11, 2025
9d8c7bd
:art: remove unused header.
Drewniok Feb 11, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ inline void quicksim(pybind11::module& m)
.def_readwrite("alpha", &fiction::quicksim_params::alpha, DOC(fiction_quicksim_params_alpha))
.def_readwrite("number_threads", &fiction::quicksim_params::number_threads,
DOC(fiction_quicksim_params_number_threads))
.def_readwrite("timeout", &fiction::quicksim_params::timeout, DOC(fiction_quicksim_params_timeout));

;
;

// NOTE be careful with the order of the following calls! Python will resolve the first matching overload!

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,77 @@
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

Drewniok marked this conversation as resolved.
Show resolved Hide resolved
#include <any>
#include <cstdint>
#include <stdexcept>
#include <string>
Drewniok marked this conversation as resolved.
Show resolved Hide resolved
#include <unordered_map>

Drewniok marked this conversation as resolved.
Show resolved Hide resolved
namespace pyfiction
{

namespace detail
{

namespace py = pybind11;

// Helper function to convert std::any to Python objects
inline py::object convert_any_to_py(const std::any& value)
{
Drewniok marked this conversation as resolved.
Show resolved Hide resolved
try
{
Drewniok marked this conversation as resolved.
Show resolved Hide resolved
if (value.type() == typeid(int))
{
Drewniok marked this conversation as resolved.
Show resolved Hide resolved
return py::int_(std::any_cast<int>(value));
}
else if (value.type() == typeid(double))
{
return py::float_(std::any_cast<double>(value));
}
else if (value.type() == typeid(bool))
{
return py::bool_(std::any_cast<bool>(value));
}
else if (value.type() == typeid(std::string))
Drewniok marked this conversation as resolved.
Show resolved Hide resolved
{
return py::str(std::any_cast<std::string>(value));
Drewniok marked this conversation as resolved.
Show resolved Hide resolved
}
else if (value.type() == typeid(uint64_t))
{
return pybind11::int_(std::any_cast<uint64_t>(value));
}
Drewniok marked this conversation as resolved.
Show resolved Hide resolved
else
Drewniok marked this conversation as resolved.
Show resolved Hide resolved
{
Drewniok marked this conversation as resolved.
Show resolved Hide resolved
throw std::runtime_error(std::string("Unsupported type in std::any: ") + value.type().name());
Drewniok marked this conversation as resolved.
Show resolved Hide resolved
}
Drewniok marked this conversation as resolved.
Show resolved Hide resolved
}
catch (const std::exception& e)
Drewniok marked this conversation as resolved.
Show resolved Hide resolved
Drewniok marked this conversation as resolved.
Show resolved Hide resolved
{
throw std::runtime_error(std::string("Error in convert_any_to_py: ") + e.what());
Drewniok marked this conversation as resolved.
Show resolved Hide resolved
}
}

inline py::dict convert_map_to_py(const std::unordered_map<std::string, std::any>& map)
{
pybind11::dict result;
for (const auto& [key, value] : map)
{
try
{
result[key.c_str()] = convert_any_to_py(value);
}
catch (const std::exception& e)
{
throw std::runtime_error(fmt::format("Error converting key: {}", e.what()));
}
}
return result;
}

template <typename Lyt>
void sidb_simulation_result(pybind11::module& m, const std::string& lattice = "")
{
namespace py = pybind11;
namespace py = pybind11;

py::class_<fiction::sidb_simulation_result<Lyt>>(m, fmt::format("sidb_simulation_result{}", lattice).c_str(),
DOC(fiction_sidb_simulation_result))
Expand All @@ -39,16 +97,17 @@ void sidb_simulation_result(pybind11::module& m, const std::string& lattice = ""
DOC(fiction_sidb_simulation_result_charge_distributions))
.def_readwrite("simulation_parameters", &fiction::sidb_simulation_result<Lyt>::simulation_parameters,
DOC(fiction_sidb_simulation_result_simulation_parameters))
.def_readwrite("additional_simulation_parameters",
&fiction::sidb_simulation_result<Lyt>::additional_simulation_parameters,
DOC(fiction_sidb_simulation_result_additional_simulation_parameters));
.def_property_readonly(
"additional_simulation_parameters", [](const fiction::sidb_simulation_result<Lyt>& self)
{ return convert_map_to_py(self.additional_simulation_parameters); },
DOC(fiction_sidb_simulation_result_additional_simulation_parameters));
}

} // namespace detail

inline void sidb_simulation_result(pybind11::module& m)
{
// NOTE be careful with the order of the following calls! Python will resolve the first matching overload!
// Define simulation result for specific lattices
detail::sidb_simulation_result<py_sidb_100_lattice>(m, "_100");
detail::sidb_simulation_result<py_sidb_111_lattice>(m, "_111");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18236,6 +18236,8 @@ the number of available hardware threads.)doc";

static const char *__doc_fiction_quicksim_params_simulation_parameters = R"doc(Simulation parameters for the simulation of the physical SiDB system.)doc";

static const char *__doc_fiction_quicksim_params_timeout = R"doc(Timeout limit (in ms).)doc";

static const char *__doc_fiction_random_coordinate =
R"doc(Generates a random coordinate within the region spanned by two given
coordinates. The two given coordinates form the top left corner and
Expand Down Expand Up @@ -19877,7 +19879,7 @@ R"doc(Default constructor. It only exists to allow for the use of

static const char *__doc_fiction_sidb_simulation_result_simulation_parameters = R"doc(Physical parameters used in the simulation.)doc";

static const char *__doc_fiction_sidb_simulation_result_simulation_runtime = R"doc(Total simulation runtime.)doc";
static const char *__doc_fiction_sidb_simulation_result_simulation_runtime = R"doc(Total simulation runtime in seconds.)doc";

static const char *__doc_fiction_sidb_skeleton_bestagon_library =
R"doc(This library contains SiDB I/O wires designed for both 1- and 2-input
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,19 @@ def test_perturber_and_sidb_pair_111(self):
self.assertEqual(groundstate[0].get_charge_state((2, 0)), sidb_charge_state.NEUTRAL)
self.assertEqual(groundstate[0].get_charge_state((3, 0)), sidb_charge_state.NEGATIVE)

# test timeout
params.timeout = 1
params.iteration_steps = 10000
params.number_threads = 1
result_timeout_1 = quicksim(layout, params)
groundstate_timeout_1 = groundstate_from_simulation_result(result_timeout_1)
print(groundstate_timeout_1)

print(result_timeout_1.additional_simulation_parameters)
self.assertEqual(result_timeout_1.additional_simulation_parameters["alpha"], 0.7)
self.assertEqual(result_timeout_1.additional_simulation_parameters["iteration_steps"], 10000)
self.assertTrue(result_timeout_1.additional_simulation_parameters["timeout_reached"])


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
Expand Up @@ -16,44 +16,100 @@

class TestSiDBSimulationResult(unittest.TestCase):
def test_negative_and_neutral_layout_100_lattice(self):
# Use standard constructor.
"""Test ground state calculation for a negative and neutral layout on the 100 lattice."""
# Initialize the result object.
result = sidb_simulation_result_100()

# Create a 2x3 lattice and assign NORMAL cell types.
layout = sidb_100_lattice((2, 3))
layout.assign_cell_type((0, 1), sidb_technology.cell_type.NORMAL)
layout.assign_cell_type((0, 3), sidb_technology.cell_type.NORMAL)

# Generate charge distributions for negative and neutral states.
cds_negative = charge_distribution_surface_100(layout)

cds_neutral = charge_distribution_surface_100(layout, sidb_simulation_parameters(), sidb_charge_state.NEUTRAL)

# Assign charge distributions to the result.
result.charge_distributions = [cds_negative, cds_neutral]

# Calculate the ground state from the simulation result.
groundstate = groundstate_from_simulation_result(result)

self.assertEqual(len(groundstate), 1)
self.assertEqual(groundstate[0].get_charge_state((0, 1)), sidb_charge_state.NEUTRAL)
self.assertEqual(groundstate[0].get_charge_state((0, 3)), sidb_charge_state.NEUTRAL)
# Validate ground state results.
self.assertEqual(len(groundstate), 1, "Expected exactly one ground state.")
self.assertEqual(
groundstate[0].get_charge_state((0, 1)),
sidb_charge_state.NEUTRAL,
"Cell (0, 1) should have a NEUTRAL charge state.",
)
self.assertEqual(
groundstate[0].get_charge_state((0, 3)),
sidb_charge_state.NEUTRAL,
"Cell (0, 3) should have a NEUTRAL charge state.",
)

def test_negative_and_neutral_layout_111_lattice(self):
# Use standard constructor.
"""Test ground state calculation for a negative and neutral layout on the 111 lattice."""
# Initialize the result object.
result = sidb_simulation_result_111()

# Create a 2x3 lattice and assign NORMAL cell types.
layout = sidb_111_lattice((2, 3))
layout.assign_cell_type((0, 1), sidb_technology.cell_type.NORMAL)
layout.assign_cell_type((0, 3), sidb_technology.cell_type.NORMAL)

# Generate charge distributions for negative and neutral states.
cds_negative = charge_distribution_surface_111(layout)

cds_neutral = charge_distribution_surface_111(layout, sidb_simulation_parameters(), sidb_charge_state.NEUTRAL)

# Assign charge distributions to the result.
result.charge_distributions = [cds_negative, cds_neutral]

# Calculate the ground state from the simulation result.
groundstate = groundstate_from_simulation_result(result)

# Validate ground state results.
self.assertEqual(len(groundstate), 1, "Expected exactly one ground state.")
self.assertEqual(
groundstate[0].get_charge_state((0, 1)),
sidb_charge_state.NEUTRAL,
"Cell (0, 1) should have a NEUTRAL charge state.",
)
self.assertEqual(
groundstate[0].get_charge_state((0, 3)),
sidb_charge_state.NEUTRAL,
"Cell (0, 3) should have a NEUTRAL charge state.",
)

def test_empty_layout_100_lattice(self):
"""Test handling of an empty layout on the 100 lattice."""
result = sidb_simulation_result_100()

layout = sidb_100_lattice((0, 0)) # Empty layout

# Attempting to generate charge distributions should not throw errors.
cds_empty = charge_distribution_surface_100(layout)

# Assign the empty distribution and check ground state handling.
result.charge_distributions = [cds_empty]
groundstate = groundstate_from_simulation_result(result)

self.assertEqual(len(groundstate), 1)

def test_empty_layout_111_lattice(self):
"""Test handling of an empty layout on the 111 lattice."""
result = sidb_simulation_result_111()

layout = sidb_111_lattice((0, 0)) # Empty layout

# Attempting to generate charge distributions should not throw errors.
cds_empty = charge_distribution_surface_111(layout)

# Assign the empty distribution and check ground state handling.
result.charge_distributions = [cds_empty]
groundstate = groundstate_from_simulation_result(result)

self.assertEqual(len(groundstate), 1)
self.assertEqual(groundstate[0].get_charge_state((0, 1)), sidb_charge_state.NEUTRAL)
self.assertEqual(groundstate[0].get_charge_state((0, 3)), sidb_charge_state.NEUTRAL)


if __name__ == "__main__":
Expand Down
43 changes: 37 additions & 6 deletions include/fiction/algorithms/simulation/sidb/quicksim.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
#include <mockturtle/utils/stopwatch.hpp>

#include <algorithm>
#include <chrono>
#include <cstdint>
#include <limits>
#include <mutex>
#include <thread>
#include <vector>

namespace fiction
{

/**
* This struct stores the parameters for the *QuickSim* algorithm.
*/
Expand All @@ -43,6 +44,10 @@ struct quicksim_params
* Number of threads to spawn. By default the number of threads is set to the number of available hardware threads.
*/
uint64_t number_threads{std::thread::hardware_concurrency()};
/**
* Timeout limit (in ms).
*/
uint64_t timeout = std::numeric_limits<uint64_t>::max();
Drewniok marked this conversation as resolved.
Show resolved Hide resolved
};

/**
Expand Down Expand Up @@ -80,9 +85,21 @@ sidb_simulation_result<Lyt> quicksim(const Lyt& lyt, const quicksim_params& ps =
st.simulation_parameters = ps.simulation_parameters;
st.charge_distributions.reserve(ps.iteration_steps);

if (ps.timeout == 0)
{
st.additional_simulation_parameters.emplace("timeout_reached", true);
return st;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really a simulation parameter that the timeout has been reached? In exact, we throw an exception in the _impl class that we then catch in the calling function exact and return an std::nullopt. I'm not 100% sure how gold handles this. You might wanna check.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, I see, @marcelwa! But since all our physical simulators return sidb_simulation_result<Lyt>, it would be weird when one would return std::optional or even through an exception, right?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. At least for the physical design algorithms, some return a Lyt, and some an std::optional<Lyt>

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay! Do you think, @marcelwa, we should use std::optional for charge_distributions in sidb_simulation_result?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could make sense. You would always return an sidb_simulation_result but it might not contain charge_distributions depending on the algorithm termination. Yes, I think that checks out.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great!

Copy link
Collaborator Author

@Drewniok Drewniok Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I am unsure because so many parts of the code had to be changed. Maybe it is better to return std::optional<sidb_simulation_result>. I will try it.


bool timeout_limit_reached = false;

mockturtle::stopwatch<>::duration time_counter{};

// Track the start time for timeout
auto start_time = std::chrono::high_resolution_clock::now();
Drewniok marked this conversation as resolved.
Show resolved Hide resolved

// measure run time (artificial scope)
// main loop
Drewniok marked this conversation as resolved.
Show resolved Hide resolved
{
const mockturtle::stopwatch stop{time_counter};

Expand Down Expand Up @@ -150,14 +167,23 @@ sidb_simulation_result<Lyt> quicksim(const Lyt& lyt, const quicksim_params& ps =

for (uint64_t l = 0ul; l < iter_per_thread; ++l)
{
// Check if the timeout has been reached before starting the iterations
const auto current_time = std::chrono::high_resolution_clock::now();
const auto elapsed_time =
std::chrono::duration_cast<std::chrono::milliseconds>(current_time - start_time).count();

Drewniok marked this conversation as resolved.
Show resolved Hide resolved
if (elapsed_time >= static_cast<decltype(elapsed_time)>(ps.timeout))
{
timeout_limit_reached = true;
return; // Exit the thread if the timeout has been reached
}

for (uint64_t i = 0ul; i < charge_lyt.num_cells(); ++i)
{
if (std::find(negative_sidb_indices.cbegin(), negative_sidb_indices.cend(), i) !=
negative_sidb_indices.cend())
{
if (std::find(negative_sidb_indices.cbegin(), negative_sidb_indices.cend(), i) !=
negative_sidb_indices.cend())
{
continue;
}
continue;
}

std::vector<uint64_t> index_start{i};
Expand Down Expand Up @@ -209,6 +235,11 @@ sidb_simulation_result<Lyt> quicksim(const Lyt& lyt, const quicksim_params& ps =

st.simulation_runtime = time_counter;

if (timeout_limit_reached)
{
st.additional_simulation_parameters.emplace("timeout_reached", true);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above


return st;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ struct sidb_simulation_result
*/
std::string algorithm_name{};
/**
* Total simulation runtime.
* Total simulation runtime in seconds.
*/
std::chrono::duration<double> simulation_runtime{};
/**
Expand Down
24 changes: 24 additions & 0 deletions test/algorithms/simulation/sidb/quicksim.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,17 @@ TEMPLATE_TEST_CASE("QuickSim simulation of a Y-shaped SiDB OR gate with input 01
check_for_absence_of_positive_charges(simulation_results);
check_for_runtime_measurement(simulation_results);
check_charge_configuration(simulation_results);

SECTION("timeout with 0 ms")
{
quicksim_params.timeout = 0;
const auto simulation_results_timeout_0 = quicksim<TestType>(lyt, quicksim_params);

CHECK(simulation_results_timeout_0.charge_distributions.empty());
CHECK(simulation_results_timeout_0.simulation_runtime.count() == 0);
CHECK(std::any_cast<bool>(
simulation_results_timeout_0.additional_simulation_parameters.at("timeout_reached")));
}
}
}

Expand Down Expand Up @@ -1095,4 +1106,17 @@ TEMPLATE_TEST_CASE("QuickSim AND gate simulation on the Si-111 surface", "[quick

CHECK(ground_state.front().get_charge_state({23, 29, 1}) == sidb_charge_state::NEGATIVE);
}

SECTION("timeout with 100 ms")
{
auto lyt = blueprints::and_gate_111<TestType>();
const quicksim_params params{sidb_simulation_parameters{2, -0.32, 5.6, 5}, 3000, 0.5, 1, 100};
const auto simulation_results_timeout_100 = quicksim<TestType>(lyt, params);

check_for_absence_of_positive_charges(simulation_results_timeout_100);
CHECK_THAT(mockturtle::to_seconds(simulation_results_timeout_100.simulation_runtime) * 1000,
Catch::Matchers::WithinAbs(100, 5));
CHECK(
std::any_cast<bool>(simulation_results_timeout_100.additional_simulation_parameters.at("timeout_reached")));
}
}
Loading