Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

/scoring refactoring and add model-run viewer endpoints #248

Closed
wants to merge 15 commits into from
Closed
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
10 changes: 8 additions & 2 deletions django/src/rdwatch_scoring/api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
from ninja import NinjaAPI

from .views import router as scores
from .views.model_run import router as modelruns
from .views.performer import router as performers
from .views.region import router as regions
from .views.scores import router as scores

api = NinjaAPI(urls_namespace='scoring')

api.add_router('/scores/', scores)
api.add_router('/scoring/scores/', scores)
api.add_router('/scoring/performers/', performers)
api.add_router('/scoring/regions/', regions)
api.add_router('/scoring/model-runs/', modelruns)
24 changes: 12 additions & 12 deletions django/src/rdwatch_scoring/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class Addrfeat(models.Model):
rtotyp = models.CharField(max_length=1, blank=True, null=True)
offsetl = models.CharField(max_length=1, blank=True, null=True)
offsetr = models.CharField(max_length=1, blank=True, null=True)
the_geom = models.LineStringField(srid=4269, blank=True, null=True)
the_geom = models.LineStringField(srid=4326, blank=True, null=True)

class Meta:
managed = False
Expand Down Expand Up @@ -79,7 +79,7 @@ class Bg(models.Model):
awater = models.FloatField(blank=True, null=True)
intptlat = models.CharField(max_length=11, blank=True, null=True)
intptlon = models.CharField(max_length=12, blank=True, null=True)
the_geom = models.MultiPolygonField(srid=4269, blank=True, null=True)
the_geom = models.MultiPolygonField(srid=4326, blank=True, null=True)

class Meta:
managed = False
Expand All @@ -106,7 +106,7 @@ class County(models.Model):
awater = models.FloatField(blank=True, null=True)
intptlat = models.CharField(max_length=11, blank=True, null=True)
intptlon = models.CharField(max_length=12, blank=True, null=True)
the_geom = models.MultiPolygonField(srid=4269, blank=True, null=True)
the_geom = models.MultiPolygonField(srid=4326, blank=True, null=True)

class Meta:
managed = False
Expand Down Expand Up @@ -162,7 +162,7 @@ class Cousub(models.Model):
awater = models.DecimalField(max_digits=14, decimal_places=0, blank=True, null=True)
intptlat = models.CharField(max_length=11, blank=True, null=True)
intptlon = models.CharField(max_length=12, blank=True, null=True)
the_geom = models.MultiPolygonField(srid=4269, blank=True, null=True)
the_geom = models.MultiPolygonField(srid=4326, blank=True, null=True)

class Meta:
managed = False
Expand Down Expand Up @@ -213,7 +213,7 @@ class Edges(models.Model):
offsetr = models.CharField(max_length=1, blank=True, null=True)
tnidf = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
tnidt = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
the_geom = models.MultiLineStringField(srid=4269, blank=True, null=True)
the_geom = models.MultiLineStringField(srid=4326, blank=True, null=True)

class Meta:
managed = False
Expand Down Expand Up @@ -513,7 +513,7 @@ class Faces(models.Model):
atotal = models.FloatField(blank=True, null=True)
intptlat = models.CharField(max_length=11, blank=True, null=True)
intptlon = models.CharField(max_length=12, blank=True, null=True)
the_geom = models.MultiPolygonField(srid=4269, blank=True, null=True)
the_geom = models.MultiPolygonField(srid=4326, blank=True, null=True)
tractce20 = models.CharField(max_length=6, blank=True, null=True)
blkgrpce20 = models.CharField(max_length=1, blank=True, null=True)
blockce20 = models.CharField(max_length=4, blank=True, null=True)
Expand Down Expand Up @@ -754,7 +754,7 @@ class Place(models.Model):
awater = models.BigIntegerField(blank=True, null=True)
intptlat = models.CharField(max_length=11, blank=True, null=True)
intptlon = models.CharField(max_length=12, blank=True, null=True)
the_geom = models.MultiPolygonField(srid=4269, blank=True, null=True)
the_geom = models.MultiPolygonField(srid=4326, blank=True, null=True)

class Meta:
managed = False
Expand Down Expand Up @@ -851,7 +851,7 @@ class State(models.Model):
awater = models.BigIntegerField(blank=True, null=True)
intptlat = models.CharField(max_length=11, blank=True, null=True)
intptlon = models.CharField(max_length=12, blank=True, null=True)
the_geom = models.MultiPolygonField(srid=4269, blank=True, null=True)
the_geom = models.MultiPolygonField(srid=4326, blank=True, null=True)

