Skip to content

Instantly share code, notes, and snippets.

@chrissnell
Last active December 18, 2021 01:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chrissnell/bcb1ce1bc15b49fd6c5b901e984060b7 to your computer and use it in GitHub Desktop.
Save chrissnell/bcb1ce1bc15b49fd6c5b901e984060b7 to your computer and use it in GitHub Desktop.
Circular average via custom PostgreSQL aggregate function
CREATE TYPE circular_avg_state AS (
sin_sum real,
cos_sum real,
accum real
);
CREATE OR REPLACE FUNCTION circular_avg_state_accumulator(state circular_avg_state, reading real)
RETURNS circular_avg_state
IMMUTABLE
LANGUAGE plpgsql
PARALLEL SAFE
AS $$
DECLARE
sin_sum real;
cos_sum real;
BEGIN
sin_sum := state.sin_sum + SIND(reading);
cos_sum := state.cos_sum + COSD(reading);
RETURN ROW(sin_sum, cos_sum, state.accum + 1)::circular_avg_state;
END;
$$;
CREATE OR REPLACE FUNCTION circular_avg_final(state circular_avg_state)
RETURNS real
IMMUTABLE
LANGUAGE plpgsql
PARALLEL SAFE
AS $$
DECLARE
sin_avg real;
cos_avg real;
atan2_result real;
final_result real;
BEGIN
sin_avg := state.sin_sum / state.accum;
cos_avg := state.cos_sum / state.accum;
atan2_result := ATAN2D(sin_avg, cos_avg);
if atan2_result < 0 THEN
final_result := atan2_result + 360;
ELSE
final_result := atan2_result;
END IF;
RETURN final_result;
END;
$$;
CREATE OR REPLACE FUNCTION circular_avg_state_combiner(state1 circular_avg_state, state2 circular_avg_state)
RETURNS circular_avg_state
STRICT
IMMUTABLE
LANGUAGE plpgsql
AS $$
DECLARE
sin_sum real;
cos_sum real;
accum_sum real;
BEGIN
sin_sum := state1.sin_sum + state2.sin_sum;
cos_sum := state1.cos_sum + state2.cos_sum;
accum_sum := state1.accum + state2.accum;
RETURN ROW(sin_sum, cos_sum, accum_sum)::circular_avg_state;
END;
$$;
CREATE OR REPLACE AGGREGATE circular_avg (real)
(
SFUNC = circular_avg_state_accumulator,
STYPE = circular_avg_state,
COMBINEFUNC = circular_avg_state_combiner,
FINALFUNC = circular_avg_final,
INITCOND = '(0,0,0)',
PARALLEL = SAFE
);
CREATE MATERIALIZED VIEW IF NOT EXISTS weather_5m
WITH (timescaledb.continuous)
AS
SELECT
time_bucket('5 minutes', time) as bucket,
stationname,
avg(barometer) as barometer,
max(barometer) as max_barometer,A
min(barometer) as min_barometer,
avg(intemp) as intemp,
max(intemp) as max_intemp,
min(intemp) as min_intemp,
avg(inhumidity) as inhumidity,
max(inhumidity) as max_inhumidity,
min(inhumidity) as min_inhumidity,
avg(outtemp) as outtemp,
max(outtemp) as max_outtemp,
min(outtemp) as min_outtemp,
avg(outhumidity) as outhumidity,
max(outhumidity) as max_outhumidity,
min(outhumidity) as min_outhumidity,
circular_avg(winddir) as winddir,
avg(windspeed) as windspeed,
max(windspeed) as max_windspeed,
avg(windchill) as windchill,
min(windchill) as min_windchill,
avg(heatindex) as heatindex,
max(heatindex) as max_heatindex,
avg(rainrate) as rainrate,
max(rainrate) as max_rainrate,
max(dayrain) as dayrain,
max(monthrain) as monthrain,
max(yearrain) as yearrain,
avg(consbatteryvoltage) as consbatteryvoltage
FROM
weather
GROUP BY bucket, stationname;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment