Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ const taskName = ref('')
const schedule = ref<TaskSchedule | null>(null)
const inputDatastreamId = ref<string | null>(null)
const outputDatastreamId = ref<string | null>(null)
const aggregationMethod = ref<AggregationMethod>('mean')
const aggregationMethod = ref<AggregationMethod>('time_weighted_mean')
const outputInterval = ref<number | null>(1)
const outputIntervalUnits = ref<IntervalUnit>('hours')
const minValues = ref<number | null>(null)
Expand All @@ -264,12 +264,13 @@ const timezone = ref<string | null>(null)
const selectedThingId = computed(() => props.initialThingId ?? null)

const aggregationMethodOptions = [
{ title: 'Mean', value: 'mean' },
{ title: 'Sum', value: 'sum' },
{ title: 'Min', value: 'min' },
{ title: 'Max', value: 'max' },
{ title: 'Arithmetic Mean', value: 'mean' },
{ title: 'First', value: 'first' },
{ title: 'Last', value: 'last' },
{ title: 'Max', value: 'max' },
{ title: 'Min', value: 'min' },
{ title: 'Sum', value: 'sum' },
{ title: 'Time-weighted mean', value: 'time_weighted_mean' },
]

const intervalUnitOptions = [
Expand Down
12 changes: 8 additions & 4 deletions django/contracts/openapi/data.openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,8 @@
"min",
"max",
"first",
"last"
"last",
"time_weighted_mean"
],
"title": "Aggregationmethod",
"type": "string"
Expand Down Expand Up @@ -731,7 +732,8 @@
"min",
"max",
"first",
"last"
"last",
"time_weighted_mean"
],
"title": "Aggregationmethod",
"type": "string"
Expand Down Expand Up @@ -823,7 +825,8 @@
"min",
"max",
"first",
"last"
"last",
"time_weighted_mean"
],
"title": "Aggregationmethod",
"type": "string"
Expand Down Expand Up @@ -912,7 +915,8 @@
"min",
"max",
"first",
"last"
"last",
"time_weighted_mean"
],
"title": "Aggregationmethod",
"type": "string"
Expand Down
2 changes: 1 addition & 1 deletion django/interfaces/api/schemas/products/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from interfaces.api.schemas.products.rating_curve import RatingCurveSummaryResponse


AggregationMethod = Literal["mean", "sum", "min", "max", "first", "last"]
AggregationMethod = Literal["mean", "sum", "min", "max", "first", "last", "time_weighted_mean"]
Period = Literal["minutes", "hours", "days", "weeks", "months"]
TimezoneType = Literal["offset", "iana"]

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.2 on 2026-07-15 17:29

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('products', '0001_initial'),
]

operations = [
migrations.AlterField(
model_name='dataproducttransformation',
name='aggregation_method',
field=models.CharField(blank=True, choices=[('mean', 'Mean'), ('sum', 'Sum'), ('min', 'Min'), ('max', 'Max'), ('first', 'First'), ('last', 'Last'), ('time_weighted_mean', 'Time Weighted Mean')], max_length=255, null=True),
),
]
1 change: 1 addition & 0 deletions django/processing/products/models/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class AggregationMethod(models.TextChoices):
MAX = "max"
FIRST = "first"
LAST = "last"
TIME_WEIGHTED_MEAN = "time_weighted_mean"


class IntervalUnits(models.TextChoices):
Expand Down
2 changes: 1 addition & 1 deletion django/processing/products/services/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
rating_curve_service = RatingCurveService()

TransformationType = Literal["rating_curve", "expression", "composite_expression", "aggregation"]
AggregationMethod = Literal["mean", "sum", "min", "max", "first", "last"]
AggregationMethod = Literal["mean", "sum", "min", "max", "first", "last", "time_weighted_mean"]
IntervalUnits = Literal["minutes", "hours", "days", "weeks", "months"]


Expand Down
64 changes: 64 additions & 0 deletions django/tests/products/services/test_transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,21 @@ def test_create_aggregation_transformation(get_principal, ds_free):
assert result.aggregation_method == "mean"


def test_create_aggregation_transformation_time_weighted_mean(get_principal, ds_free):
result = transformation_service.create(
task=uuid.UUID(TASK1),
principal=get_principal("owner"),
transformation_type="aggregation",
output_datastream=ds_free,
aggregation_method="time_weighted_mean",
output_interval_units="hours",
output_interval=1,
input_datastreams=[TransformationInput(datastream=uuid.UUID(DS_IN_RC))],
)
assert result.transformation_type == "aggregation"
assert result.aggregation_method == "time_weighted_mean"