class Meta:
managed = False
Expand Down Expand Up @@ -898,7 +898,7 @@ class Tabblock(models.Model):
awater = models.FloatField(blank=True, null=True)
intptlat = models.CharField(max_length=11, blank=True, null=True)
intptlon = models.CharField(max_length=12, blank=True, null=True)
the_geom = models.MultiPolygonField(srid=4269, blank=True, null=True)
the_geom = models.MultiPolygonField(srid=4326, blank=True, null=True)

class Meta:
managed = False
Expand All @@ -922,7 +922,7 @@ class Tabblock20(models.Model):
awater = models.FloatField(blank=True, null=True)
intptlat = models.CharField(max_length=11, blank=True, null=True)
intptlon = models.CharField(max_length=12, blank=True, null=True)
the_geom = models.MultiPolygonField(srid=4269, blank=True, null=True)
the_geom = models.MultiPolygonField(srid=4326, blank=True, null=True)
housing = models.FloatField(blank=True, null=True)
pop = models.FloatField(blank=True, null=True)

Expand Down Expand Up @@ -958,7 +958,7 @@ class Tract(models.Model):
awater = models.FloatField(blank=True, null=True)
intptlat = models.CharField(max_length=11, blank=True, null=True)
intptlon = models.CharField(max_length=12, blank=True, null=True)
the_geom = models.MultiPolygonField(srid=4269, blank=True, null=True)
the_geom = models.MultiPolygonField(srid=4326, blank=True, null=True)

class Meta:
managed = False
Expand All @@ -978,7 +978,7 @@ class Zcta5(models.Model):
intptlat = models.CharField(max_length=11, blank=True, null=True)
intptlon = models.CharField(max_length=12, blank=True, null=True)
partflg = models.CharField(max_length=1, blank=True, null=True)
the_geom = models.MultiPolygonField(srid=4269, blank=True, null=True)
the_geom = models.MultiPolygonField(srid=4326, blank=True, null=True)

class Meta:
managed = False
Expand Down
162 changes: 162 additions & 0 deletions django/src/rdwatch_scoring/views/model_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
from ninja import FilterSchema, Query, Schema
from ninja.errors import ValidationError
from ninja.pagination import RouterPaginated

from django.contrib.gis.db.models.fields import PolygonField
from django.contrib.gis.db.models.functions import AsGeoJSON
from django.db.models import Aggregate, CharField, Count, F, JSONField, Max, Min, Value
from django.db.models.functions import Cast, Concat, JSONObject, NullIf, Upper
from django.http import HttpRequest

from rdwatch.db.functions import AggregateArraySubquery, ExtractEpoch
from rdwatch.schemas.common import TimeRangeSchema
from rdwatch.views.performer import PerformerSchema
from rdwatch.views.region import RegionSchema
from rdwatch_scoring.models import EvaluationRun

router = RouterPaginated()


class TimeRangeJSON(NullIf):
"""Represents the min/max time of a field as JSON"""

def __init__(self, min, max, performer, site_originator):
json = JSONObject(
min=ExtractEpoch(Min(min, filter=F(performer) == F(site_originator))),
max=ExtractEpoch(Max(max, filter=F(performer) == F(site_originator))),
)
null = Value({'min': None, 'max': None}, output_field=JSONField())
return super().__init__(json, null)


class ModelRunFilterSchema(FilterSchema):
performer: str | None
region: str | None


class BoundingBoxPolygon(Aggregate):
"""Gets the WGS-84 bounding box of a geometry stored in Web Mercator coordinates"""

template = 'ST_Extent(%(expressions)s)'
arity = 1
output_field = PolygonField() # type: ignore


class BoundingBoxGeoJSON(Cast):
"""Gets the GeoJSON bounding box of a geometry in Web Mercator coordinates"""

def __init__(self, field):
bbox = BoundingBoxPolygon(field)
json_str = AsGeoJSON(bbox)
return super().__init__(json_str, JSONField())


