Skip to content

Commit 6d2a0cd

Browse files
authored
Merge pull request #327 from OpenGeoscience/emphasize-ai
Modify labels to emphasize AI use
2 parents 05b7f5b + e3e5ac4 commit 6d2a0cd

File tree

11 files changed

+72
-3
lines changed

11 files changed

+72
-3
lines changed

uvdat/core/rest/analytics.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from __future__ import annotations
22

3+
import inspect
4+
35
from django.db.models import QuerySet
46
from rest_framework.decorators import action
57
from rest_framework.response import Response
8+
from rest_framework.serializers import ModelSerializer
69
from rest_framework.viewsets import ReadOnlyModelViewSet
710

811
from uvdat.core.models import Project, TaskResult
@@ -34,7 +37,21 @@ def list_types(self, request, project_id: int, **kwargs):
3437
for k, v in instance.get_input_options().items():
3538
if isinstance(v, QuerySet):
3639
filtered_queryset = v.filter_by_projects(Project.objects.filter(id=project_id))
37-
options = [{"id": o.id, "name": o.name} for o in filtered_queryset]
40+
input_serializer = next(
41+
iter(
42+
[
43+
s
44+
for _, s in inspect.getmembers(uvdat_serializers, inspect.isclass)
45+
if issubclass(s, ModelSerializer)
46+
and s.Meta.model == filtered_queryset.model
47+
]
48+
),
49+
None,
50+
)
51+
if input_serializer is not None:
52+
options = [input_serializer(o).data for o in filtered_queryset]
53+
else:
54+
options = [{"id": o.id, "name": o.name} for o in filtered_queryset]
3855
elif any(not isinstance(o, dict) for o in v):
3956
options = [{"id": o, "name": o} for o in v]
4057
else:
@@ -45,6 +62,7 @@ def list_types(self, request, project_id: int, **kwargs):
4562
"name": instance.name,
4663
"db_value": instance.db_value,
4764
"description": instance.description,
65+
"details": instance.details,
4866
"attribution": instance.attribution,
4967
"input_options": filtered_input_options,
5068
"input_types": instance.input_types,

uvdat/core/rest/serializers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ class AnalysisTypeSerializer(serializers.Serializer):
264264
name = serializers.CharField(max_length=255)
265265
db_value = serializers.CharField(max_length=25)
266266
description = serializers.CharField(max_length=255)
267+
details = serializers.CharField(max_length=2048, allow_null=True)
267268
attribution = serializers.CharField(max_length=255)
268269
input_options = serializers.JSONField()
269270
input_types = serializers.JSONField()

uvdat/core/tasks/analytics/analysis_type.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class AnalysisType(ABC):
1111
def __init__(self, *args):
1212
self.name = ""
1313
self.description = ""
14+
self.details = None
1415
self.db_value = "" # cannot be longer than 25 characters
1516
self.input_types = {}
1617
self.output_types = {}
@@ -34,6 +35,10 @@ def validate_inputs(self, inputs):
3435
if input_name not in inputs:
3536
raise AnalysisInputError(f"{input_name} not provided.")
3637

38+
@abstractmethod
39+
def finalize(self, result):
40+
raise NotImplementedError
41+
3742

3843
class AnalysisInputError(Exception):
3944
pass
@@ -55,5 +60,14 @@ def on_failure(self, exc, task_id, args, kwargs, einfo):
5560
task_result.write_error(err_msg)
5661

5762
def after_return(self, status, retval, task_id, args, kwargs, einfo): # noqa: PLR0913
63+
# Avoid circular import
64+
from . import analysis_types # noqa: PLC0415
65+
5866
task_result = self.get_task_result(args)
5967
task_result.complete()
68+
69+
analysis_type = next(
70+
iter(t for t in analysis_types if t().db_value == task_result.task_type), None
71+
)
72+
if analysis_type is not None:
73+
analysis_type().finalize(task_result)

uvdat/core/tasks/analytics/create_road_network.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ def run_task(self, *, project, **inputs):
5151
create_road_network.delay(result.id)
5252
return result
5353

