diff options
Diffstat (limited to 'samplefilt.c')
-rw-r--r-- | samplefilt.c | 452 |
1 files changed, 452 insertions, 0 deletions
diff --git a/samplefilt.c b/samplefilt.c new file mode 100644 index 0000000..f350e40 --- /dev/null +++ b/samplefilt.c @@ -0,0 +1,452 @@ +/* + chronyd/chronyc - Programs for keeping computer clocks accurate. + + ********************************************************************** + * Copyright (C) Miroslav Lichvar 2009-2011, 2014, 2016, 2018 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of version 2 of the GNU General Public License as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + ********************************************************************** + + ======================================================================= + + Routines implementing a median sample filter. + + */ + +#include "config.h" + +#include "local.h" +#include "logging.h" +#include "memory.h" +#include "regress.h" +#include "samplefilt.h" +#include "util.h" + +#define MIN_SAMPLES 1 +#define MAX_SAMPLES 256 + +struct SPF_Instance_Record { + int min_samples; + int max_samples; + int index; + int used; + int last; + int avg_var_n; + double avg_var; + double max_var; + double combine_ratio; + NTP_Sample *samples; + int *selected; + double *x_data; + double *y_data; + double *w_data; +}; + +/* ================================================== */ + +SPF_Instance +SPF_CreateInstance(int min_samples, int max_samples, double max_dispersion, double combine_ratio) +{ + SPF_Instance filter; + + filter = MallocNew(struct SPF_Instance_Record); + + min_samples = CLAMP(MIN_SAMPLES, min_samples, MAX_SAMPLES); + max_samples = CLAMP(MIN_SAMPLES, max_samples, MAX_SAMPLES); + max_samples = MAX(min_samples, max_samples); + combine_ratio = CLAMP(0.0, combine_ratio, 1.0); + + filter->min_samples = min_samples; + filter->max_samples = max_samples; + filter->index = -1; + filter->used = 0; + filter->last = -1; + /* Set the first estimate to the system precision */ + filter->avg_var_n = 0; + filter->avg_var = SQUARE(LCL_GetSysPrecisionAsQuantum()); + filter->max_var = SQUARE(max_dispersion); + filter->combine_ratio = combine_ratio; + filter->samples = MallocArray(NTP_Sample, filter->max_samples); + filter->selected = MallocArray(int, filter->max_samples); + filter->x_data = MallocArray(double, filter->max_samples); + filter->y_data = MallocArray(double, filter->max_samples); + filter->w_data = MallocArray(double, filter->max_samples); + + return filter; +} + +/* ================================================== */ + +void +SPF_DestroyInstance(SPF_Instance filter) +{ + Free(filter->samples); + Free(filter->selected); + Free(filter->x_data); + Free(filter->y_data); + Free(filter->w_data); + Free(filter); +} + +/* ================================================== */ + +/* Check that samples times are strictly increasing */ + +static int +check_sample(SPF_Instance filter, NTP_Sample *sample) +{ + if (filter->used <= 0) + return 1; + + if (UTI_CompareTimespecs(&filter->samples[filter->last].time, &sample->time) >= 0) { + DEBUG_LOG("filter non-increasing sample time %s", UTI_TimespecToString(&sample->time)); + return 0; + } + + return 1; +} + +/* ================================================== */ + +int +SPF_AccumulateSample(SPF_Instance filter, NTP_Sample *sample) +{ + if (!check_sample(filter, sample)) + return 0; + + filter->index++; + filter->index %= filter->max_samples; + filter->last = filter->index; + if (filter->used < filter->max_samples) + filter->used++; + + filter->samples[filter->index] = *sample; + + DEBUG_LOG("filter sample %d t=%s offset=%.9f peer_disp=%.9f", + filter->index, UTI_TimespecToString(&sample->time), + sample->offset, sample->peer_dispersion); + return 1; +} + +/* ================================================== */ + +int +SPF_GetLastSample(SPF_Instance filter, NTP_Sample *sample) +{ + if (filter->last < 0) + return 0; + + *sample = filter->samples[filter->last]; + return 1; +} + +/* ================================================== */ + +int +SPF_GetNumberOfSamples(SPF_Instance filter) +{ + return filter->used; +} + +/* ================================================== */ + +double +SPF_GetAvgSampleDispersion(SPF_Instance filter) +{ + return sqrt(filter->avg_var); +} + +/* ================================================== */ + +void +SPF_DropSamples(SPF_Instance filter) +{ + filter->index = -1; + filter->used = 0; +} + +/* ================================================== */ + +static const NTP_Sample *tmp_sort_samples; + +static int +compare_samples(const void *a, const void *b) +{ + const NTP_Sample *s1, *s2; + + s1 = &tmp_sort_samples[*(int *)a]; + s2 = &tmp_sort_samples[*(int *)b]; + + if (s1->offset < s2->offset) + return -1; + else if (s1->offset > s2->offset) + return 1; + return 0; +} + +/* ================================================== */ + +static int +select_samples(SPF_Instance filter) +{ + int i, j, k, o, from, to, *selected; + double min_dispersion; + + if (filter->used < filter->min_samples) + return 0; + + selected = filter->selected; + + /* With 4 or more samples, select those that have peer dispersion smaller + than 1.5x of the minimum dispersion */ + if (filter->used > 4) { + for (i = 1, min_dispersion = filter->samples[0].peer_dispersion; i < filter->used; i++) { + if (min_dispersion > filter->samples[i].peer_dispersion) + min_dispersion = filter->samples[i].peer_dispersion; + } + + for (i = j = 0; i < filter->used; i++) { + if (filter->samples[i].peer_dispersion <= 1.5 * min_dispersion) + selected[j++] = i; + } + } else { + j = 0; + } + + if (j < 4) { + /* Select all samples */ + + for (j = 0; j < filter->used; j++) + selected[j] = j; + } + + /* And sort their indices by offset */ + tmp_sort_samples = filter->samples; + qsort(selected, j, sizeof (int), compare_samples); + + /* Select samples closest to the median */ + if (j > 2) { + from = j * (1.0 - filter->combine_ratio) / 2.0; + from = CLAMP(1, from, (j - 1) / 2); + } else { + from = 0; + } + + to = j - from; + + /* Mark unused samples and sort the rest by their time */ + + o = filter->used - filter->index - 1; + + for (i = 0; i < from; i++) + selected[i] = -1; + for (; i < to; i++) + selected[i] = (selected[i] + o) % filter->used; + for (; i < filter->used; i++) + selected[i] = -1; + + for (i = from; i < to; i++) { + j = selected[i]; + selected[i] = -1; + while (j != -1 && selected[j] != j) { + k = selected[j]; + selected[j] = j; + j = k; + } + } + + for (i = j = 0; i < filter->used; i++) { + if (selected[i] != -1) + selected[j++] = (selected[i] + filter->used - o) % filter->used; + } + + assert(j > 0 && j <= filter->max_samples); + + return j; +} + +/* ================================================== */ + +static int +combine_selected_samples(SPF_Instance filter, int n, NTP_Sample *result) +{ + double mean_peer_dispersion, mean_root_dispersion, mean_peer_delay, mean_root_delay; + double mean_x, mean_y, disp, var, prev_avg_var; + NTP_Sample *sample, *last_sample; + int i, dof; + + last_sample = &filter->samples[filter->selected[n - 1]]; + + /* Prepare data */ + for (i = 0; i < n; i++) { + sample = &filter->samples[filter->selected[i]]; + + filter->x_data[i] = UTI_DiffTimespecsToDouble(&sample->time, &last_sample->time); + filter->y_data[i] = sample->offset; + filter->w_data[i] = sample->peer_dispersion; + } + + /* Calculate mean offset and interval since the last sample */ + for (i = 0, mean_x = mean_y = 0.0; i < n; i++) { + mean_x += filter->x_data[i]; + mean_y += filter->y_data[i]; + } + mean_x /= n; + mean_y /= n; + + if (n >= 4) { + double b0, b1, s2, sb0, sb1; + + /* Set y axis to the mean sample time */ + for (i = 0; i < n; i++) + filter->x_data[i] -= mean_x; + + /* Make a linear fit and use the estimated standard deviation of the + intercept as dispersion */ + RGR_WeightedRegression(filter->x_data, filter->y_data, filter->w_data, n, + &b0, &b1, &s2, &sb0, &sb1); + var = s2; + disp = sb0; + dof = n - 2; + } else if (n >= 2) { + for (i = 0, disp = 0.0; i < n; i++) + disp += (filter->y_data[i] - mean_y) * (filter->y_data[i] - mean_y); + var = disp / (n - 1); + disp = sqrt(var); + dof = n - 1; + } else { + var = filter->avg_var; + disp = sqrt(var); + dof = 1; + } + + /* Avoid working with zero dispersion */ + if (var < 1e-20) { + var = 1e-20; + disp = sqrt(var); + } + + /* Drop the sample if the variance is larger than the maximum */ + if (filter->max_var > 0.0 && var > filter->max_var) { + DEBUG_LOG("filter dispersion too large disp=%.9f max=%.9f", + sqrt(var), sqrt(filter->max_var)); + return 0; + } + + prev_avg_var = filter->avg_var; + + /* Update the exponential moving average of the variance */ + if (filter->avg_var_n > 50) { + filter->avg_var += dof / (dof + 50.0) * (var - filter->avg_var); + } else { + filter->avg_var = (filter->avg_var * filter->avg_var_n + var * dof) / + (dof + filter->avg_var_n); + if (filter->avg_var_n == 0) + prev_avg_var = filter->avg_var; + filter->avg_var_n += dof; + } + + /* Use the long-term average of variance instead of the estimated value + unless it is significantly smaller in order to reduce the noise in + sourcestats weights */ + if (var * dof / RGR_GetChi2Coef(dof) < prev_avg_var) + disp = sqrt(filter->avg_var) * disp / sqrt(var); + + mean_peer_dispersion = mean_root_dispersion = mean_peer_delay = mean_root_delay = 0.0; + + for (i = 0; i < n; i++) { + sample = &filter->samples[filter->selected[i]]; + + mean_peer_dispersion += sample->peer_dispersion; + mean_root_dispersion += sample->root_dispersion; + mean_peer_delay += sample->peer_delay; + mean_root_delay += sample->root_delay; + } + + mean_peer_dispersion /= n; + mean_root_dispersion /= n; + mean_peer_delay /= n; + mean_root_delay /= n; + + UTI_AddDoubleToTimespec(&last_sample->time, mean_x, &result->time); + result->offset = mean_y; + result->peer_dispersion = MAX(disp, mean_peer_dispersion); + result->root_dispersion = MAX(disp, mean_root_dispersion); + result->peer_delay = mean_peer_delay; + result->root_delay = mean_root_delay; + result->stratum = last_sample->stratum; + + return 1; +} + +/* ================================================== */ + +int +SPF_GetFilteredSample(SPF_Instance filter, NTP_Sample *sample) +{ + int n; + + n = select_samples(filter); + + if (n < 1) + return 0; + + if (!combine_selected_samples(filter, n, sample)) + return 0; + + SPF_DropSamples(filter); + + return 1; +} + +/* ================================================== */ + +void +SPF_SlewSamples(SPF_Instance filter, struct timespec *when, double dfreq, double doffset) +{ + int i, first, last; + double delta_time; + + if (filter->last < 0) + return; + + /* Always slew the last sample as it may be returned even if no new + samples were accumulated */ + if (filter->used > 0) { + first = 0; + last = filter->used - 1; + } else { + first = last = filter->last; + } + + for (i = first; i <= last; i++) { + UTI_AdjustTimespec(&filter->samples[i].time, when, &filter->samples[i].time, + &delta_time, dfreq, doffset); + filter->samples[i].offset -= delta_time; + } +} + +/* ================================================== */ + +void +SPF_AddDispersion(SPF_Instance filter, double dispersion) +{ + int i; + + for (i = 0; i < filter->used; i++) { + filter->samples[i].peer_dispersion += dispersion; + filter->samples[i].root_dispersion += dispersion; + } +} |