Skip to content
Open
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
276 changes: 242 additions & 34 deletions dwave/optimization/include/dwave-optimization/functional.hpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,45 +15,185 @@
#pragma once

#include <algorithm>
#include <cassert>
#include <cmath>
#include <concepts>
#include <cstdlib>
#include <limits>
#include <utility>

#include "dwave-optimization/interval.hpp"
#include "dwave-optimization/typing.hpp"

namespace dwave::optimization::functional {

template <class T>
struct abs {
static constexpr T operator()(const T& x) { return std::abs(x); }
enum class Monotonicity { Decreasing = -1, None = 0, Increasing = 1 };

template <typename UnaryOp>
struct UnaryOpMixin {
template <DType T>
requires(UnaryOp::monotonic != Monotonicity::None)
static auto operator()(const interval<T>& domain) {
using return_type = interval<decltype(UnaryOp::operator()(T()))>;

// op(empty domain) -> empty domain
if (not static_cast<bool>(domain)) return return_type();

assert(
domain <= UnaryOp::template domain<T> and
"input domain must be a subset of the func's domain"
);

// We don't worry about outward rounding here because this overload is meant
// to reflect the behavior of the scalar overload, not necessarily to be
// mathematically correct.
// We *do* assume that UnaryOp (e.g., std::exp()) is monotonic, which
// is not always true, but I think it's an OK assumption for our purposes.
if constexpr (UnaryOp::monotonic == Monotonicity::Increasing) {
return return_type(
UnaryOp::operator()(domain.infimum), UnaryOp::operator()(domain.supremum)
);
} else if constexpr (UnaryOp::monotonic == Monotonicity::Decreasing) {
return return_type(
UnaryOp::operator()(domain.supremum), UnaryOp::operator()(domain.infimum)
);
} else {
assert(false and "unexpected monotonicity");
std::unreachable();
}
}

template <DType T>
static constexpr interval<T> domain = interval<T>::all();
};

template <class T>
struct cos {
static auto operator()(const T& num) { return std::cos(num); }
struct absolute : UnaryOpMixin<absolute> {
template <DType T>
static T operator()(const T& x) {
// Unlike NumPy/std, we define std::abs(INT_MIN) to equal INT_MAX under the reasoning
// that it's more important to us to preserve the sign than to preseve the correct value.
if constexpr (std::integral<T>) {
if (x == std::numeric_limits<T>::lowest()) return std::numeric_limits<T>::max();
}

// std::abs() is not defined for int8 or int16 so we static_cast to avoid widening.
return static_cast<T>(std::abs(x));
}
static bool operator()(const bool& x) { return x; }

template <DType T>
static interval<T> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

assert(domain.infimum <= domain.supremum); // implied by non-empty

// If the domain is non-negative, then absolute is identity
if (0 <= domain.infimum) return domain;

// If the domain is negative, then absolute is just the inverse
if (domain.supremum < 0) return -domain;

// Otherwise, the domain straddles 0

// Handle the -INT_MIN case. Again we treat abs(-INT_MIN) as INT_MAX under the reasoning
// that [INT_MIN, ...] is probably intended to mean unbounded.
if constexpr (std::integral<T>) {
if (domain.infimum == std::numeric_limits<T>::lowest()) {
return interval<T>(0, std::numeric_limits<T>::max());
}
}

return interval<T>(
0, -domain.infimum < domain.supremum ? domain.supremum : -domain.infimum
);
}
static interval<bool> operator()(const interval<bool>& domain) { return domain; }

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct exp {
static constexpr auto operator()(const T& x) { return std::exp(x); }
struct cos : UnaryOpMixin<cos> {
static auto operator()(const DType auto& x) { return std::cos(x); }

template <DType T>
static interval<decltype(std::cos(T()))> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

// It is possible to be a lot more specific than this by checking whether
// our domain spans a full period or not, but I think this is of dubious
// benefit to the user so for now we just return [-1, +1]
return {-1, +1};
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct expit {
static constexpr double operator()(const T& x) { return 1.0 / (1.0 + std::exp(-1. * x)); }
struct exp : UnaryOpMixin<exp> {
static auto operator()(const DType auto& x) { return std::exp(x); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct log {
static constexpr auto operator()(const T& x) { return std::log(x); }
struct expit : UnaryOpMixin<expit> {
template <DType T>
static auto operator()(const T& x) {
return 1 / (1 + std::exp(-x));
}
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct logical {
static constexpr bool operator()(const T& x) { return x; }
struct log : UnaryOpMixin<log> {
template <DType T>
static auto operator()(const T& x) {
assert(domain<T>.contains(x) and "x must be non-negative");
return std::log(x);
}
using UnaryOpMixin::operator();

template <DType T>
static constexpr interval<T> domain = interval<T>::nonnegative();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

struct logical : UnaryOpMixin<logical> {
static bool operator()(const DType auto& x) { return x; }

static interval<bool> operator()(const interval<bool>& domain) { return domain; }
template <DType T>
static interval<bool> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

if (domain.infimum == 0 and domain.supremum == 0) return interval(false, false);
if (domain.infimum <= 0 and domain.supremum >= 0) return interval(false, true);
return interval(true, true);
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

struct logical_not : UnaryOpMixin<logical_not> {
static bool operator()(const DType auto& x) { return not x; }

static interval<bool> operator()(const interval<bool>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain
return interval(not domain.supremum, not domain.infimum);
}
template <DType T>
static interval<bool> operator()(const interval<T>& domain) {
// Call the more specific interval<bool> overload
return operator()(logical{}(domain));
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct logical_xor {
static constexpr bool operator()(const T& x, const T& y) {
static bool operator()(const T& x, const T& y) {
return static_cast<bool>(x) != static_cast<bool>(y);
}
};
Expand DownExpand Up@@ -90,9 +230,28 @@ struct modulus {
}
};

template <class T>
struct rint {
static constexpr auto operator()(const T& x) { return std::rint(x); }
struct negative : UnaryOpMixin<negative> {
template <class T>
requires(DType<T> and not std::same_as<T, bool>) // not defined for bool
static auto operator()(const T& x) {
// We define -INT_MIN to equal INT_MAX under the reasoning that it's more
// important to us to preserve the sign than to preseve the correct value.
if constexpr (std::integral<T>) {
if (x == std::numeric_limits<T>::lowest()) return std::numeric_limits<T>::max();
}

return static_cast<T>(-x); // so it doesn't widen e.g., int8_t->int
}
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Decreasing;
};

struct rint : UnaryOpMixin<rint> {
static auto operator()(const DType auto& x) { return std::rint(x); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
Expand All@@ -103,24 +262,73 @@ struct safe_divides {
}
};

template <class T>
struct sin {
static auto operator()(const T& num) { return std::sin(num); }
struct sin : UnaryOpMixin<sin> {
static auto operator()(const DType auto& x) { return std::sin(x); }

template <DType T>
static interval<decltype(std::sin(T()))> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

// It is possible to be a lot more specific than this by checking whether
// our domain spans a full period or not, but I think this is of dubious
// benefit to the user so for now we just return [-1, +1]
return {-1, +1};
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct square {
static constexpr T operator()(const T& x) { return x * x; }
struct square : UnaryOpMixin<square> {
template <DType T>
static T operator()(const T& x) {
return x * x;
}
static bool operator()(const bool& x) { return x; }

template <DType T>
static interval<T> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

assert(domain.infimum <= domain.supremum); // implied by non-empty

square op{};
T inf_squared = op(domain.infimum);
T sup_squared = op(domain.supremum);

// Non-negative domain: square is increasing
if (0 <= domain.infimum) return interval<T>(inf_squared, sup_squared);

// Non-positive domain: square is decreasing
if (domain.supremum <= 0) return interval<T>(sup_squared, inf_squared);

// Otherwise the domain straddles 0: minimum is 0, maximum is the larger squared endpoint.

return interval<T>(0, inf_squared < sup_squared ? sup_squared : inf_squared);
}
static interval<bool> operator()(const interval<bool>& domain) { return domain; }

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct square_root {
static constexpr auto operator()(const T& x) { return std::sqrt(x); }
struct square_root : UnaryOpMixin<square_root> {
template <DType T>
static auto operator()(const T& x) {
assert(domain<T>.contains(x) and "x must be non-negative");
return std::sqrt(x);
}
using UnaryOpMixin::operator();

template <DType T>
static constexpr interval<T> domain = interval<T>::nonnegative();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct tanh {
static auto operator()(const T& num) { return std::tanh(num); }
struct tanh : UnaryOpMixin<tanh> {
static auto operator()(const DType auto& num) { return std::tanh(num); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

} // namespace dwave::optimization::functional
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Rework `UnaryOpNode` by arcondello · Pull Request #627 · dwavesystems/dwave-optimization · GitHub
Skip to content
Open
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
276 changes: 242 additions & 34 deletions dwave/optimization/include/dwave-optimization/functional.hpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,45 +15,185 @@
#pragma once

#include <algorithm>
#include <cassert>
#include <cmath>
#include <concepts>
#include <cstdlib>
#include <limits>
#include <utility>

#include "dwave-optimization/interval.hpp"
#include "dwave-optimization/typing.hpp"

namespace dwave::optimization::functional {

template <class T>
struct abs {
static constexpr T operator()(const T& x) { return std::abs(x); }
enum class Monotonicity { Decreasing = -1, None = 0, Increasing = 1 };

template <typename UnaryOp>
struct UnaryOpMixin {
template <DType T>
requires(UnaryOp::monotonic != Monotonicity::None)
static auto operator()(const interval<T>& domain) {
using return_type = interval<decltype(UnaryOp::operator()(T()))>;

// op(empty domain) -> empty domain
if (not static_cast<bool>(domain)) return return_type();

assert(
domain <= UnaryOp::template domain<T> and
"input domain must be a subset of the func's domain"
);

// We don't worry about outward rounding here because this overload is meant
// to reflect the behavior of the scalar overload, not necessarily to be
// mathematically correct.
// We *do* assume that UnaryOp (e.g., std::exp()) is monotonic, which
// is not always true, but I think it's an OK assumption for our purposes.
if constexpr (UnaryOp::monotonic == Monotonicity::Increasing) {
return return_type(
UnaryOp::operator()(domain.infimum), UnaryOp::operator()(domain.supremum)
);
} else if constexpr (UnaryOp::monotonic == Monotonicity::Decreasing) {
return return_type(
UnaryOp::operator()(domain.supremum), UnaryOp::operator()(domain.infimum)
);
} else {
assert(false and "unexpected monotonicity");
std::unreachable();
}
}

template <DType T>
static constexpr interval<T> domain = interval<T>::all();
};

template <class T>
struct cos {
static auto operator()(const T& num) { return std::cos(num); }
struct absolute : UnaryOpMixin<absolute> {
template <DType T>
static T operator()(const T& x) {
// Unlike NumPy/std, we define std::abs(INT_MIN) to equal INT_MAX under the reasoning
// that it's more important to us to preserve the sign than to preseve the correct value.
if constexpr (std::integral<T>) {
if (x == std::numeric_limits<T>::lowest()) return std::numeric_limits<T>::max();
}

// std::abs() is not defined for int8 or int16 so we static_cast to avoid widening.
return static_cast<T>(std::abs(x));
}
static bool operator()(const bool& x) { return x; }

template <DType T>
static interval<T> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

assert(domain.infimum <= domain.supremum); // implied by non-empty

// If the domain is non-negative, then absolute is identity
if (0 <= domain.infimum) return domain;

// If the domain is negative, then absolute is just the inverse
if (domain.supremum < 0) return -domain;

// Otherwise, the domain straddles 0

// Handle the -INT_MIN case. Again we treat abs(-INT_MIN) as INT_MAX under the reasoning
// that [INT_MIN, ...] is probably intended to mean unbounded.
if constexpr (std::integral<T>) {
if (domain.infimum == std::numeric_limits<T>::lowest()) {
return interval<T>(0, std::numeric_limits<T>::max());
}
}

return interval<T>(
0, -domain.infimum < domain.supremum ? domain.supremum : -domain.infimum
);
}
static interval<bool> operator()(const interval<bool>& domain) { return domain; }

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct exp {
static constexpr auto operator()(const T& x) { return std::exp(x); }
struct cos : UnaryOpMixin<cos> {
static auto operator()(const DType auto& x) { return std::cos(x); }

template <DType T>
static interval<decltype(std::cos(T()))> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

// It is possible to be a lot more specific than this by checking whether
// our domain spans a full period or not, but I think this is of dubious
// benefit to the user so for now we just return [-1, +1]
return {-1, +1};
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct expit {
static constexpr double operator()(const T& x) { return 1.0 / (1.0 + std::exp(-1. * x)); }
struct exp : UnaryOpMixin<exp> {
static auto operator()(const DType auto& x) { return std::exp(x); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct log {
static constexpr auto operator()(const T& x) { return std::log(x); }
struct expit : UnaryOpMixin<expit> {
template <DType T>
static auto operator()(const T& x) {
return 1 / (1 + std::exp(-x));
}
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct logical {
static constexpr bool operator()(const T& x) { return x; }
struct log : UnaryOpMixin<log> {
template <DType T>
static auto operator()(const T& x) {
assert(domain<T>.contains(x) and "x must be non-negative");
return std::log(x);
}
using UnaryOpMixin::operator();

template <DType T>
static constexpr interval<T> domain = interval<T>::nonnegative();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

struct logical : UnaryOpMixin<logical> {
static bool operator()(const DType auto& x) { return x; }

static interval<bool> operator()(const interval<bool>& domain) { return domain; }
template <DType T>
static interval<bool> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

if (domain.infimum == 0 and domain.supremum == 0) return interval(false, false);
if (domain.infimum <= 0 and domain.supremum >= 0) return interval(false, true);
return interval(true, true);
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

struct logical_not : UnaryOpMixin<logical_not> {
static bool operator()(const DType auto& x) { return not x; }

static interval<bool> operator()(const interval<bool>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain
return interval(not domain.supremum, not domain.infimum);
}
template <DType T>
static interval<bool> operator()(const interval<T>& domain) {
// Call the more specific interval<bool> overload
return operator()(logical{}(domain));
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct logical_xor {
static constexpr bool operator()(const T& x, const T& y) {
static bool operator()(const T& x, const T& y) {
return static_cast<bool>(x) != static_cast<bool>(y);
}
};
Expand DownExpand Up@@ -90,9 +230,28 @@ struct modulus {
}
};

template <class T>
struct rint {
static constexpr auto operator()(const T& x) { return std::rint(x); }
struct negative : UnaryOpMixin<negative> {
template <class T>
requires(DType<T> and not std::same_as<T, bool>) // not defined for bool
static auto operator()(const T& x) {
// We define -INT_MIN to equal INT_MAX under the reasoning that it's more
// important to us to preserve the sign than to preseve the correct value.
if constexpr (std::integral<T>) {
if (x == std::numeric_limits<T>::lowest()) return std::numeric_limits<T>::max();
}

return static_cast<T>(-x); // so it doesn't widen e.g., int8_t->int
}
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Decreasing;
};

struct rint : UnaryOpMixin<rint> {
static auto operator()(const DType auto& x) { return std::rint(x); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
Expand All@@ -103,24 +262,73 @@ struct safe_divides {
}
};

template <class T>
struct sin {
static auto operator()(const T& num) { return std::sin(num); }
struct sin : UnaryOpMixin<sin> {
static auto operator()(const DType auto& x) { return std::sin(x); }

template <DType T>
static interval<decltype(std::sin(T()))> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

// It is possible to be a lot more specific than this by checking whether
// our domain spans a full period or not, but I think this is of dubious
// benefit to the user so for now we just return [-1, +1]
return {-1, +1};
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct square {
static constexpr T operator()(const T& x) { return x * x; }
struct square : UnaryOpMixin<square> {
template <DType T>
static T operator()(const T& x) {
return x * x;
}
static bool operator()(const bool& x) { return x; }

template <DType T>
static interval<T> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

assert(domain.infimum <= domain.supremum); // implied by non-empty

square op{};
T inf_squared = op(domain.infimum);
T sup_squared = op(domain.supremum);

// Non-negative domain: square is increasing
if (0 <= domain.infimum) return interval<T>(inf_squared, sup_squared);

// Non-positive domain: square is decreasing
if (domain.supremum <= 0) return interval<T>(sup_squared, inf_squared);

// Otherwise the domain straddles 0: minimum is 0, maximum is the larger squared endpoint.

return interval<T>(0, inf_squared < sup_squared ? sup_squared : inf_squared);
}
static interval<bool> operator()(const interval<bool>& domain) { return domain; }

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct square_root {
static constexpr auto operator()(const T& x) { return std::sqrt(x); }
struct square_root : UnaryOpMixin<square_root> {
template <DType T>
static auto operator()(const T& x) {
assert(domain<T>.contains(x) and "x must be non-negative");
return std::sqrt(x);
}
using UnaryOpMixin::operator();

template <DType T>
static constexpr interval<T> domain = interval<T>::nonnegative();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct tanh {
static auto operator()(const T& num) { return std::tanh(num); }
struct tanh : UnaryOpMixin<tanh> {
static auto operator()(const DType auto& num) { return std::tanh(num); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

} // namespace dwave::optimization::functional
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Rework `UnaryOpNode` by arcondello · Pull Request #627 · dwavesystems/dwave-optimization · GitHub
Skip to content
Open
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
276 changes: 242 additions & 34 deletions dwave/optimization/include/dwave-optimization/functional.hpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,45 +15,185 @@
#pragma once

#include <algorithm>
#include <cassert>
#include <cmath>
#include <concepts>
#include <cstdlib>
#include <limits>
#include <utility>

#include "dwave-optimization/interval.hpp"
#include "dwave-optimization/typing.hpp"

namespace dwave::optimization::functional {

template <class T>
struct abs {
static constexpr T operator()(const T& x) { return std::abs(x); }
enum class Monotonicity { Decreasing = -1, None = 0, Increasing = 1 };

template <typename UnaryOp>
struct UnaryOpMixin {
template <DType T>
requires(UnaryOp::monotonic != Monotonicity::None)
static auto operator()(const interval<T>& domain) {
using return_type = interval<decltype(UnaryOp::operator()(T()))>;

// op(empty domain) -> empty domain
if (not static_cast<bool>(domain)) return return_type();

assert(
domain <= UnaryOp::template domain<T> and
"input domain must be a subset of the func's domain"
);

// We don't worry about outward rounding here because this overload is meant
// to reflect the behavior of the scalar overload, not necessarily to be
// mathematically correct.
// We *do* assume that UnaryOp (e.g., std::exp()) is monotonic, which
// is not always true, but I think it's an OK assumption for our purposes.
if constexpr (UnaryOp::monotonic == Monotonicity::Increasing) {
return return_type(
UnaryOp::operator()(domain.infimum), UnaryOp::operator()(domain.supremum)
);
} else if constexpr (UnaryOp::monotonic == Monotonicity::Decreasing) {
return return_type(
UnaryOp::operator()(domain.supremum), UnaryOp::operator()(domain.infimum)
);
} else {
assert(false and "unexpected monotonicity");
std::unreachable();
}
}

template <DType T>
static constexpr interval<T> domain = interval<T>::all();
};

template <class T>
struct cos {
static auto operator()(const T& num) { return std::cos(num); }
struct absolute : UnaryOpMixin<absolute> {
template <DType T>
static T operator()(const T& x) {
// Unlike NumPy/std, we define std::abs(INT_MIN) to equal INT_MAX under the reasoning
// that it's more important to us to preserve the sign than to preseve the correct value.
if constexpr (std::integral<T>) {
if (x == std::numeric_limits<T>::lowest()) return std::numeric_limits<T>::max();
}

// std::abs() is not defined for int8 or int16 so we static_cast to avoid widening.
return static_cast<T>(std::abs(x));
}
static bool operator()(const bool& x) { return x; }

template <DType T>
static interval<T> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

assert(domain.infimum <= domain.supremum); // implied by non-empty

// If the domain is non-negative, then absolute is identity
if (0 <= domain.infimum) return domain;

// If the domain is negative, then absolute is just the inverse
if (domain.supremum < 0) return -domain;

// Otherwise, the domain straddles 0

// Handle the -INT_MIN case. Again we treat abs(-INT_MIN) as INT_MAX under the reasoning
// that [INT_MIN, ...] is probably intended to mean unbounded.
if constexpr (std::integral<T>) {
if (domain.infimum == std::numeric_limits<T>::lowest()) {
return interval<T>(0, std::numeric_limits<T>::max());
}
}

return interval<T>(
0, -domain.infimum < domain.supremum ? domain.supremum : -domain.infimum
);
}
static interval<bool> operator()(const interval<bool>& domain) { return domain; }

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct exp {
static constexpr auto operator()(const T& x) { return std::exp(x); }
struct cos : UnaryOpMixin<cos> {
static auto operator()(const DType auto& x) { return std::cos(x); }

template <DType T>
static interval<decltype(std::cos(T()))> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

// It is possible to be a lot more specific than this by checking whether
// our domain spans a full period or not, but I think this is of dubious
// benefit to the user so for now we just return [-1, +1]
return {-1, +1};
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct expit {
static constexpr double operator()(const T& x) { return 1.0 / (1.0 + std::exp(-1. * x)); }
struct exp : UnaryOpMixin<exp> {
static auto operator()(const DType auto& x) { return std::exp(x); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct log {
static constexpr auto operator()(const T& x) { return std::log(x); }
struct expit : UnaryOpMixin<expit> {
template <DType T>
static auto operator()(const T& x) {
return 1 / (1 + std::exp(-x));
}
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct logical {
static constexpr bool operator()(const T& x) { return x; }
struct log : UnaryOpMixin<log> {
template <DType T>
static auto operator()(const T& x) {
assert(domain<T>.contains(x) and "x must be non-negative");
return std::log(x);
}
using UnaryOpMixin::operator();

template <DType T>
static constexpr interval<T> domain = interval<T>::nonnegative();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

struct logical : UnaryOpMixin<logical> {
static bool operator()(const DType auto& x) { return x; }

static interval<bool> operator()(const interval<bool>& domain) { return domain; }
template <DType T>
static interval<bool> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

if (domain.infimum == 0 and domain.supremum == 0) return interval(false, false);
if (domain.infimum <= 0 and domain.supremum >= 0) return interval(false, true);
return interval(true, true);
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

struct logical_not : UnaryOpMixin<logical_not> {
static bool operator()(const DType auto& x) { return not x; }

static interval<bool> operator()(const interval<bool>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain
return interval(not domain.supremum, not domain.infimum);
}
template <DType T>
static interval<bool> operator()(const interval<T>& domain) {
// Call the more specific interval<bool> overload
return operator()(logical{}(domain));
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct logical_xor {
static constexpr bool operator()(const T& x, const T& y) {
static bool operator()(const T& x, const T& y) {
return static_cast<bool>(x) != static_cast<bool>(y);
}
};
Expand DownExpand Up@@ -90,9 +230,28 @@ struct modulus {
}
};

template <class T>
struct rint {
static constexpr auto operator()(const T& x) { return std::rint(x); }
struct negative : UnaryOpMixin<negative> {
template <class T>
requires(DType<T> and not std::same_as<T, bool>) // not defined for bool
static auto operator()(const T& x) {
// We define -INT_MIN to equal INT_MAX under the reasoning that it's more
// important to us to preserve the sign than to preseve the correct value.
if constexpr (std::integral<T>) {
if (x == std::numeric_limits<T>::lowest()) return std::numeric_limits<T>::max();
}

return static_cast<T>(-x); // so it doesn't widen e.g., int8_t->int
}
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Decreasing;
};

struct rint : UnaryOpMixin<rint> {
static auto operator()(const DType auto& x) { return std::rint(x); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
Expand All@@ -103,24 +262,73 @@ struct safe_divides {
}
};

template <class T>
struct sin {
static auto operator()(const T& num) { return std::sin(num); }
struct sin : UnaryOpMixin<sin> {
static auto operator()(const DType auto& x) { return std::sin(x); }

template <DType T>
static interval<decltype(std::sin(T()))> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

// It is possible to be a lot more specific than this by checking whether
// our domain spans a full period or not, but I think this is of dubious
// benefit to the user so for now we just return [-1, +1]
return {-1, +1};
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct square {
static constexpr T operator()(const T& x) { return x * x; }
struct square : UnaryOpMixin<square> {
template <DType T>
static T operator()(const T& x) {
return x * x;
}
static bool operator()(const bool& x) { return x; }

template <DType T>
static interval<T> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

assert(domain.infimum <= domain.supremum); // implied by non-empty

square op{};
T inf_squared = op(domain.infimum);
T sup_squared = op(domain.supremum);

// Non-negative domain: square is increasing
if (0 <= domain.infimum) return interval<T>(inf_squared, sup_squared);

// Non-positive domain: square is decreasing
if (domain.supremum <= 0) return interval<T>(sup_squared, inf_squared);

// Otherwise the domain straddles 0: minimum is 0, maximum is the larger squared endpoint.

return interval<T>(0, inf_squared < sup_squared ? sup_squared : inf_squared);
}
static interval<bool> operator()(const interval<bool>& domain) { return domain; }

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct square_root {
static constexpr auto operator()(const T& x) { return std::sqrt(x); }
struct square_root : UnaryOpMixin<square_root> {
template <DType T>
static auto operator()(const T& x) {
assert(domain<T>.contains(x) and "x must be non-negative");
return std::sqrt(x);
}
using UnaryOpMixin::operator();

template <DType T>
static constexpr interval<T> domain = interval<T>::nonnegative();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct tanh {
static auto operator()(const T& num) { return std::tanh(num); }
struct tanh : UnaryOpMixin<tanh> {
static auto operator()(const DType auto& num) { return std::tanh(num); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

} // namespace dwave::optimization::functional
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Rework `UnaryOpNode` by arcondello · Pull Request #627 · dwavesystems/dwave-optimization · GitHub
Skip to content
Open
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
276 changes: 242 additions & 34 deletions dwave/optimization/include/dwave-optimization/functional.hpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,45 +15,185 @@
#pragma once

#include <algorithm>
#include <cassert>
#include <cmath>
#include <concepts>
#include <cstdlib>
#include <limits>
#include <utility>

#include "dwave-optimization/interval.hpp"
#include "dwave-optimization/typing.hpp"

namespace dwave::optimization::functional {

template <class T>
struct abs {
static constexpr T operator()(const T& x) { return std::abs(x); }
enum class Monotonicity { Decreasing = -1, None = 0, Increasing = 1 };

template <typename UnaryOp>
struct UnaryOpMixin {
template <DType T>
requires(UnaryOp::monotonic != Monotonicity::None)
static auto operator()(const interval<T>& domain) {
using return_type = interval<decltype(UnaryOp::operator()(T()))>;

// op(empty domain) -> empty domain
if (not static_cast<bool>(domain)) return return_type();

assert(
domain <= UnaryOp::template domain<T> and
"input domain must be a subset of the func's domain"
);

// We don't worry about outward rounding here because this overload is meant
// to reflect the behavior of the scalar overload, not necessarily to be
// mathematically correct.
// We *do* assume that UnaryOp (e.g., std::exp()) is monotonic, which
// is not always true, but I think it's an OK assumption for our purposes.
if constexpr (UnaryOp::monotonic == Monotonicity::Increasing) {
return return_type(
UnaryOp::operator()(domain.infimum), UnaryOp::operator()(domain.supremum)
);
} else if constexpr (UnaryOp::monotonic == Monotonicity::Decreasing) {
return return_type(
UnaryOp::operator()(domain.supremum), UnaryOp::operator()(domain.infimum)
);
} else {
assert(false and "unexpected monotonicity");
std::unreachable();
}
}

template <DType T>
static constexpr interval<T> domain = interval<T>::all();
};

template <class T>
struct cos {
static auto operator()(const T& num) { return std::cos(num); }
struct absolute : UnaryOpMixin<absolute> {
template <DType T>
static T operator()(const T& x) {
// Unlike NumPy/std, we define std::abs(INT_MIN) to equal INT_MAX under the reasoning
// that it's more important to us to preserve the sign than to preseve the correct value.
if constexpr (std::integral<T>) {
if (x == std::numeric_limits<T>::lowest()) return std::numeric_limits<T>::max();
}

// std::abs() is not defined for int8 or int16 so we static_cast to avoid widening.
return static_cast<T>(std::abs(x));
}
static bool operator()(const bool& x) { return x; }

template <DType T>
static interval<T> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

assert(domain.infimum <= domain.supremum); // implied by non-empty

// If the domain is non-negative, then absolute is identity
if (0 <= domain.infimum) return domain;

// If the domain is negative, then absolute is just the inverse
if (domain.supremum < 0) return -domain;

// Otherwise, the domain straddles 0

// Handle the -INT_MIN case. Again we treat abs(-INT_MIN) as INT_MAX under the reasoning
// that [INT_MIN, ...] is probably intended to mean unbounded.
if constexpr (std::integral<T>) {
if (domain.infimum == std::numeric_limits<T>::lowest()) {
return interval<T>(0, std::numeric_limits<T>::max());
}
}

return interval<T>(
0, -domain.infimum < domain.supremum ? domain.supremum : -domain.infimum
);
}
static interval<bool> operator()(const interval<bool>& domain) { return domain; }

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct exp {
static constexpr auto operator()(const T& x) { return std::exp(x); }
struct cos : UnaryOpMixin<cos> {
static auto operator()(const DType auto& x) { return std::cos(x); }

template <DType T>
static interval<decltype(std::cos(T()))> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

// It is possible to be a lot more specific than this by checking whether
// our domain spans a full period or not, but I think this is of dubious
// benefit to the user so for now we just return [-1, +1]
return {-1, +1};
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct expit {
static constexpr double operator()(const T& x) { return 1.0 / (1.0 + std::exp(-1. * x)); }
struct exp : UnaryOpMixin<exp> {
static auto operator()(const DType auto& x) { return std::exp(x); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct log {
static constexpr auto operator()(const T& x) { return std::log(x); }
struct expit : UnaryOpMixin<expit> {
template <DType T>
static auto operator()(const T& x) {
return 1 / (1 + std::exp(-x));
}
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct logical {
static constexpr bool operator()(const T& x) { return x; }
struct log : UnaryOpMixin<log> {
template <DType T>
static auto operator()(const T& x) {
assert(domain<T>.contains(x) and "x must be non-negative");
return std::log(x);
}
using UnaryOpMixin::operator();

template <DType T>
static constexpr interval<T> domain = interval<T>::nonnegative();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

struct logical : UnaryOpMixin<logical> {
static bool operator()(const DType auto& x) { return x; }

static interval<bool> operator()(const interval<bool>& domain) { return domain; }
template <DType T>
static interval<bool> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

if (domain.infimum == 0 and domain.supremum == 0) return interval(false, false);
if (domain.infimum <= 0 and domain.supremum >= 0) return interval(false, true);
return interval(true, true);
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

struct logical_not : UnaryOpMixin<logical_not> {
static bool operator()(const DType auto& x) { return not x; }

static interval<bool> operator()(const interval<bool>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain
return interval(not domain.supremum, not domain.infimum);
}
template <DType T>
static interval<bool> operator()(const interval<T>& domain) {
// Call the more specific interval<bool> overload
return operator()(logical{}(domain));
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct logical_xor {
static constexpr bool operator()(const T& x, const T& y) {
static bool operator()(const T& x, const T& y) {
return static_cast<bool>(x) != static_cast<bool>(y);
}
};
Expand DownExpand Up@@ -90,9 +230,28 @@ struct modulus {
}
};

template <class T>
struct rint {
static constexpr auto operator()(const T& x) { return std::rint(x); }
struct negative : UnaryOpMixin<negative> {
template <class T>
requires(DType<T> and not std::same_as<T, bool>) // not defined for bool
static auto operator()(const T& x) {
// We define -INT_MIN to equal INT_MAX under the reasoning that it's more
// important to us to preserve the sign than to preseve the correct value.
if constexpr (std::integral<T>) {
if (x == std::numeric_limits<T>::lowest()) return std::numeric_limits<T>::max();
}

return static_cast<T>(-x); // so it doesn't widen e.g., int8_t->int
}
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Decreasing;
};

struct rint : UnaryOpMixin<rint> {
static auto operator()(const DType auto& x) { return std::rint(x); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
Expand All@@ -103,24 +262,73 @@ struct safe_divides {
}
};

template <class T>
struct sin {
static auto operator()(const T& num) { return std::sin(num); }
struct sin : UnaryOpMixin<sin> {
static auto operator()(const DType auto& x) { return std::sin(x); }

template <DType T>
static interval<decltype(std::sin(T()))> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

// It is possible to be a lot more specific than this by checking whether
// our domain spans a full period or not, but I think this is of dubious
// benefit to the user so for now we just return [-1, +1]
return {-1, +1};
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct square {
static constexpr T operator()(const T& x) { return x * x; }
struct square : UnaryOpMixin<square> {
template <DType T>
static T operator()(const T& x) {
return x * x;
}
static bool operator()(const bool& x) { return x; }

template <DType T>
static interval<T> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

assert(domain.infimum <= domain.supremum); // implied by non-empty

square op{};
T inf_squared = op(domain.infimum);
T sup_squared = op(domain.supremum);

// Non-negative domain: square is increasing
if (0 <= domain.infimum) return interval<T>(inf_squared, sup_squared);

// Non-positive domain: square is decreasing
if (domain.supremum <= 0) return interval<T>(sup_squared, inf_squared);

// Otherwise the domain straddles 0: minimum is 0, maximum is the larger squared endpoint.

return interval<T>(0, inf_squared < sup_squared ? sup_squared : inf_squared);
}
static interval<bool> operator()(const interval<bool>& domain) { return domain; }

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct square_root {
static constexpr auto operator()(const T& x) { return std::sqrt(x); }
struct square_root : UnaryOpMixin<square_root> {
template <DType T>
static auto operator()(const T& x) {
assert(domain<T>.contains(x) and "x must be non-negative");
return std::sqrt(x);
}
using UnaryOpMixin::operator();

template <DType T>
static constexpr interval<T> domain = interval<T>::nonnegative();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct tanh {
static auto operator()(const T& num) { return std::tanh(num); }
struct tanh : UnaryOpMixin<tanh> {
static auto operator()(const DType auto& num) { return std::tanh(num); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

} // namespace dwave::optimization::functional
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Rework `UnaryOpNode` by arcondello · Pull Request #627 · dwavesystems/dwave-optimization · GitHub
Skip to content
Open
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
276 changes: 242 additions & 34 deletions dwave/optimization/include/dwave-optimization/functional.hpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,45 +15,185 @@
#pragma once

#include <algorithm>
#include <cassert>
#include <cmath>
#include <concepts>
#include <cstdlib>
#include <limits>
#include <utility>

#include "dwave-optimization/interval.hpp"
#include "dwave-optimization/typing.hpp"

namespace dwave::optimization::functional {

template <class T>
struct abs {
static constexpr T operator()(const T& x) { return std::abs(x); }
enum class Monotonicity { Decreasing = -1, None = 0, Increasing = 1 };

template <typename UnaryOp>
struct UnaryOpMixin {
template <DType T>
requires(UnaryOp::monotonic != Monotonicity::None)
static auto operator()(const interval<T>& domain) {
using return_type = interval<decltype(UnaryOp::operator()(T()))>;

// op(empty domain) -> empty domain
if (not static_cast<bool>(domain)) return return_type();

assert(
domain <= UnaryOp::template domain<T> and
"input domain must be a subset of the func's domain"
);

// We don't worry about outward rounding here because this overload is meant
// to reflect the behavior of the scalar overload, not necessarily to be
// mathematically correct.
// We *do* assume that UnaryOp (e.g., std::exp()) is monotonic, which
// is not always true, but I think it's an OK assumption for our purposes.
if constexpr (UnaryOp::monotonic == Monotonicity::Increasing) {
return return_type(
UnaryOp::operator()(domain.infimum), UnaryOp::operator()(domain.supremum)
);
} else if constexpr (UnaryOp::monotonic == Monotonicity::Decreasing) {
return return_type(
UnaryOp::operator()(domain.supremum), UnaryOp::operator()(domain.infimum)
);
} else {
assert(false and "unexpected monotonicity");
std::unreachable();
}
}

template <DType T>
static constexpr interval<T> domain = interval<T>::all();
};

template <class T>
struct cos {
static auto operator()(const T& num) { return std::cos(num); }
struct absolute : UnaryOpMixin<absolute> {
template <DType T>
static T operator()(const T& x) {
// Unlike NumPy/std, we define std::abs(INT_MIN) to equal INT_MAX under the reasoning
// that it's more important to us to preserve the sign than to preseve the correct value.
if constexpr (std::integral<T>) {
if (x == std::numeric_limits<T>::lowest()) return std::numeric_limits<T>::max();
}

// std::abs() is not defined for int8 or int16 so we static_cast to avoid widening.
return static_cast<T>(std::abs(x));
}
static bool operator()(const bool& x) { return x; }

template <DType T>
static interval<T> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

assert(domain.infimum <= domain.supremum); // implied by non-empty

// If the domain is non-negative, then absolute is identity
if (0 <= domain.infimum) return domain;

// If the domain is negative, then absolute is just the inverse
if (domain.supremum < 0) return -domain;

// Otherwise, the domain straddles 0

// Handle the -INT_MIN case. Again we treat abs(-INT_MIN) as INT_MAX under the reasoning
// that [INT_MIN, ...] is probably intended to mean unbounded.
if constexpr (std::integral<T>) {
if (domain.infimum == std::numeric_limits<T>::lowest()) {
return interval<T>(0, std::numeric_limits<T>::max());
}
}

return interval<T>(
0, -domain.infimum < domain.supremum ? domain.supremum : -domain.infimum
);
}
static interval<bool> operator()(const interval<bool>& domain) { return domain; }

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct exp {
static constexpr auto operator()(const T& x) { return std::exp(x); }
struct cos : UnaryOpMixin<cos> {
static auto operator()(const DType auto& x) { return std::cos(x); }

template <DType T>
static interval<decltype(std::cos(T()))> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

// It is possible to be a lot more specific than this by checking whether
// our domain spans a full period or not, but I think this is of dubious
// benefit to the user so for now we just return [-1, +1]
return {-1, +1};
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct expit {
static constexpr double operator()(const T& x) { return 1.0 / (1.0 + std::exp(-1. * x)); }
struct exp : UnaryOpMixin<exp> {
static auto operator()(const DType auto& x) { return std::exp(x); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct log {
static constexpr auto operator()(const T& x) { return std::log(x); }
struct expit : UnaryOpMixin<expit> {
template <DType T>
static auto operator()(const T& x) {
return 1 / (1 + std::exp(-x));
}
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct logical {
static constexpr bool operator()(const T& x) { return x; }
struct log : UnaryOpMixin<log> {
template <DType T>
static auto operator()(const T& x) {
assert(domain<T>.contains(x) and "x must be non-negative");
return std::log(x);
}
using UnaryOpMixin::operator();

template <DType T>
static constexpr interval<T> domain = interval<T>::nonnegative();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

struct logical : UnaryOpMixin<logical> {
static bool operator()(const DType auto& x) { return x; }

static interval<bool> operator()(const interval<bool>& domain) { return domain; }
template <DType T>
static interval<bool> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

if (domain.infimum == 0 and domain.supremum == 0) return interval(false, false);
if (domain.infimum <= 0 and domain.supremum >= 0) return interval(false, true);
return interval(true, true);
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

struct logical_not : UnaryOpMixin<logical_not> {
static bool operator()(const DType auto& x) { return not x; }

static interval<bool> operator()(const interval<bool>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain
return interval(not domain.supremum, not domain.infimum);
}
template <DType T>
static interval<bool> operator()(const interval<T>& domain) {
// Call the more specific interval<bool> overload
return operator()(logical{}(domain));
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct logical_xor {
static constexpr bool operator()(const T& x, const T& y) {
static bool operator()(const T& x, const T& y) {
return static_cast<bool>(x) != static_cast<bool>(y);
}
};
Expand DownExpand Up@@ -90,9 +230,28 @@ struct modulus {
}
};

template <class T>
struct rint {
static constexpr auto operator()(const T& x) { return std::rint(x); }
struct negative : UnaryOpMixin<negative> {
template <class T>
requires(DType<T> and not std::same_as<T, bool>) // not defined for bool
static auto operator()(const T& x) {
// We define -INT_MIN to equal INT_MAX under the reasoning that it's more
// important to us to preserve the sign than to preseve the correct value.
if constexpr (std::integral<T>) {
if (x == std::numeric_limits<T>::lowest()) return std::numeric_limits<T>::max();
}

return static_cast<T>(-x); // so it doesn't widen e.g., int8_t->int
}
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Decreasing;
};

struct rint : UnaryOpMixin<rint> {
static auto operator()(const DType auto& x) { return std::rint(x); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
Expand All@@ -103,24 +262,73 @@ struct safe_divides {
}
};

template <class T>
struct sin {
static auto operator()(const T& num) { return std::sin(num); }
struct sin : UnaryOpMixin<sin> {
static auto operator()(const DType auto& x) { return std::sin(x); }

template <DType T>
static interval<decltype(std::sin(T()))> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

// It is possible to be a lot more specific than this by checking whether
// our domain spans a full period or not, but I think this is of dubious
// benefit to the user so for now we just return [-1, +1]
return {-1, +1};
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct square {
static constexpr T operator()(const T& x) { return x * x; }
struct square : UnaryOpMixin<square> {
template <DType T>
static T operator()(const T& x) {
return x * x;
}
static bool operator()(const bool& x) { return x; }

template <DType T>
static interval<T> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

assert(domain.infimum <= domain.supremum); // implied by non-empty

square op{};
T inf_squared = op(domain.infimum);
T sup_squared = op(domain.supremum);

// Non-negative domain: square is increasing
if (0 <= domain.infimum) return interval<T>(inf_squared, sup_squared);

// Non-positive domain: square is decreasing
if (domain.supremum <= 0) return interval<T>(sup_squared, inf_squared);

// Otherwise the domain straddles 0: minimum is 0, maximum is the larger squared endpoint.

return interval<T>(0, inf_squared < sup_squared ? sup_squared : inf_squared);
}
static interval<bool> operator()(const interval<bool>& domain) { return domain; }

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct square_root {
static constexpr auto operator()(const T& x) { return std::sqrt(x); }
struct square_root : UnaryOpMixin<square_root> {
template <DType T>
static auto operator()(const T& x) {
assert(domain<T>.contains(x) and "x must be non-negative");
return std::sqrt(x);
}
using UnaryOpMixin::operator();

template <DType T>
static constexpr interval<T> domain = interval<T>::nonnegative();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct tanh {
static auto operator()(const T& num) { return std::tanh(num); }
struct tanh : UnaryOpMixin<tanh> {
static auto operator()(const DType auto& num) { return std::tanh(num); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

} // namespace dwave::optimization::functional
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Rework `UnaryOpNode` by arcondello · Pull Request #627 · dwavesystems/dwave-optimization · GitHub
Skip to content
Open
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
276 changes: 242 additions & 34 deletions dwave/optimization/include/dwave-optimization/functional.hpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,45 +15,185 @@
#pragma once

#include <algorithm>
#include <cassert>
#include <cmath>
#include <concepts>
#include <cstdlib>
#include <limits>
#include <utility>

#include "dwave-optimization/interval.hpp"
#include "dwave-optimization/typing.hpp"

namespace dwave::optimization::functional {

template <class T>
struct abs {
static constexpr T operator()(const T& x) { return std::abs(x); }
enum class Monotonicity { Decreasing = -1, None = 0, Increasing = 1 };

template <typename UnaryOp>
struct UnaryOpMixin {
template <DType T>
requires(UnaryOp::monotonic != Monotonicity::None)
static auto operator()(const interval<T>& domain) {
using return_type = interval<decltype(UnaryOp::operator()(T()))>;

// op(empty domain) -> empty domain
if (not static_cast<bool>(domain)) return return_type();

assert(
domain <= UnaryOp::template domain<T> and
"input domain must be a subset of the func's domain"
);

// We don't worry about outward rounding here because this overload is meant
// to reflect the behavior of the scalar overload, not necessarily to be
// mathematically correct.
// We *do* assume that UnaryOp (e.g., std::exp()) is monotonic, which
// is not always true, but I think it's an OK assumption for our purposes.
if constexpr (UnaryOp::monotonic == Monotonicity::Increasing) {
return return_type(
UnaryOp::operator()(domain.infimum), UnaryOp::operator()(domain.supremum)
);
} else if constexpr (UnaryOp::monotonic == Monotonicity::Decreasing) {
return return_type(
UnaryOp::operator()(domain.supremum), UnaryOp::operator()(domain.infimum)
);
} else {
assert(false and "unexpected monotonicity");
std::unreachable();
}
}

template <DType T>
static constexpr interval<T> domain = interval<T>::all();
};

template <class T>
struct cos {
static auto operator()(const T& num) { return std::cos(num); }
struct absolute : UnaryOpMixin<absolute> {
template <DType T>
static T operator()(const T& x) {
// Unlike NumPy/std, we define std::abs(INT_MIN) to equal INT_MAX under the reasoning
// that it's more important to us to preserve the sign than to preseve the correct value.
if constexpr (std::integral<T>) {
if (x == std::numeric_limits<T>::lowest()) return std::numeric_limits<T>::max();
}

// std::abs() is not defined for int8 or int16 so we static_cast to avoid widening.
return static_cast<T>(std::abs(x));
}
static bool operator()(const bool& x) { return x; }

template <DType T>
static interval<T> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

assert(domain.infimum <= domain.supremum); // implied by non-empty

// If the domain is non-negative, then absolute is identity
if (0 <= domain.infimum) return domain;

// If the domain is negative, then absolute is just the inverse
if (domain.supremum < 0) return -domain;

// Otherwise, the domain straddles 0

// Handle the -INT_MIN case. Again we treat abs(-INT_MIN) as INT_MAX under the reasoning
// that [INT_MIN, ...] is probably intended to mean unbounded.
if constexpr (std::integral<T>) {
if (domain.infimum == std::numeric_limits<T>::lowest()) {
return interval<T>(0, std::numeric_limits<T>::max());
}
}

return interval<T>(
0, -domain.infimum < domain.supremum ? domain.supremum : -domain.infimum
);
}
static interval<bool> operator()(const interval<bool>& domain) { return domain; }

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct exp {
static constexpr auto operator()(const T& x) { return std::exp(x); }
struct cos : UnaryOpMixin<cos> {
static auto operator()(const DType auto& x) { return std::cos(x); }

template <DType T>
static interval<decltype(std::cos(T()))> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

// It is possible to be a lot more specific than this by checking whether
// our domain spans a full period or not, but I think this is of dubious
// benefit to the user so for now we just return [-1, +1]
return {-1, +1};
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct expit {
static constexpr double operator()(const T& x) { return 1.0 / (1.0 + std::exp(-1. * x)); }
struct exp : UnaryOpMixin<exp> {
static auto operator()(const DType auto& x) { return std::exp(x); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct log {
static constexpr auto operator()(const T& x) { return std::log(x); }
struct expit : UnaryOpMixin<expit> {
template <DType T>
static auto operator()(const T& x) {
return 1 / (1 + std::exp(-x));
}
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct logical {
static constexpr bool operator()(const T& x) { return x; }
struct log : UnaryOpMixin<log> {
template <DType T>
static auto operator()(const T& x) {
assert(domain<T>.contains(x) and "x must be non-negative");
return std::log(x);
}
using UnaryOpMixin::operator();

template <DType T>
static constexpr interval<T> domain = interval<T>::nonnegative();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

struct logical : UnaryOpMixin<logical> {
static bool operator()(const DType auto& x) { return x; }

static interval<bool> operator()(const interval<bool>& domain) { return domain; }
template <DType T>
static interval<bool> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

if (domain.infimum == 0 and domain.supremum == 0) return interval(false, false);
if (domain.infimum <= 0 and domain.supremum >= 0) return interval(false, true);
return interval(true, true);
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

struct logical_not : UnaryOpMixin<logical_not> {
static bool operator()(const DType auto& x) { return not x; }

static interval<bool> operator()(const interval<bool>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain
return interval(not domain.supremum, not domain.infimum);
}
template <DType T>
static interval<bool> operator()(const interval<T>& domain) {
// Call the more specific interval<bool> overload
return operator()(logical{}(domain));
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct logical_xor {
static constexpr bool operator()(const T& x, const T& y) {
static bool operator()(const T& x, const T& y) {
return static_cast<bool>(x) != static_cast<bool>(y);
}
};
Expand DownExpand Up@@ -90,9 +230,28 @@ struct modulus {
}
};

template <class T>
struct rint {
static constexpr auto operator()(const T& x) { return std::rint(x); }
struct negative : UnaryOpMixin<negative> {
template <class T>
requires(DType<T> and not std::same_as<T, bool>) // not defined for bool
static auto operator()(const T& x) {
// We define -INT_MIN to equal INT_MAX under the reasoning that it's more
// important to us to preserve the sign than to preseve the correct value.
if constexpr (std::integral<T>) {
if (x == std::numeric_limits<T>::lowest()) return std::numeric_limits<T>::max();
}

return static_cast<T>(-x); // so it doesn't widen e.g., int8_t->int
}
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Decreasing;
};

struct rint : UnaryOpMixin<rint> {
static auto operator()(const DType auto& x) { return std::rint(x); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
Expand All@@ -103,24 +262,73 @@ struct safe_divides {
}
};

template <class T>
struct sin {
static auto operator()(const T& num) { return std::sin(num); }
struct sin : UnaryOpMixin<sin> {
static auto operator()(const DType auto& x) { return std::sin(x); }

template <DType T>
static interval<decltype(std::sin(T()))> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

// It is possible to be a lot more specific than this by checking whether
// our domain spans a full period or not, but I think this is of dubious
// benefit to the user so for now we just return [-1, +1]
return {-1, +1};
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct square {
static constexpr T operator()(const T& x) { return x * x; }
struct square : UnaryOpMixin<square> {
template <DType T>
static T operator()(const T& x) {
return x * x;
}
static bool operator()(const bool& x) { return x; }

template <DType T>
static interval<T> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

assert(domain.infimum <= domain.supremum); // implied by non-empty

square op{};
T inf_squared = op(domain.infimum);
T sup_squared = op(domain.supremum);

// Non-negative domain: square is increasing
if (0 <= domain.infimum) return interval<T>(inf_squared, sup_squared);

// Non-positive domain: square is decreasing
if (domain.supremum <= 0) return interval<T>(sup_squared, inf_squared);

// Otherwise the domain straddles 0: minimum is 0, maximum is the larger squared endpoint.

return interval<T>(0, inf_squared < sup_squared ? sup_squared : inf_squared);
}
static interval<bool> operator()(const interval<bool>& domain) { return domain; }

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct square_root {
static constexpr auto operator()(const T& x) { return std::sqrt(x); }
struct square_root : UnaryOpMixin<square_root> {
template <DType T>
static auto operator()(const T& x) {
assert(domain<T>.contains(x) and "x must be non-negative");
return std::sqrt(x);
}
using UnaryOpMixin::operator();

template <DType T>
static constexpr interval<T> domain = interval<T>::nonnegative();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct tanh {
static auto operator()(const T& num) { return std::tanh(num); }
struct tanh : UnaryOpMixin<tanh> {
static auto operator()(const DType auto& num) { return std::tanh(num); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

} // namespace dwave::optimization::functional
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Rework `UnaryOpNode` by arcondello · Pull Request #627 · dwavesystems/dwave-optimization · GitHub
Skip to content
Open
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
276 changes: 242 additions & 34 deletions dwave/optimization/include/dwave-optimization/functional.hpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,45 +15,185 @@
#pragma once

#include <algorithm>
#include <cassert>
#include <cmath>
#include <concepts>
#include <cstdlib>
#include <limits>
#include <utility>

#include "dwave-optimization/interval.hpp"
#include "dwave-optimization/typing.hpp"

namespace dwave::optimization::functional {

template <class T>
struct abs {
static constexpr T operator()(const T& x) { return std::abs(x); }
enum class Monotonicity { Decreasing = -1, None = 0, Increasing = 1 };

template <typename UnaryOp>
struct UnaryOpMixin {
template <DType T>
requires(UnaryOp::monotonic != Monotonicity::None)
static auto operator()(const interval<T>& domain) {
using return_type = interval<decltype(UnaryOp::operator()(T()))>;

// op(empty domain) -> empty domain
if (not static_cast<bool>(domain)) return return_type();

assert(
domain <= UnaryOp::template domain<T> and
"input domain must be a subset of the func's domain"
);

// We don't worry about outward rounding here because this overload is meant
// to reflect the behavior of the scalar overload, not necessarily to be
// mathematically correct.
// We *do* assume that UnaryOp (e.g., std::exp()) is monotonic, which
// is not always true, but I think it's an OK assumption for our purposes.
if constexpr (UnaryOp::monotonic == Monotonicity::Increasing) {
return return_type(
UnaryOp::operator()(domain.infimum), UnaryOp::operator()(domain.supremum)
);
} else if constexpr (UnaryOp::monotonic == Monotonicity::Decreasing) {
return return_type(
UnaryOp::operator()(domain.supremum), UnaryOp::operator()(domain.infimum)
);
} else {
assert(false and "unexpected monotonicity");
std::unreachable();
}
}

template <DType T>
static constexpr interval<T> domain = interval<T>::all();
};

template <class T>
struct cos {
static auto operator()(const T& num) { return std::cos(num); }
struct absolute : UnaryOpMixin<absolute> {
template <DType T>
static T operator()(const T& x) {
// Unlike NumPy/std, we define std::abs(INT_MIN) to equal INT_MAX under the reasoning
// that it's more important to us to preserve the sign than to preseve the correct value.
if constexpr (std::integral<T>) {
if (x == std::numeric_limits<T>::lowest()) return std::numeric_limits<T>::max();
}

// std::abs() is not defined for int8 or int16 so we static_cast to avoid widening.
return static_cast<T>(std::abs(x));
}
static bool operator()(const bool& x) { return x; }

template <DType T>
static interval<T> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

assert(domain.infimum <= domain.supremum); // implied by non-empty

// If the domain is non-negative, then absolute is identity
if (0 <= domain.infimum) return domain;

// If the domain is negative, then absolute is just the inverse
if (domain.supremum < 0) return -domain;

// Otherwise, the domain straddles 0

// Handle the -INT_MIN case. Again we treat abs(-INT_MIN) as INT_MAX under the reasoning
// that [INT_MIN, ...] is probably intended to mean unbounded.
if constexpr (std::integral<T>) {
if (domain.infimum == std::numeric_limits<T>::lowest()) {
return interval<T>(0, std::numeric_limits<T>::max());
}
}

return interval<T>(
0, -domain.infimum < domain.supremum ? domain.supremum : -domain.infimum
);
}
static interval<bool> operator()(const interval<bool>& domain) { return domain; }

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct exp {
static constexpr auto operator()(const T& x) { return std::exp(x); }
struct cos : UnaryOpMixin<cos> {
static auto operator()(const DType auto& x) { return std::cos(x); }

template <DType T>
static interval<decltype(std::cos(T()))> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

// It is possible to be a lot more specific than this by checking whether
// our domain spans a full period or not, but I think this is of dubious
// benefit to the user so for now we just return [-1, +1]
return {-1, +1};
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct expit {
static constexpr double operator()(const T& x) { return 1.0 / (1.0 + std::exp(-1. * x)); }
struct exp : UnaryOpMixin<exp> {
static auto operator()(const DType auto& x) { return std::exp(x); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct log {
static constexpr auto operator()(const T& x) { return std::log(x); }
struct expit : UnaryOpMixin<expit> {
template <DType T>
static auto operator()(const T& x) {
return 1 / (1 + std::exp(-x));
}
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct logical {
static constexpr bool operator()(const T& x) { return x; }
struct log : UnaryOpMixin<log> {
template <DType T>
static auto operator()(const T& x) {
assert(domain<T>.contains(x) and "x must be non-negative");
return std::log(x);
}
using UnaryOpMixin::operator();

template <DType T>
static constexpr interval<T> domain = interval<T>::nonnegative();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

struct logical : UnaryOpMixin<logical> {
static bool operator()(const DType auto& x) { return x; }

static interval<bool> operator()(const interval<bool>& domain) { return domain; }
template <DType T>
static interval<bool> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

if (domain.infimum == 0 and domain.supremum == 0) return interval(false, false);
if (domain.infimum <= 0 and domain.supremum >= 0) return interval(false, true);
return interval(true, true);
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

struct logical_not : UnaryOpMixin<logical_not> {
static bool operator()(const DType auto& x) { return not x; }

static interval<bool> operator()(const interval<bool>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain
return interval(not domain.supremum, not domain.infimum);
}
template <DType T>
static interval<bool> operator()(const interval<T>& domain) {
// Call the more specific interval<bool> overload
return operator()(logical{}(domain));
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct logical_xor {
static constexpr bool operator()(const T& x, const T& y) {
static bool operator()(const T& x, const T& y) {
return static_cast<bool>(x) != static_cast<bool>(y);
}
};
Expand DownExpand Up@@ -90,9 +230,28 @@ struct modulus {
}
};

template <class T>
struct rint {
static constexpr auto operator()(const T& x) { return std::rint(x); }
struct negative : UnaryOpMixin<negative> {
template <class T>
requires(DType<T> and not std::same_as<T, bool>) // not defined for bool
static auto operator()(const T& x) {
// We define -INT_MIN to equal INT_MAX under the reasoning that it's more
// important to us to preserve the sign than to preseve the correct value.
if constexpr (std::integral<T>) {
if (x == std::numeric_limits<T>::lowest()) return std::numeric_limits<T>::max();
}

return static_cast<T>(-x); // so it doesn't widen e.g., int8_t->int
}
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Decreasing;
};

struct rint : UnaryOpMixin<rint> {
static auto operator()(const DType auto& x) { return std::rint(x); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
Expand All@@ -103,24 +262,73 @@ struct safe_divides {
}
};

template <class T>
struct sin {
static auto operator()(const T& num) { return std::sin(num); }
struct sin : UnaryOpMixin<sin> {
static auto operator()(const DType auto& x) { return std::sin(x); }

template <DType T>
static interval<decltype(std::sin(T()))> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

// It is possible to be a lot more specific than this by checking whether
// our domain spans a full period or not, but I think this is of dubious
// benefit to the user so for now we just return [-1, +1]
return {-1, +1};
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct square {
static constexpr T operator()(const T& x) { return x * x; }
struct square : UnaryOpMixin<square> {
template <DType T>
static T operator()(const T& x) {
return x * x;
}
static bool operator()(const bool& x) { return x; }

template <DType T>
static interval<T> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

assert(domain.infimum <= domain.supremum); // implied by non-empty

square op{};
T inf_squared = op(domain.infimum);
T sup_squared = op(domain.supremum);

// Non-negative domain: square is increasing
if (0 <= domain.infimum) return interval<T>(inf_squared, sup_squared);

// Non-positive domain: square is decreasing
if (domain.supremum <= 0) return interval<T>(sup_squared, inf_squared);

// Otherwise the domain straddles 0: minimum is 0, maximum is the larger squared endpoint.

return interval<T>(0, inf_squared < sup_squared ? sup_squared : inf_squared);
}
static interval<bool> operator()(const interval<bool>& domain) { return domain; }

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct square_root {
static constexpr auto operator()(const T& x) { return std::sqrt(x); }
struct square_root : UnaryOpMixin<square_root> {
template <DType T>
static auto operator()(const T& x) {
assert(domain<T>.contains(x) and "x must be non-negative");
return std::sqrt(x);
}
using UnaryOpMixin::operator();

template <DType T>
static constexpr interval<T> domain = interval<T>::nonnegative();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct tanh {
static auto operator()(const T& num) { return std::tanh(num); }
struct tanh : UnaryOpMixin<tanh> {
static auto operator()(const DType auto& num) { return std::tanh(num); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

} // namespace dwave::optimization::functional
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Rework `UnaryOpNode` by arcondello · Pull Request #627 · dwavesystems/dwave-optimization · GitHub
Skip to content
Open
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
276 changes: 242 additions & 34 deletions dwave/optimization/include/dwave-optimization/functional.hpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,45 +15,185 @@
#pragma once

#include <algorithm>
#include <cassert>
#include <cmath>
#include <concepts>
#include <cstdlib>
#include <limits>
#include <utility>

#include "dwave-optimization/interval.hpp"
#include "dwave-optimization/typing.hpp"

namespace dwave::optimization::functional {

template <class T>
struct abs {
static constexpr T operator()(const T& x) { return std::abs(x); }
enum class Monotonicity { Decreasing = -1, None = 0, Increasing = 1 };

template <typename UnaryOp>
struct UnaryOpMixin {
template <DType T>
requires(UnaryOp::monotonic != Monotonicity::None)
static auto operator()(const interval<T>& domain) {
using return_type = interval<decltype(UnaryOp::operator()(T()))>;

// op(empty domain) -> empty domain
if (not static_cast<bool>(domain)) return return_type();

assert(
domain <= UnaryOp::template domain<T> and
"input domain must be a subset of the func's domain"
);

// We don't worry about outward rounding here because this overload is meant
// to reflect the behavior of the scalar overload, not necessarily to be
// mathematically correct.
// We *do* assume that UnaryOp (e.g., std::exp()) is monotonic, which
// is not always true, but I think it's an OK assumption for our purposes.
if constexpr (UnaryOp::monotonic == Monotonicity::Increasing) {
return return_type(
UnaryOp::operator()(domain.infimum), UnaryOp::operator()(domain.supremum)
);
} else if constexpr (UnaryOp::monotonic == Monotonicity::Decreasing) {
return return_type(
UnaryOp::operator()(domain.supremum), UnaryOp::operator()(domain.infimum)
);
} else {
assert(false and "unexpected monotonicity");
std::unreachable();
}
}

template <DType T>
static constexpr interval<T> domain = interval<T>::all();
};

template <class T>
struct cos {
static auto operator()(const T& num) { return std::cos(num); }
struct absolute : UnaryOpMixin<absolute> {
template <DType T>
static T operator()(const T& x) {
// Unlike NumPy/std, we define std::abs(INT_MIN) to equal INT_MAX under the reasoning
// that it's more important to us to preserve the sign than to preseve the correct value.
if constexpr (std::integral<T>) {
if (x == std::numeric_limits<T>::lowest()) return std::numeric_limits<T>::max();
}

// std::abs() is not defined for int8 or int16 so we static_cast to avoid widening.
return static_cast<T>(std::abs(x));
}
static bool operator()(const bool& x) { return x; }

template <DType T>
static interval<T> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

assert(domain.infimum <= domain.supremum); // implied by non-empty

// If the domain is non-negative, then absolute is identity
if (0 <= domain.infimum) return domain;

// If the domain is negative, then absolute is just the inverse
if (domain.supremum < 0) return -domain;

// Otherwise, the domain straddles 0

// Handle the -INT_MIN case. Again we treat abs(-INT_MIN) as INT_MAX under the reasoning
// that [INT_MIN, ...] is probably intended to mean unbounded.
if constexpr (std::integral<T>) {
if (domain.infimum == std::numeric_limits<T>::lowest()) {
return interval<T>(0, std::numeric_limits<T>::max());
}
}

return interval<T>(
0, -domain.infimum < domain.supremum ? domain.supremum : -domain.infimum
);
}
static interval<bool> operator()(const interval<bool>& domain) { return domain; }

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct exp {
static constexpr auto operator()(const T& x) { return std::exp(x); }
struct cos : UnaryOpMixin<cos> {
static auto operator()(const DType auto& x) { return std::cos(x); }

template <DType T>
static interval<decltype(std::cos(T()))> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

// It is possible to be a lot more specific than this by checking whether
// our domain spans a full period or not, but I think this is of dubious
// benefit to the user so for now we just return [-1, +1]
return {-1, +1};
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct expit {
static constexpr double operator()(const T& x) { return 1.0 / (1.0 + std::exp(-1. * x)); }
struct exp : UnaryOpMixin<exp> {
static auto operator()(const DType auto& x) { return std::exp(x); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct log {
static constexpr auto operator()(const T& x) { return std::log(x); }
struct expit : UnaryOpMixin<expit> {
template <DType T>
static auto operator()(const T& x) {
return 1 / (1 + std::exp(-x));
}
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct logical {
static constexpr bool operator()(const T& x) { return x; }
struct log : UnaryOpMixin<log> {
template <DType T>
static auto operator()(const T& x) {
assert(domain<T>.contains(x) and "x must be non-negative");
return std::log(x);
}
using UnaryOpMixin::operator();

template <DType T>
static constexpr interval<T> domain = interval<T>::nonnegative();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

struct logical : UnaryOpMixin<logical> {
static bool operator()(const DType auto& x) { return x; }

static interval<bool> operator()(const interval<bool>& domain) { return domain; }
template <DType T>
static interval<bool> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

if (domain.infimum == 0 and domain.supremum == 0) return interval(false, false);
if (domain.infimum <= 0 and domain.supremum >= 0) return interval(false, true);
return interval(true, true);
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

struct logical_not : UnaryOpMixin<logical_not> {
static bool operator()(const DType auto& x) { return not x; }

static interval<bool> operator()(const interval<bool>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain
return interval(not domain.supremum, not domain.infimum);
}
template <DType T>
static interval<bool> operator()(const interval<T>& domain) {
// Call the more specific interval<bool> overload
return operator()(logical{}(domain));
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct logical_xor {
static constexpr bool operator()(const T& x, const T& y) {
static bool operator()(const T& x, const T& y) {
return static_cast<bool>(x) != static_cast<bool>(y);
}
};
Expand DownExpand Up@@ -90,9 +230,28 @@ struct modulus {
}
};

template <class T>
struct rint {
static constexpr auto operator()(const T& x) { return std::rint(x); }
struct negative : UnaryOpMixin<negative> {
template <class T>
requires(DType<T> and not std::same_as<T, bool>) // not defined for bool
static auto operator()(const T& x) {
// We define -INT_MIN to equal INT_MAX under the reasoning that it's more
// important to us to preserve the sign than to preseve the correct value.
if constexpr (std::integral<T>) {
if (x == std::numeric_limits<T>::lowest()) return std::numeric_limits<T>::max();
}

return static_cast<T>(-x); // so it doesn't widen e.g., int8_t->int
}
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Decreasing;
};

struct rint : UnaryOpMixin<rint> {
static auto operator()(const DType auto& x) { return std::rint(x); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
Expand All@@ -103,24 +262,73 @@ struct safe_divides {
}
};

template <class T>
struct sin {
static auto operator()(const T& num) { return std::sin(num); }
struct sin : UnaryOpMixin<sin> {
static auto operator()(const DType auto& x) { return std::sin(x); }

template <DType T>
static interval<decltype(std::sin(T()))> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

// It is possible to be a lot more specific than this by checking whether
// our domain spans a full period or not, but I think this is of dubious
// benefit to the user so for now we just return [-1, +1]
return {-1, +1};
}

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct square {
static constexpr T operator()(const T& x) { return x * x; }
struct square : UnaryOpMixin<square> {
template <DType T>
static T operator()(const T& x) {
return x * x;
}
static bool operator()(const bool& x) { return x; }

template <DType T>
static interval<T> operator()(const interval<T>& domain) {
if (not static_cast<bool>(domain)) return {}; // op(empty domain) -> empty domain

assert(domain.infimum <= domain.supremum); // implied by non-empty

square op{};
T inf_squared = op(domain.infimum);
T sup_squared = op(domain.supremum);

// Non-negative domain: square is increasing
if (0 <= domain.infimum) return interval<T>(inf_squared, sup_squared);

// Non-positive domain: square is decreasing
if (domain.supremum <= 0) return interval<T>(sup_squared, inf_squared);

// Otherwise the domain straddles 0: minimum is 0, maximum is the larger squared endpoint.

return interval<T>(0, inf_squared < sup_squared ? sup_squared : inf_squared);
}
static interval<bool> operator()(const interval<bool>& domain) { return domain; }

static constexpr Monotonicity monotonic = Monotonicity::None;
};

template <class T>
struct square_root {
static constexpr auto operator()(const T& x) { return std::sqrt(x); }
struct square_root : UnaryOpMixin<square_root> {
template <DType T>
static auto operator()(const T& x) {
assert(domain<T>.contains(x) and "x must be non-negative");
return std::sqrt(x);
}
using UnaryOpMixin::operator();

template <DType T>
static constexpr interval<T> domain = interval<T>::nonnegative();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

template <class T>
struct tanh {
static auto operator()(const T& num) { return std::tanh(num); }
struct tanh : UnaryOpMixin<tanh> {
static auto operator()(const DType auto& num) { return std::tanh(num); }
using UnaryOpMixin::operator();

static constexpr Monotonicity monotonic = Monotonicity::Increasing;
};

} // namespace dwave::optimization::functional
Loading