54+
def finalize(self, result):
55+
pass
56+
5457

5558
def metadata_for_row(row):
5659
return {

uvdat/core/tasks/analytics/flood_network_failure.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ def validate_inputs(self, inputs):
6868
# data is at 10 meter resolution
6969
raise AnalysisInputError("Station radius must be greater than 10")
7070

71+
def finalize(self, result):
72+
pass
73+
7174

7275
def _get_station_region(point: Point, radius_meters: float) -> dict[str, Any]:
7376
"""Get a rectangular region around a point, sized by radius_meters."""

uvdat/core/tasks/analytics/flood_simulation.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,15 @@
1616
class FloodSimulation(AnalysisType):
1717
def __init__(self):
1818
super().__init__()
19-
self.name = "Flood Simulation"
19+
self.name = "AI Flood Simulation"
2020
self.description = "Select parameters to simulate a 24-hour flood of the Charles River"
21+
self.details = (
22+
"The AI extreme precipitation downscaler achieves a Mean Absolute Error (MAE) "
23+
"of 3.82 mm and a MAE of Annual Precipitation Maxima of 20.46 mm when tested "
24+
"on an unseen validation period of n=7305 days. The AI-optimized SIMHYD "
25+
"hydrological model achieves a Nash-Sutcliffe Efficiency (NSE) of 0.70 when "
26+
"tested on an unseen validation period of n=6209 days."
27+
)
2128
self.db_value = "flood_simulation"
2229
self.input_types = {
2330
"initial_conditions_id": "string",
@@ -61,6 +68,14 @@ def run_task(self, *, project, **inputs):
6168
flood_simulation.delay(result.id)
6269
return result
6370

71+
def finalize(self, result):
72+
seconds = (result.completed - result.created).total_seconds()
73+
result.status = (
74+
f"AI Simulation completed in {seconds:.2f} seconds "
75+
"(compare to baseline 5-hour numerical simulation)."
76+
)
77+
result.save()
78+
6479

6580
@shared_task(base=AnalysisTask)
6681
def flood_simulation(result_id):

uvdat/core/tasks/analytics/geoai_segmentation.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ def run_task(self, *, project, **inputs):
5959
geoai_segmentation.delay(result.id)
6060
return result
6161

62+
def finalize(self, result):
63+
pass
64+
6265

6366
@shared_task(base=AnalysisTask)
6467
def geoai_segmentation(result_id): # noqa: PLR0915

uvdat/core/tasks/analytics/network_recovery.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ def validate_inputs(self, inputs):
7979
if mode not in RECOVERY_MODES:
8080
raise AnalysisInputError("Recovery mode not a valid option")
8181

82+
def finalize(self, result):
83+
pass
84+
8285

8386
def get_network_graph(network):
8487
network = {

web/src/components/sidebars/AnalyticsPanel.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,14 @@ watch(
233233
</template>
234234
</v-tooltip>
235235
</v-card-title>
236+
<v-expansion-panels v-if="analysisStore.currentAnalysisType.details">
237+
<v-expansion-panel bg-color="transparent">
238+
<v-expansion-panel-title class="py-3" style="min-height: 0">Details</v-expansion-panel-title>
239+
<v-expansion-panel-text class="px-3">
240+
{{ analysisStore.currentAnalysisType.details }}
241+
</v-expansion-panel-text>
242+
</v-expansion-panel>
243+
</v-expansion-panels>
236244

237245
<v-tabs v-model="analysisStore.currentAnalysisTab" align-tabs="center" fixed-tabs>
238246
<v-tab value="new">Run New</v-tab>

web/src/store/panel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ function defaultPanelArrangement(): FloatingPanelConfig[] {
6464
},
6565
{
6666
id: "analytics",
67-
label: "Analytics",
67+
label: "AI & Analytics",
6868
visible: false,
6969
closeable: true,
7070
collapsed: true,

0 commit comments

Comments
 (0)