def test_create_composite_expression_transformation(get_principal, ds_free):
result = transformation_service.create(
task=uuid.UUID(TASK1),
Expand Down Expand Up @@ -906,3 +921,52 @@ def test_run_aggregation(run_context):
from core.sta.models.observation import Observation
results = sorted(Observation.objects.filter(datastream=ds_out).values_list("result", flat=True))
np.testing.assert_allclose(results, [2.0, 3.0])


def test_run_aggregation_time_weighted_mean(run_context):
"""
time_weighted_mean, 1-day UTC bucket [2025-02-10T00:00Z, 2025-02-11T00:00Z).

Observations at 06:00=1.0, 08:00=3.0, 10:00=2.0, 12:00=4.0. Boundary values are
flat-extrapolated (00:00 -> 1.0, 24:00 -> 4.0), then trapezoidal integration gives:
[00-06]: (1+1)/2*6 = 6
[06-08]: (1+3)/2*2 = 4
[08-10]: (3+2)/2*2 = 5
[10-12]: (2+4)/2*2 = 6
[12-24]: (4+4)/2*12= 48
total = 69 over 24h -> 2.875
This differs from the simple arithmetic mean of the same observations (2.5),
confirming the time-weighted method is actually wired through the run pipeline.
"""
ctx = run_context
kwargs = {k: ctx[k] for k in ("thing", "sensor", "observed_property", "processing_level", "unit")}
ds_in = _make_datastream(**kwargs)
ds_out = _make_datastream(**kwargs)

for dt, val in [
(datetime(2025, 2, 10, 6, 0, tzinfo=dt_timezone.utc), 1.0),
(datetime(2025, 2, 10, 8, 0, tzinfo=dt_timezone.utc), 3.0),
(datetime(2025, 2, 10, 10, 0, tzinfo=dt_timezone.utc), 2.0),
(datetime(2025, 2, 10, 12, 0, tzinfo=dt_timezone.utc), 4.0),
]:
_add_obs(ds_in, dt, val)
_set_end_time(ds_in, datetime(2025, 2, 11, 0, 0, tzinfo=dt_timezone.utc))

t = transformation_service.create(
task=uuid.UUID(TASK1),
principal=ctx["owner"],
transformation_type="aggregation",
output_datastream=ds_out.pk,
aggregation_method="time_weighted_mean",
output_interval_units="days",
output_interval=1,
input_datastreams=[TransformationInput(datastream=ds_in.pk)],
)
loaded = transformation_service.run(t)
assert loaded == 1

from core.sta.models.observation import Observation
out_obs = list(Observation.objects.filter(datastream=ds_out))
assert len(out_obs) == 1
assert out_obs[0].phenomenon_time == datetime(2025, 2, 10, 0, 0, tzinfo=dt_timezone.utc)
np.testing.assert_allclose(out_obs[0].result, 2.875)
8 changes: 4 additions & 4 deletions packages/hydroserver-ts/src/generated/data.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2258,7 +2258,7 @@ export interface components {
* Aggregationmethod
* @enum {string}
*/
aggregationMethod?: "mean" | "sum" | "min" | "max" | "first" | "last";
aggregationMethod?: "mean" | "sum" | "min" | "max" | "first" | "last" | "time_weighted_mean";
/**
* Inputdatastreamid
* Format: uuid
Expand Down Expand Up @@ -2289,7 +2289,7 @@ export interface components {
* Aggregationmethod
* @enum {string}
*/
aggregationMethod: "mean" | "sum" | "min" | "max" | "first" | "last";
aggregationMethod: "mean" | "sum" | "min" | "max" | "first" | "last" | "time_weighted_mean";
/**
* Id
* Format: uuid
Expand Down Expand Up @@ -2325,7 +2325,7 @@ export interface components {
* Aggregationmethod
* @enum {string}
*/
aggregationMethod: "mean" | "sum" | "min" | "max" | "first" | "last";
aggregationMethod: "mean" | "sum" | "min" | "max" | "first" | "last" | "time_weighted_mean";
/**
* Id
* Format: uuid
Expand Down Expand Up @@ -2353,7 +2353,7 @@ export interface components {
* Aggregationmethod
* @enum {string}
*/
aggregationMethod: "mean" | "sum" | "min" | "max" | "first" | "last";
aggregationMethod: "mean" | "sum" | "min" | "max" | "first" | "last" | "time_weighted_mean";
/**
* Id
* Format: uuid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel

AggregationMethod = Literal["mean", "sum", "min", "max", "first", "last"]
AggregationMethod = Literal["mean", "sum", "min", "max", "first", "last", "time_weighted_mean"]
Period = Literal["minutes", "hours", "days", "weeks", "months"]


Expand Down
Loading
Loading