src-local/central-film.h
Central-film observation windows
Small, solver-independent helpers for deciding whether a measured deposited film is quasi-steady. The caller owns interface reconstruction and supplies one spatially screened measurement at a time. A film gate and a whole-shape diagnostic are deliberately separate: a steady central film does not imply that both menisci form a travelling shape.
#ifndef CENTRAL_FILM_H
#define CENTRAL_FILM_H12
#include <math.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
double advance_window, minimum_observed_advance;
double film_relative_tolerance, front_speed_relative_tolerance;
double minimum_coverage, maximum_spatial_spread;
double minimum_fresh_fraction, minimum_cells;
double shape_relative_tolerance;
int hold_windows;
} CentralFilmConfig;
typedef struct {
double time, front, rear, centroid;
double film, spatial_spread, coverage, fresh_fraction, minimum_cells;
} CentralFilmMeasurement;
typedef struct {
double advance, film, front_speed, rear_speed, centroid_speed;
double film_drift, front_speed_drift;
double shape_speed_mismatch, length_drift;
double minimum_coverage, maximum_spatial_spread;
double minimum_fresh_fraction, minimum_cells;
int hold_count, shape_hold_count;
bool quality_ok, stable, film_converged, shape_steady, shape_converged;
} CentralFilmWindow;
typedef struct {
CentralFilmConfig config;
CentralFilmMeasurement first, previous, window_start;
double film_integral, covered_advance;
double previous_window_film, previous_front_speed;
double minimum_coverage, maximum_spatial_spread;
double minimum_fresh_fraction, minimum_cells;
int hold_count, shape_hold_count;
bool initialized, have_previous_window;
} CentralFilmObserver;
static int central_film_compare_double (const void * a, const void * b)
{
const double da = *(const double *) a, db = *(const double *) b;
return (da > db) - (da < db);
}Sorts values and returns its linearly
interpolated quantile.
static inline double central_film_quantile (double * values, int count,
double quantile)
{
if (!values || count <= 0 || !isfinite(quantile) ||
quantile < 0. || quantile > 1.)
return NAN;
qsort (values, (size_t) count, sizeof(double),
central_film_compare_double);
double index = quantile*(count - 1), lower = floor(index);
int lo = (int) lower, hi = lo + 1 < count ? lo + 1 : lo;
return values[lo] + (index - lower)*(values[hi] - values[lo]);
}
static inline double central_film_median (double * values, int count)
{
return central_film_quantile (values, count, 0.5);
}
static inline void central_film_observer_init (CentralFilmObserver * observer,
CentralFilmConfig config)
{
memset (observer, 0, sizeof(*observer));
observer->config = config;
}Clears all observations while preserving the configured predicates.
static inline void central_film_observer_reset (CentralFilmObserver * observer)
{
if (!observer)
return;
CentralFilmConfig config = observer->config;
memset (observer, 0, sizeof(*observer));
observer->config = config;
}
static inline double central_film_relative_change (double current,
double previous)
{
return previous != 0. ? fabs(current - previous)/fabs(previous) : INFINITY;
}
static inline void central_film_window_reset (CentralFilmObserver * observer,
CentralFilmMeasurement sample)
{
observer->window_start = sample;
observer->previous = sample;
observer->film_integral = observer->covered_advance = 0.;
observer->minimum_coverage = sample.coverage;
observer->maximum_spatial_spread = sample.spatial_spread;
observer->minimum_fresh_fraction = sample.fresh_fraction;
observer->minimum_cells = sample.minimum_cells;
}Adds a measurement. Returns true only when an advance
window has completed; window is unchanged
otherwise. Initialising a new observer anchors all
cumulative distances to the first supplied sample, which
makes a restored run earn fresh observation windows
before it can stop.
static inline bool central_film_observer_push (CentralFilmObserver * observer,
CentralFilmMeasurement sample,
CentralFilmWindow * window)
{
if (!observer || !window)
return false;
if (!isfinite(sample.time) ||
!isfinite(sample.front) || !isfinite(sample.rear) ||
!isfinite(sample.centroid) || !isfinite(sample.film) ||
!isfinite(sample.spatial_spread) || !isfinite(sample.coverage) ||
!isfinite(sample.fresh_fraction) || !isfinite(sample.minimum_cells)) {
central_film_observer_reset (observer);
return false;
}
const CentralFilmConfig * c = &observer->config;
bool sampleQuality = sample.film > 0. &&
sample.coverage >= c->minimum_coverage &&
sample.spatial_spread <= c->maximum_spatial_spread &&
sample.fresh_fraction >= c->minimum_fresh_fraction &&
(c->minimum_cells <= 0. || sample.minimum_cells >= c->minimum_cells);
if (!sampleQuality) {
central_film_observer_reset (observer);
return false;
}
if (!observer->initialized) {
observer->initialized = true;
observer->first = sample;
central_film_window_reset (observer, sample);
return false;
}
if (sample.time <= observer->previous.time ||
sample.front <= observer->previous.front) {
central_film_observer_reset (observer);
observer->initialized = true;
observer->first = sample;
central_film_window_reset (observer, sample);
return false;
}
double advance = sample.front - observer->previous.front;
if (advance > 0.) {
observer->film_integral +=
0.5*(sample.film + observer->previous.film)*advance;
observer->covered_advance += advance;
}
observer->minimum_coverage = fmin(observer->minimum_coverage,
sample.coverage);
observer->maximum_spatial_spread = fmax(observer->maximum_spatial_spread,
sample.spatial_spread);
observer->minimum_fresh_fraction = fmin(observer->minimum_fresh_fraction,
sample.fresh_fraction);
observer->minimum_cells = fmin(observer->minimum_cells,
sample.minimum_cells);
observer->previous = sample;
const double span = sample.front - observer->window_start.front;
const double elapsed = sample.time - observer->window_start.time;
if (span < observer->config.advance_window ||
observer->covered_advance <= 0. || elapsed <= 0.)
return false;
memset (window, 0, sizeof(*window));
window->advance = span;
window->film = observer->film_integral/observer->covered_advance;
window->front_speed = span/elapsed;
window->rear_speed = (sample.rear - observer->window_start.rear)/elapsed;
window->centroid_speed =
(sample.centroid - observer->window_start.centroid)/elapsed;
window->minimum_coverage = observer->minimum_coverage;
window->maximum_spatial_spread = observer->maximum_spatial_spread;
window->minimum_fresh_fraction = observer->minimum_fresh_fraction;
window->minimum_cells = observer->minimum_cells;
const double length0 = observer->window_start.front -
observer->window_start.rear;
const double length1 = sample.front - sample.rear;
window->length_drift = length0 > 0. ? fabs(length1 - length0)/length0 :
INFINITY;
const double speed_scale = fabs(window->centroid_speed);
window->shape_speed_mismatch = speed_scale > 0. ?
fmax(fabs(window->front_speed - window->centroid_speed),
fabs(window->rear_speed - window->centroid_speed))/speed_scale :
INFINITY;
window->quality_ok = window->film > 0. && window->front_speed > 0. &&
window->minimum_coverage >= c->minimum_coverage &&
window->maximum_spatial_spread <= c->maximum_spatial_spread &&
window->minimum_fresh_fraction >= c->minimum_fresh_fraction &&
(c->minimum_cells <= 0. || window->minimum_cells >= c->minimum_cells);
window->shape_steady =
window->shape_speed_mismatch <= c->shape_relative_tolerance &&
window->length_drift <= c->shape_relative_tolerance;
if (observer->have_previous_window) {
window->film_drift = central_film_relative_change
(window->film, observer->previous_window_film);
window->front_speed_drift = central_film_relative_change
(window->front_speed, observer->previous_front_speed);
const double observed = sample.front - observer->first.front;
window->stable = window->quality_ok &&
observed >= c->minimum_observed_advance &&
window->film_drift <= c->film_relative_tolerance &&
window->front_speed_drift <= c->front_speed_relative_tolerance;
observer->hold_count = window->stable ? observer->hold_count + 1 : 0;
observer->shape_hold_count = window->stable && window->shape_steady ?
observer->shape_hold_count + 1 : 0;
}
else {
window->film_drift = window->front_speed_drift = INFINITY;
observer->hold_count = 0;
observer->shape_hold_count = 0;
observer->have_previous_window = true;
}
observer->previous_window_film = window->film;
observer->previous_front_speed = window->front_speed;
window->hold_count = observer->hold_count;
window->shape_hold_count = observer->shape_hold_count;
window->film_converged = observer->hold_count >= c->hold_windows;
window->shape_converged = observer->shape_hold_count >= c->hold_windows;
central_film_window_reset (observer, sample);
return true;
}
#endif