Skip to content

Random tweaks + ofUrn [needed in 12.1 for final of::random interface] #7736

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

Merged
merged 13 commits into from
Sep 5, 2024
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
26 changes: 13 additions & 13 deletions examples/math/randomExample/src/ofApp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ void ofApp::perform() {
ofLogNotice("ofRandomPoisson<int>(2.5)") << ofRandomPoisson<long long>(5.5);
ofLogNotice("ofRandomPoisson<glm::vec2>(2.5)") << ofRandomPoisson<glm::vec2>(5.5);
ofLogNotice("ofRandomPoisson<glm::vec2>({2.5, 10})") << ofRandomPoisson<glm::vec2>({ 5.5, 10 });
ofLogNotice("ofRandomPoisson<glm::vec3>(2.5, 10, 0.1") << ofRandomPoisson<glm::vec3>(5.5);
ofLogNotice("ofRandomPoisson<glm::vec3>({2.5, 10, 0.1}") << ofRandomPoisson<glm::vec3>({ 5.5, 10, 0.1 });
ofLogNotice("ofRandomPoisson<glm::vec3>(2.5, 10, 3") << ofRandomPoisson<glm::vec3>(5.5);
ofLogNotice("ofRandomPoisson<glm::vec3>({2.5, 10, 3}") << ofRandomPoisson<glm::vec3>({ 5.5, 10, 0.1 });

// bernoulli requires 1 arg (no defaults), and return type is bool
// again, glm usage case is cast to float
Expand All @@ -187,17 +187,17 @@ void ofApp::perform() {
ofLogNotice("ofRandomBernoulli<bool>(.5)") << ofRandomBernoulli<bool>(.5);
ofLogNotice("ofRandomBernoulli(.5)") << ofRandomBernoulli(.5);

ofLogNotice("yes aka bernoulli<glm::vec3>(.33)") << yes<glm::vec3>(.33);
ofLogNotice("yes aka bernoulli<glm::vec2>(.5)") << yes<glm::vec2>(.5);
ofLogNotice("yes aka bernoulli<float>(.5)") << yes<float>(.5);
ofLogNotice("yes aka bernoulli<bool>(.5)") << yes<bool>(.5);
ofLogNotice("yes aka bernoulli(.5)") << yes(.5);

ofLogNotice("ofRandomYes aka bernoulli<glm::vec3>(.33)") << ofRandomYes<glm::vec3>(.33);
ofLogNotice("ofRandomYes aka bernoulli<glm::vec2>(.5)") << ofRandomYes<glm::vec2>(.5);
ofLogNotice("ofRandomYes aka bernoulli<float>(.5)") << ofRandomYes<float>(.5);
ofLogNotice("ofRandomYes aka bernoulli<bool>(.5)") << ofRandomYes<bool>(.5);
ofLogNotice("ofRandomYes aka bernoulli(.5)") << ofRandomYes(.5);
ofLogNotice("sometimes aka bernoulli<glm::vec3>(.33)") << sometimes<glm::vec3>(.33);
ofLogNotice("sometimes aka bernoulli<glm::vec2>(.5)") << sometimes<glm::vec2>(.5);
ofLogNotice("sometimes aka bernoulli<float>(.5)") << sometimes<float>(.5);
ofLogNotice("sometimes aka bernoulli<bool>(.5)") << sometimes<bool>(.5);
ofLogNotice("sometimes aka bernoulli(.5)") << sometimes(.5);

ofLogNotice("ofRandomSometimes aka bernoulli<glm::vec3>(.33)") << ofRandomSometimes<glm::vec3>(.33);
ofLogNotice("ofRandomSometimes aka bernoulli<glm::vec2>(.5)") << ofRandomSometimes<glm::vec2>(.5);
ofLogNotice("ofRandomSometimes aka bernoulli<float>(.5)") << ofRandomSometimes<float>(.5);
ofLogNotice("ofRandomSometimes aka bernoulli<bool>(.5)") << ofRandomSometimes<bool>(.5);
ofLogNotice("ofRandomSometimes aka bernoulli(.5)") << ofRandomSometimes(.5);

// lognormal requires 2 args (no defaults)

Expand Down
2 changes: 1 addition & 1 deletion libs/openFrameworks/math/ofMath.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
/// ~~~~~
///
/// \param max The maximum value of the random number.
float ofRandom(float max);
float ofRandom(float max = 1.0f);

/// \brief Get a uniform random number between two values.
///
Expand Down
6 changes: 3 additions & 3 deletions libs/openFrameworks/utils/ofRandomDistributions.h
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ constexpr auto gaussian(Args &&... args) -> decltype(normal<T>(std::forward<Args

/// \brief alias for of::random::bernoulli
template <class T = bool, typename... Args>
constexpr auto yes(Args &&... args) -> decltype(bernoulli<T>(std::forward<Args>(args)...)) {
constexpr auto sometimes(Args &&... args) -> decltype(bernoulli<T>(std::forward<Args>(args)...)) {
return bernoulli<T>(std::forward<Args>(args)...);
}

Expand Down Expand Up @@ -763,10 +763,10 @@ template <class T>
T ofRandomBernoulli(T prob) { return of::random::bernoulli<T>(prob); }

template <class T, typename... Args>
T ofRandomYes(Args &&... args) { return of::random::yes<T>(std::forward<Args>(args)...); }
T ofRandomSometimes(Args &&... args) { return of::random::sometimes<T>(std::forward<Args>(args)...); }

template <class T>
T ofRandomYes(T prob) { return of::random::yes<T>(prob); }
T ofRandomSometimes(T prob) { return of::random::sometimes<T>(prob); }

template <class T, typename... Args>
T ofRandomPoisson(Args &&... args) { return of::random::poisson<T>(std::forward<Args>(args)...); }
Expand Down
216 changes: 215 additions & 1 deletion libs/openFrameworks/utils/ofUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
#include <algorithm>
#include <bitset> // For ofToBinary.
#include <chrono>
#include <iomanip> //for setprecision
#include <iomanip> //for setprecision
#include <optional>
#include <sstream>

Expand Down Expand Up @@ -247,6 +247,220 @@ void ofShuffle(Args &&... args) {
of::shuffle(std::forward<Args>(args)...);
}

namespace of {

/// \class ofUrn
///
/// A vector-backed class that can be progressively "emptied" by randomly pulling values out of it.
/// Useful to get non-repeated patterns from a set, aka Sampling Without Replacement.
/// When the Urn is depleted, it is automatically refilled (with ways to know about such events; see below)
///
/// By default:
/// if the Urn contains unique values, repetitions are not allowed across phases;
/// if the Urn contains duplicates, repetitions are allowed.
///
/// this arbitrary behavioral choice is "practical" and can controlled with auto_configure_edge_repeat_
///
/// note that objects deposited herein get copied around when shuffling to refill; for large objects
/// TODO: an optimisation would be to shadow the objects with a vector of iterators and shuffle the iterators instead
/// (or maybe it's the user's responsibility to pass a containe of references/pointers?
/// in any case it's not clear where the threshold (qty x size) for such an optimisation lies)
///
/// \tparam T the type of contained values
/// \tparam Container the type of the underlying Container (only tested with vector; in place for future expansion)
template <class T, class Container = std::vector<T>>
class urn {

Container values_;
typename Container::const_iterator phase_;

auto prepare() {
if (valid()) {
if (auto_configure_edge_repeat_) {
std::sort(values_.begin(), values_.end()); // perhaps costly for large sets
allow_edge_repeat_ = std::adjacent_find(values_.begin(), values_.end()) != values_.end();
}
return refill();
} else {
// ofLogError("ofUrn::prepare()") << "called on unitialized Urn";
return false;
}
}

public:
/// \brief if true, repetitions are not allowed for vectors of unique values
bool allow_edge_repeat_ { false };

/// \brief if true, allow edge repeats if the vector contains duplicates
bool auto_configure_edge_repeat_ { true };

/// \brief Construct an unitialized urn
urn() = default;

/// \brief Copy-Construct an urn with contents of other urn
/// \param other the other Urn
urn(urn<T> & other) {
values_ = other.get_values();
prepare();
}

urn(urn<T> && other) noexcept
: urn(std::exchange(other.values_, nullptr)) {
prepare();
}

/// \brief move-assign an urn with another urn
/// \param other the other Urn
/// \return a new Urn
auto & operator=(urn<T> && other) noexcept {
std::swap(values_, other.values_);
prepare();
return *this;
}

/// \brief Assign-Construct an urn with contents of other urn
/// \param other the other Urn
/// \return a new Urn
auto & operator=(urn<T> & other) {
if (this == &other) return *this;
values_ = other.get_values();
prepare();
return *this;
}

/// \brief Assigns with values from compatibe container and resets the phase
/// \param Container the container of values
/// \return void
auto & operator=(Container & values) {
values_ = values;
prepare();
return *this;
}

/// \brief Construct an urn initialized with contents
/// \param Args the values
template <typename... Args>
urn(Args &&... args) {
set(std::forward<Args>(args)...);
}

~urn() = default;

/// \brief Sets values by assignement and resets the phase
/// \param Args the values
/// \return void
template <typename... Args>
auto operator=(Args &&... args) {
set(std::forward<Args>(args)...);
}

/// \brief Assigns with values from another container and resets the phase
/// \param Container the container of values
/// \return void
auto set(Container & values) {
values_ = values;
prepare();
}

/// \brief Sets the values of the container from another Urn and resets the phase
/// \param urn the other urn
/// \return void
auto set(urn<T> & urn) {
values_ = urn.get_values();
prepare();
}

/// \brief Sets the values of the container and resets the phase
/// \param Args the values
/// \return void
template <typename... Args>
auto set(Args &&... args) {
values_.clear();
values_.reserve(sizeof...(Args));
std::cout << ("preparing for") << sizeof...(Args) << std::endl;
(values_.emplace_back(std::forward<Args>(args)), ...);
prepare();
}

/// \brief Check if urn contains potential values
/// (not using the name empty to distinguish from depleted())
/// \return True if not empty
auto valid() const { return !values_.empty(); }

/// \brief Check if urn is depleted
/// (not using the name empty to distinguish from valid())
/// \return true if the phase is at the end of the container
auto depleted() const { return phase_ == values_.cend(); }

/// \brief Get the total number of elements given a full urn
/// (not using the name size to distinguish from remain())
/// \return number of elements
auto capacity() const { return values_.size(); }

/// \brief Get the remaining number of elements in phase
/// (not using the name size to distinguish from capacity())
/// \return number of remaining elements in phase
auto remain() const { return std::distance(phase_, values_.cend()); }

/// \brief Refills the urn with the original elements and resets the phase
/// \return void
auto refill() {
if (valid()) {
auto last = *values_.end();
ofShuffle(values_);
if (!allow_edge_repeat_) {
while (last == *values_.begin())
ofShuffle(values_);
}
phase_ = values_.begin();
return true;
} else {
// ofLogError("of::urn::refill()") << "called on unitialized Urn";
return false;
}
}

/// \brief Get a random element from the urn, refilling as needed
/// \return a T from the urn (could be garbage if Urn is invalid)
auto pull() {
if (valid()) {
if (depleted()) refill();
return *phase_++;
} else {
// ofLogError("of::urn::pull()") << "called on unitialized Urn -- returning unitialized T";
T v;
return v;
}
}

/// \brief Get a random element from the urn
/// \return an optional<T> from the urn, nullopt if empty or invalid
auto pull_or_empty() {
if (valid()) {
if (depleted()) return std::optional<T> {};
return std::optional<T>(*phase_++);
} else {
// ofLogError("of::urn::pull_or_empty()") << "called on unitialized Urn -- returning nullopt";
return std::optional<T> {};
}
}

/// \brief Access the internals (advanced introspection)
/// \return a reference to the underlying stuff
auto & get_values() const { return values_; }
auto & get_phase() const { return phase_; }
};

/// \brief CTAD (namely to help deduce references across the perfect forwarding)
template <class T>
urn(T) -> urn<T>;
}

/// \brief alias to ofUrn
/// note that not all deductions carry across the aliasing, so ofUrn is slightly disavantaged vs of::urn
template <typename T, typename Container = std::vector<T>>
using ofUrn = typename of::urn<T, Container>;

/// \section Vectors
/// \brief Randomly reorder the values in a vector.
/// \tparam T the type contained by the vector.
Expand Down