class HyperParametersDetailSchema(Schema):
id: str
title: str
region: RegionSchema | None = None
performer: PerformerSchema
# parameters: dict
numsites: int
# downloading: int | None = None
score: float | None = None
timestamp: int | None = None
timerange: TimeRangeSchema | None = None
bbox: dict | None
ground_truth: bool
# created: datetime
expiration_time: str | None = None
evaluation: int | None = None
evaluation_run: int | None = None


class EvaluationListSchema(Schema):
id: int
siteNumber: int
region: RegionSchema | None


class HyperParametersListSchema(Schema):
count: int
timerange: TimeRangeSchema | None = None
bbox: dict | None = None
results: list[HyperParametersDetailSchema]


def get_queryset():
return (
EvaluationRun.objects.select_related('site', 'observation')
.order_by(
'start_datetime',
)
.annotate(
json=JSONObject(
id='pk',
title=Concat(
F('performer'),
Value('_'),
F('region'),
Value('_'),
F('evaluation_number'),
Value('_'),
F('evaluation_run_number'),
output_field=CharField(),
),
performer=JSONObject(
id=Value(-1), team_name='performer', short_code=Upper('performer')
),
region=JSONObject(id=Value(-1), name='region'),
score=None,
numsites=Count(
'site__site_id',
filter=F('performer') == F('site__originator'),
distinct=True,
),
evaluation='evaluation_number',
evaluation_run='evaluation_run_number',
timerange=TimeRangeJSON(
'site__start_date', 'site__end_date', 'performer', 'site_originator'
),
timestamp=ExtractEpoch('start_datetime'),
ground_truth=False,
# timerange=TimeRangeJSON('evaluations__observations__timestamp'),
bbox=BoundingBoxGeoJSON('site__union_geometry'),
)
)
)


@router.get('/', response={200: HyperParametersListSchema})
def list_model_runs(
request: HttpRequest,
filters: ModelRunFilterSchema = Query(...), # noqa: B008
limit: int = 25,
page: int = 1,
):
queryset = get_queryset()
queryset = filters.filter(queryset=queryset)

if page < 1 or (not limit and page != 1):
raise ValidationError(f"Invalid page '{page}'")

# Calculate total number of model runs prior to paginating queryset
total_model_run_count = queryset.count()

subquery = queryset[(page - 1) * limit : page * limit] if limit else queryset
aggregate = queryset.defer('json').aggregate(
timerange=TimeRangeJSON(
'site__start_date', 'site__end_date', 'performer', 'site_originator'
),
results=AggregateArraySubquery(subquery.values('json')),
)

aggregate['count'] = total_model_run_count

if filters.region is not None:
aggregate |= queryset.defer('json').aggregate(
# Use the region polygon for the bbox if it exists.
# Otherwise, fall back on the site polygon.
bbox=BoundingBoxGeoJSON('site__union_geometry'),
)

return 200, aggregate
31 changes: 31 additions & 0 deletions django/src/rdwatch_scoring/views/performer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from ninja import Schema
from ninja.pagination import RouterPaginated

from django.http import HttpRequest

from rdwatch_scoring.models import EvaluationRun


class PerformerSchema(Schema):
id: int
team_name: str
short_code: str


router = RouterPaginated()


@router.get('/', response=list[PerformerSchema])
def list_performers(request: HttpRequest):
unique_performers = (
EvaluationRun.objects.order_by().values_list('performer', flat=True).distinct()
)
performers_list = [
{
'id': idx + 1,
'team_name': performer,
'short_code': performer.lower().replace(' ', '_'),
}
for idx, performer in enumerate(unique_performers)
]
return performers_list
27 changes: 27 additions & 0 deletions django/src/rdwatch_scoring/views/region.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from ninja import Schema
from ninja.pagination import RouterPaginated

from django.http import HttpRequest

from rdwatch_scoring.models import EvaluationRun


class RegionSchema(Schema):
id: int
name: str


router = RouterPaginated()


@router.get('/', response=list[RegionSchema])
def list_regions(request: HttpRequest):
unique_regions = (
EvaluationRun.objects.order_by().values_list('region', flat=True).distinct()
)

region_list = [
{'id': idx + 1, 'name': region} for idx, region in enumerate(unique_regions)
]

return region_list
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from rdwatch.models import HyperParameters, Region
from rdwatch.views.region import RegionSchema

from .models import (
from ..models import (
EvaluationActivityClassificationTemporalIou,
EvaluationBroadAreaSearchDetection,
EvaluationRun,
Expand Down
Loading
Loading