Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: true
- name: Install dependencies
run: ./helper.py -d
- name: Build
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: true

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
Expand Down
8 changes: 8 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[submodule "tests/deps/toml++"]
path = tests/deps/toml++
url = [email protected]:marzer/tomlplusplus
shallow = true
[submodule "tests/test_data"]
path = tests/test_data
url = [email protected]:ALFI-library/test_data
shallow = true
101 changes: 57 additions & 44 deletions ALFI/ALFI/dist.h
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#pragma once

#include "config.h"

#include <cmath>

#include "config.h"
#include "util/points.h"

namespace alfi::dist {
enum class Type {
GENERAL,
Expand All @@ -20,30 +21,6 @@ namespace alfi::dist {
ERF_STRETCHED,
};

template <typename Number = DefaultNumber>
void stretch(auto& points, Number a, Number b) {
if (points.empty()) {
return;
}

if (points.size() == 1 || points.front() == points.back()) {
std::fill(points.begin(), points.end(), (a + b) / 2);
return;
}

const Number& min = points.front();
const Number& max = points.back();
const Number mid = (a + b) / 2;
const Number scale = (b - a) / (max - min);

for (SizeT i = 1; i < points.size() - 1; ++i) {
points[i] = mid + scale * (points[i] - mid);
}

points.front() = a;
points.back() = b;
}

template <typename Number = DefaultNumber, template <typename> class Container = DefaultContainer>
Container<Number> uniform(SizeT n, Number a, Number b) {
if (n == 1)
Expand All @@ -69,9 +46,7 @@ namespace alfi::dist {

template <typename Number = DefaultNumber, template <typename> class Container = DefaultContainer>
Container<Number> chebyshev_stretched(SizeT n, Number a, Number b) {
Container<Number> points = chebyshev(n, a, b);
stretch(points, a, b);
return points;
return points::stretched(chebyshev(n, a, b), a, b);
}

template <typename Number = DefaultNumber, template <typename> class Container = DefaultContainer>
Expand All @@ -90,9 +65,7 @@ namespace alfi::dist {

template <typename Number = DefaultNumber, template <typename> class Container = DefaultContainer>
Container<Number> chebyshev_ellipse_stretched(SizeT n, Number a, Number b, Number ratio) {
Container<Number> points = chebyshev_ellipse(n, a, b, ratio);
stretch(points, a, b);
return points;
return points::stretched(chebyshev_ellipse(n, a, b, ratio), a, b);
}

template <typename Number = DefaultNumber, template <typename> class Container = DefaultContainer>
Expand Down Expand Up @@ -121,14 +94,35 @@ namespace alfi::dist {
return points;
}

/**
@brief Generates a distribution of \p n points on the interval `(a, b)` using the sigmoid function.

The following transform function \f(f\f):
\f[f(x) = \frac{1}{1 + e^{-steepness \cdot x}}\f]
is applied to `n` points uniformly distributed on the interval `(-1, 1)`, mapping them onto the `(0, 1)` interval.\n
The resulting points are then linearly mapped to the target interval `(a, b)`.

The slope of the transform function \f(f\f) at `x = 0` is determined by \p steepness and is given by:
\f[
\left. \dv{f}{x} \right\vert_{x=0} = \frac14 \cdot steepness
\f]

@note Extreme points don't lie on the interval `(a, b)` boundaries.

@param n number of points
@param a left boundary of the interval
@param b right boundary of the interval
@param steepness determines the slope of the transform function at `x = 0`
@return a container with \p n points distributed on the interval `(a, b)` according to the transform function
*/
template <typename Number = DefaultNumber, template <typename> class Container = DefaultContainer>
Container<Number> sigmoid(SizeT n, Number a, Number b, Number steepness) {
if (n == 1)
return {(a+b)/2};
Container<Number> points(n);
for (SizeT i = 0; i < n; ++i) {
const Number x = static_cast<Number>(i) / (static_cast<Number>(n) - 1);
const Number sigmoidValue = 1.0 / (1.0 + exp(-steepness * (x - 0.5)));
const Number x = 2 * static_cast<Number>(i) / (static_cast<Number>(n) - 1) - 1;
const Number sigmoidValue = 1 / (1 + exp(-steepness * x));
points[i] = a + (b - a) * sigmoidValue;
}
return points;
Expand All @@ -141,18 +135,39 @@ namespace alfi::dist {
if (n == 1)
return {(a+b)/2};
Container<Number> points(n);
const Number stretch_factor = 1 - 2 / (1 + std::exp(0.5 * steepness));
const Number stretch_factor = 1 - 2 / (1 + std::exp(steepness));
for (SizeT i = 1; i < n - 1; ++i) {
const Number x = static_cast<double>(i) / (static_cast<Number>(n) - 1);
const Number sigmoid = 1.0 / (1.0 + std::exp(-steepness * (x - 0.5)));
const Number stretched = (sigmoid - 1.0 / (1.0 + std::exp(0.5 * steepness))) / stretch_factor;
const Number x = 2 * static_cast<double>(i) / (static_cast<Number>(n) - 1) - 1;
const Number sigmoid = 1 / (1 + std::exp(-steepness * x));
const Number stretched = (sigmoid - 1 / (1 + std::exp(steepness))) / stretch_factor;
points[i] = a + (b - a) * stretched;
}
points[0] = a;
points[n-1] = b;
return points;
}

/**
@brief Generates a distribution of \p n points on the interval `(a, b)` using the error function.

The following transform function \f(f\f):
\f[f(x) = \operatorname{erf}(steepness \cdot x)\f]
is applied to `n` points uniformly distributed on the interval `(-1, 1)`, mapping them onto the same interval.\n
The resulting points are then linearly mapped to the target interval `(a, b)`.

The slope of the transform function \f(f\f) at `x = 0` is determined by \p steepness and is given by:
\f[
\left. \dv{f}{x} \right\vert_{x=0} = \frac2\pi \cdot steepness
\f]

@note Extreme points don't lie on the interval `(a, b)` boundaries.

@param n number of points
@param a left boundary of the interval
@param b right boundary of the interval
@param steepness determines the slope of the transform function at `x = 0`
@return a container with \p n points distributed on the interval `(a, b)` according to the transform function
*/
template <typename Number = DefaultNumber, template <typename> class Container = DefaultContainer>
Container<Number> erf(SizeT n, Number a, Number b, Number steepness) {
if (n == 0)
Expand All @@ -161,22 +176,20 @@ namespace alfi::dist {
return {(a+b)/2};
Container<Number> points(n);
for (SizeT i = 0; i < n; ++i) {
const Number x = static_cast<Number>(i) / (static_cast<Number>(n) - 1);
const Number erf_value = std::erf(steepness * (x - 0.5));
const Number x = 2 * static_cast<Number>(i) / (static_cast<Number>(n) - 1) - 1;
const Number erf_value = std::erf(steepness * x);
points[i] = a + (b - a) * (1 + erf_value) / 2;
}
return points;
}

template <typename Number = DefaultNumber, template <typename> class Container = DefaultContainer>
Container<Number> erf_stretched(SizeT n, Number a, Number b, Number steepness) {
Container<Number> points = erf(n, a, b, steepness);
stretch(points, a, b);
return points;
return points::stretched(erf(n, a, b, steepness), a, b);
}

template <typename Number = DefaultNumber, template <typename> class Container = DefaultContainer>
Container<Number> of_type(Type type, SizeT n, Number a, Number b, Number parameter = 0) {
Container<Number> of_type(Type type, SizeT n, Number a, Number b, Number parameter = NAN) {
switch (type) {
case Type::CHEBYSHEV:
return chebyshev(n, a, b);
Expand Down
43 changes: 43 additions & 0 deletions ALFI/ALFI/util/points.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#pragma once

#include "../config.h"

namespace alfi::points {
template <typename Number = DefaultNumber>
void lin_map(auto& points, Number a, Number b, Number c, Number d) {
const auto mid1 = (a + b) / 2;
const auto mid2 = (c + d) / 2;
const auto scale = (d - c) / (b - a);
for (auto& point : points) {
point = mid2 + scale * (point - mid1);
Copy link
Member Author

Choose a reason for hiding this comment

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

It's unclear whether the mapping should be based on the beginning or the middle of the segment, or if some hybrid approach should be used.

  • Which way would be more accurate?
  • How does this affect the fact that the points in the library are symmetrical relative to the midpoints of the segments?

}
}

template <typename Number = DefaultNumber>
auto lin_mapped(const auto& points, Number a, Number b, Number c, Number d) {
auto mapped_points = points;
lin_map(mapped_points, a, b, c, d);
return mapped_points;
}

template <typename Number = DefaultNumber>
void stretch(auto& points, Number a, Number b) {
if (points.empty()) {
return;
}
if (points.size() == 1 || points.front() == points.back()) {
std::fill(points.begin(), points.end(), (a + b) / 2);
return;
}
lin_map(points, points.front(), points.back(), a, b);
points.front() = a;
points.back() = b;
}

template <typename Number = DefaultNumber>
auto stretched(const auto& points, Number a, Number b) {
auto stretched_points = points;
stretch(stretched_points, a, b);
return stretched_points;
}
}
2 changes: 2 additions & 0 deletions helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
dependencies = [
'build-essential',
'cmake',
'git',
'libgnuplot-iostream-dev',
'libgtest-dev',
'libqcustomplot-dev',
Expand Down Expand Up @@ -48,6 +49,7 @@ def execute_command(command):
if args.deps:
execute_command(['sudo', 'apt', 'update'])
execute_command(['sudo', 'apt', 'install', '-y'] + dependencies)
execute_command(['git', 'submodule', 'update', '--init'])
if args.build:
execute_command(['cmake', '-DCMAKE_BUILD_TYPE=' + args.profile, '-B', profile_dir])
execute_command(['cmake', '--build', profile_dir, '-j', str(os.cpu_count())])
Expand Down
6 changes: 5 additions & 1 deletion tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
set(CMAKE_CXX_STANDARD 23)

find_package(GTest REQUIRED)
add_subdirectory(deps/toml++)

set(TEST_DATA_DIR "${CMAKE_CURRENT_LIST_DIR}/test_data")

macro(add_test_executable test_name)
add_executable(${test_name} ${ARGN} test_utils.h)
target_include_directories(${test_name} PRIVATE ${GTEST_INCLUDE_DIRS})
target_link_libraries(${test_name} PRIVATE ALFI GTest::GTest GTest::Main)
target_link_libraries(${test_name} PRIVATE ALFI GTest::GTest GTest::Main tomlplusplus::tomlplusplus)
target_compile_options(${test_name} PRIVATE -fsanitize=address,leak,undefined)
target_link_options(${test_name} PRIVATE -fsanitize=address,leak,undefined)
target_compile_definitions(${test_name} PRIVATE TEST_DATA_DIR="${TEST_DATA_DIR}")
add_test(NAME ${test_name} COMMAND ${test_name} --gtest_color=yes)
endmacro()

Expand Down
1 change: 1 addition & 0 deletions tests/deps/toml++
Submodule toml++ added at c4369a
Loading
Loading