DP3
PhaseLineFitter.h
Go to the documentation of this file.
1 // Copyright (C) 2025 ASTRON (Netherlands Institute for Radio Astronomy)
2 // SPDX-License-Identifier: GPL-3.0-or-later
3 
4 #ifndef DP3_COMMON_PHASE_LINE_FITTER_H_
5 #define DP3_COMMON_PHASE_LINE_FITTER_H_
6 
7 #include <cmath>
8 #include <limits>
9 #include <vector>
10 
11 #include <aocommon/optionalnumber.h>
12 
14 
15 struct FitSample {
20  double x;
24  double y;
25  double weight;
26 };
27 
28 struct SlopeFitRange {
29  int max_wraps;
30  double wrap_step;
31 };
32 
33 inline double SlopeModelCost(const std::vector<FitSample>& data, double slope) {
34  double cost = 0.0;
35  double weight_sum = 0.0;
36  for (const FitSample& sample : data) {
37  const double y_model = slope * sample.x;
38  double sample_cost = std::fmod(std::abs(y_model - sample.y), 2.0 * M_PI);
39  if (sample_cost > M_PI) sample_cost = 2.0 * M_PI - sample_cost;
40  sample_cost *= sample.weight;
41  cost += sample_cost;
42  weight_sum += sample.weight;
43  }
44  if (weight_sum == 0.0)
45  return 0.0;
46  else
47  return cost / weight_sum;
48 }
49 
50 inline double UnwrapAndFit(const std::vector<FitSample>& data,
51  double unwrapping_slope) {
52  // Minimize: sum w_i (slope * x_i - y_i)^2
53  // Therefore:
54  // sum w_i 2 x_i (slope * x_i - y_i) = 0
55  // slope = (sum w_i x_i y_i) / (sum w_i x_i^2)
56  double numerator = 0.0;
57  double denominator = 0.0;
58  for (const FitSample& sample : data) {
59  const double y_model = unwrapping_slope * sample.x;
60  double difference = std::fmod(sample.y - y_model, 2.0 * M_PI);
61  if (difference >= M_PI)
62  difference -= 2.0 * M_PI;
63  else if (difference < -M_PI)
64  difference += 2.0 * M_PI;
65  const double unwrapped_y = y_model + difference;
66  const double w_times_x = sample.x * sample.weight;
67  numerator += w_times_x * unwrapped_y;
68  denominator += w_times_x * sample.x;
69  }
70  if (denominator != 0.0)
71  return numerator / denominator;
72  else
73  return 0.0;
74 }
75 
91  const std::vector<FitSample>& data,
92  aocommon::OptionalNumber<int> wrap_count_limit = {}) {
93  if (data.empty()) return SlopeFitRange{0, 0.0};
94 
95  double min_delta = std::numeric_limits<double>::max();
96  for (size_t i = 0; i != data.size() - 1; ++i) {
97  const double delta = data[i + 1].x - data[i].x;
98  assert(delta >= 0.0); // 'data' should be ordered on 'x'.
99  if (delta > 0.0 && delta < min_delta) min_delta = delta;
100  }
101  const double min_x = data.front().x;
102  const double max_x = data.back().x;
103  const double max_abs_x = max_x < 0.0 ? -min_x : max_x;
104 
105  // The search must go from 0 wraps to the maximum number of wraps that can
106  // still lead to finding the right wrapping.
107  // In theory, it is still possible to find the right slope value when the
108  // slope is so steep that there is a wrap in the small grid distance. This can
109  // lead to a large nr of wraps; e.g. in an extreme case of fitting a slope
110  // through two values around 150 MHz separated by 1 Hz it would cause 150e6
111  // values to search through.
112  int max_wraps = std::ceil(max_abs_x / min_delta);
113 
114  // There is another upper bound to the max nr of wraps: with a low nr of
115  // values, there is a fixed, low nr of ways that the values can have been
116  // wrapped. This leads to an exponential bound (~ 2^n_samples). However, in
117  // this case, the search space is not uniform, so we add a bit more space. It
118  // is only relevant for a low nr of samples.
119  if (data.size() < 20) {
120  constexpr int extra_space = 10;
121  max_wraps = std::min(max_wraps, extra_space * (2 << data.size()));
122  }
123 
124  if (wrap_count_limit) {
125  max_wraps = std::min(max_wraps, *wrap_count_limit);
126  }
127 
128  // To not miss any possible correct unwrapping, we need to step through every
129  // slope for which the largest x-value increases by 2pi. Given y = slope * x,
130  // we can dy = dslope * x. Therefore, dslope = dy / max_abs_x = 2 pi /
131  // max_abs_x. However, with 2pi steps it seems some slopes are missed (unit
132  // tests fail), but with 'pi' steps it works.
133  const double wrap_step = M_PI / max_abs_x;
134 
135  return SlopeFitRange{max_wraps, wrap_step};
136 }
137 
143 inline double FitSlope(const std::vector<FitSample>& data,
144  const SlopeFitRange& range) {
145  double best_cost = std::numeric_limits<double>::max();
146  double best_estimate = 0.0;
147  for (int i = -range.max_wraps; i <= range.max_wraps; ++i) {
148  double slope = range.wrap_step * i;
149  const double estimate = UnwrapAndFit(data, slope);
150  const double cost = SlopeModelCost(data, estimate);
151  if (cost < best_cost) {
152  best_cost = cost;
153  best_estimate = estimate;
154  }
155  }
156  // Perform one more fit iteration on the best estimate. Because a better
157  // estimate is known, the wrapping of noisy values might slightly change, so
158  // refit.
159  return UnwrapAndFit(data, best_estimate);
160 }
161 
185 inline double FitSlope(const std::vector<FitSample>& data) {
186  return FitSlope(data, GetRange(data));
187 }
188 
189 } // namespace dp3::common::phase_fitting
190 
191 #endif
bool estimate(size_t nDirection, size_t nStation, size_t nBaseline, size_t nChannel, const_cursor< Baseline > baselines, std::vector< const_cursor< std::complex< float >>> data, std::vector< const_cursor< std::complex< double >>> model, const_cursor< bool > flag, const_cursor< float > weight, const_cursor< std::complex< double >> mix, double *unknowns, size_t maxiter=50)
Definition: PhaseLineFitter.h:13
SlopeFitRange GetRange(const std::vector< FitSample > &data, aocommon::OptionalNumber< int > wrap_count_limit={})
Definition: PhaseLineFitter.h:90
double UnwrapAndFit(const std::vector< FitSample > &data, double unwrapping_slope)
Definition: PhaseLineFitter.h:50
double FitSlope(const std::vector< FitSample > &data, const SlopeFitRange &range)
Definition: PhaseLineFitter.h:143
double SlopeModelCost(const std::vector< FitSample > &data, double slope)
Definition: PhaseLineFitter.h:33
Definition: PhaseLineFitter.h:15
double y
Definition: PhaseLineFitter.h:24
double x
Definition: PhaseLineFitter.h:20
double weight
Definition: PhaseLineFitter.h:25
Definition: PhaseLineFitter.h:28
double wrap_step
Definition: PhaseLineFitter.h:30
int max_wraps
Definition: PhaseLineFitter.h:29