FuncSketch
Loading...
Searching...
No Matches
function_sampler.cpp
Go to the documentation of this file.
1/*
2 * Copyright 2026 MusicScience37 (Kenta Kabashima)
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
21
22#include <cmath>
23#include <cstddef>
24#include <cstdlib>
25
26namespace func_sketch::plotter {
27
29 // NOLINTNEXTLINE(*-pass-by-value): Wrong warning for small objects.
30 const PlotRange& range, const PlotConfig& config)
32
34 range_ = value;
35 return *this;
36}
37
39 config_ = value;
40 return *this;
41}
42
43std::vector<Point> FunctionSampler::operator()(
44 const expressions::Expression& function) const {
45 const std::vector<Point> initial_samples = sample_initial_points(function);
46 return sample_adaptive_points(function, initial_samples);
47}
48
50 const expressions::Expression& function) const {
51 std::vector<Point> samples;
52 const std::size_t num_points = config_.initial_num_sample_points();
53 samples.reserve(num_points);
54 for (std::size_t i = 0; i < num_points; ++i) {
55 const Real x_ratio =
56 static_cast<Real>(i) / static_cast<Real>(num_points - 1);
57 const Real x_value = range_.x_range().first +
58 x_ratio * (range_.x_range().second - range_.x_range().first);
59
60 Real y_value;
61 evaluator_(function, x_value, y_value);
62
63 samples.push_back(Point{.x = x_value, .y = y_value});
64 }
65 return samples;
66}
67
69 const expressions::Expression& function,
70 const std::vector<Point>& initial_samples) const {
71 std::vector<Point> samples;
72 // Heuristic estimate for the number of the final sampled points.
73 // 3 is determined by experiments. Memory reallocation did not occur in
74 // most cases in our experiments.
75 const std::size_t num_reserved_points = initial_samples.size() * 3;
76 samples.reserve(num_reserved_points);
77
78 auto initial_sample_iter = initial_samples.begin();
79 std::size_t left_index = 0;
80 while (true) {
81 // Insert points to provide three points if possible.
82 while (initial_sample_iter != initial_samples.end() &&
83 samples.size() < left_index + 3) {
84 samples.push_back(*initial_sample_iter);
85 ++initial_sample_iter;
86 }
87 const std::size_t num_points_from_here = samples.size() - left_index;
88 if (num_points_from_here < 2) {
89 // Finished sampling all segments.
90 break;
91 }
92 // At least two points are available.
93
94 bool is_divided = false;
96 function, samples, left_index)) {
97 is_divided = true;
98 }
99 if (num_points_from_here >= 3) {
101 function, samples, left_index)) {
102 is_divided = true;
103 }
104 }
106 function, samples, left_index)) {
107 is_divided = true;
108 }
109 if (!is_divided) {
110 ++left_index;
111 }
112
113 if (samples.size() >= config_.max_num_sample_points()) {
114 // Reached the maximum number of sample points. Insert remaining
115 // initial sample points and finish sampling.
116 samples.insert(
117 samples.end(), initial_sample_iter, initial_samples.end());
118 break;
119 }
120 }
121 return samples;
122}
123
125 const expressions::Expression& function, std::vector<Point>& samples,
126 std::size_t left_index) const {
127 const auto& left_point = samples[left_index];
128 const auto& right_point = samples[left_index + 1];
129 const double y_diff = std::abs(right_point.y - left_point.y);
130 const double y_range = range_.y_range().second - range_.y_range().first;
131 const double y_diff_rate = y_diff / y_range;
132 if (std::isfinite(y_diff_rate) &&
133 y_diff_rate > config_.max_coordinate_change_rate()) {
134 return divide_segment(function, samples, left_index);
135 }
136 return false;
137}
138
140 const expressions::Expression& function, std::vector<Point>& samples,
141 std::size_t left_index) const {
142 const auto& left_point = samples[left_index];
143 const auto& mid_point = samples[left_index + 1];
144 const auto& right_point = samples[left_index + 2];
145 const double slope_normalization_coeff =
146 (range_.x_range().second - range_.x_range().first) /
147 (range_.y_range().second - range_.y_range().first);
148 const double left_slope = slope_normalization_coeff *
149 (mid_point.y - left_point.y) / (mid_point.x - left_point.x);
150 const double right_slope = slope_normalization_coeff *
151 (right_point.y - mid_point.y) / (right_point.x - mid_point.x);
152 const double slope_diff = std::abs(right_slope - left_slope);
153 if (std::isfinite(slope_diff) &&
154 slope_diff > config_.slope_change_threshold()) {
155 // Divide the right segment first to avoid the change of the index of
156 // the right segment before dividing the right segment.
157 const bool is_right_divided =
158 divide_segment(function, samples, left_index + 1);
159 const bool is_left_divided =
160 divide_segment(function, samples, left_index);
161 return is_left_divided || is_right_divided;
162 }
163 return false;
164}
165
167 const expressions::Expression& function, std::vector<Point>& samples,
168 std::size_t left_index) const {
169 const auto& left_point = samples[left_index];
170 const auto& right_point = samples[left_index + 1];
171 const bool is_left_finite = std::isfinite(left_point.y);
172 const bool is_right_finite = std::isfinite(right_point.y);
173 if ((is_left_finite && is_right_finite) ||
174 (!is_left_finite && !is_right_finite)) {
175 return false;
176 }
177 return divide_segment(function, samples, left_index);
178}
179
181 std::vector<Point>& samples, std::size_t left_index) const {
182 const std::size_t right_index = left_index + 1;
183 const Point& left_point = samples[left_index];
184 const Point& right_point = samples[right_index];
185 if (is_sampling_should_be_skipped(left_point, right_point)) {
186 return false;
187 }
188 const Point mid_point =
189 sample_additional_point(function, left_point, right_point);
190 samples.insert(
191 samples.begin() + static_cast<std::ptrdiff_t>(right_index), mid_point);
192 return true;
193}
194
196 const expressions::Expression& function, const Point& left_point,
197 const Point& right_point) const {
198 Point mid_point{};
199 mid_point.x = 0.5 * (left_point.x + right_point.x);
200 evaluator_(function, mid_point.x, mid_point.y);
201 return mid_point;
202}
203
205 const Point& left_point, const Point& right_point) const {
206 if (!range_.contains(left_point) && !range_.contains(right_point)) {
207 // Both points are outside the range, so this segment may outside the
208 // range or has a singularity.
209 return true;
210 }
211
212 const double x_diff = std::abs(right_point.x - left_point.x);
213 const double x_range = range_.x_range().second - range_.x_range().first;
214 const double x_diff_rate = x_diff / x_range;
215 if (x_diff_rate < config_.min_param_change_rate()) {
216 // The change in the parameter is already small enough, this segment may
217 // contain a discontinuity or a singularity.
218 return true; // NOLINT(readability-simplify-boolean-expr): for readability.
219 }
220
221 return false;
222}
223
224} // namespace func_sketch::plotter
bool divide_segment_according_to_y_change(const expressions::Expression &function, std::vector< Point > &samples, std::size_t left_index) const
Divide if the change of y is large.
bool divide_segment(const expressions::Expression &function, std::vector< Point > &samples, std::size_t left_index) const
Divide a segment into two segments in sample points if applicable.
std::vector< Point > sample_adaptive_points(const expressions::Expression &function, const std::vector< Point > &initial_samples) const
Sample additional points adaptively.
FunctionSampler & config(const PlotConfig &value)
Set the configuration of plots.
bool divide_segment_when_one_end_is_not_finite(const expressions::Expression &function, std::vector< Point > &samples, std::size_t left_index) const
Divide if one end of the segment is not finite.
std::vector< Point > operator()(const expressions::Expression &function) const
Sample a function for plotting.
FunctionSampler(const PlotRange &range, const PlotConfig &config)
Constructor.
bool divide_segment_according_to_slope_change(const expressions::Expression &function, std::vector< Point > &samples, std::size_t left_index) const
Divide if the change of slope of y is large.
expressions::ExpressionEvaluator evaluator_
Evaluator of expressions.
std::vector< Point > sample_initial_points(const expressions::Expression &function) const
Sample initial points.
PlotConfig config_
Configuration of plots.
bool is_sampling_should_be_skipped(const Point &left_point, const Point &right_point) const
Check the condition to skip sampling of a point between two points.
Point sample_additional_point(const expressions::Expression &function, const Point &left_point, const Point &right_point) const
Sample an additional point between two points.
FunctionSampler & range(const PlotRange &value)
Set the range of plots.
Class of configurations of a plot.
Definition plot_config.h:97
Class of a range of a plot.
Definition plot_range.h:35
double Real
Type of real numbers in this project.
Definition of FunctionSampler class.
Struct of a point.
Definition point.h:29
double y
Y coordinate.
Definition point.h:34
double x
X coordinate.
Definition point.h:31