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

Simulations pane #20

Merged
merged 25 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a4da097
Add backend simulations viewset and tasks
annehaley Sep 13, 2023
e7e0a2d
Add available simulations list to UI
annehaley Sep 13, 2023
b85f928
Add refresh button to Available Datasets panel and reorder panels (fo…
annehaley Sep 13, 2023
45470b8
Remove console log
annehaley Sep 13, 2023
f21d33f
Clear error message on a successful request
annehaley Sep 13, 2023
b8c761e
Fix simulation arg options in request response
annehaley Sep 13, 2023
65bbde1
Allow POST requests without authentication (Phase 1 solution only)
annehaley Sep 13, 2023
615e524
Add POST endpoint to run simulation
annehaley Sep 13, 2023
6d089f5
Add Simulations panel to select inputs for run
annehaley Sep 13, 2023
e5e0717
Add client POST requests functions
annehaley Sep 13, 2023
7acf2c4
Add SimulationResult model
annehaley Sep 13, 2023
deb898b
Upgrade dependencies to eliminate errors from vuetify
annehaley Sep 13, 2023
07c3d15
Get SimulationResult objects related to a simulation type
annehaley Sep 13, 2023
dad8382
Show existing SimulationResult objects and their input args
annehaley Sep 13, 2023
d5652cc
Improve appearance of items in Active Layers
annehaley Sep 13, 2023
9202e6f
Fill simulation function
annehaley Sep 14, 2023
9e27e03
Add Node Failure Animation component
annehaley Sep 14, 2023
9814351
Don't redraw network layer until new gcc is received (avoid flickering)
annehaley Sep 15, 2023
d58061d
Remove redundant model inheritance
annehaley Sep 20, 2023
9fb1640
Add error message to SimulationResult, add input checking
annehaley Sep 21, 2023
3f60dd8
Constrain simulation_id and input_args as unique together
annehaley Sep 21, 2023
6cf524a
Don't overwrite other rest framework defaults
annehaley Sep 21, 2023
1cd52b8
Add City reference on Simulation Results
annehaley Sep 21, 2023
c1c5661
Require network mode enabled for network failure animation
annehaley Sep 21, 2023
c992c06
Use UniqueConstraint instead of unique_together
annehaley Sep 21, 2023
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
7 changes: 6 additions & 1 deletion uvdat/core/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.contrib import admin

from uvdat.core.models import Chart, City, Dataset, NetworkNode, Region
from uvdat.core.models import Chart, City, Dataset, NetworkNode, Region, SimulationResult


class CityAdmin(admin.ModelAdmin):
Expand All @@ -26,8 +26,13 @@ class ChartAdmin(admin.ModelAdmin):
list_display = ['id', 'name']


class SimulationResultAdmin(admin.ModelAdmin):
list_display = ['id', 'simulation_id', 'input_args']


admin.site.register(City, CityAdmin)
admin.site.register(Dataset, DatasetAdmin)
admin.site.register(NetworkNode, NetworkNodeAdmin)
admin.site.register(Region, RegionAdmin)
admin.site.register(Chart, ChartAdmin)
admin.site.register(SimulationResult, SimulationResultAdmin)
43 changes: 43 additions & 0 deletions uvdat/core/migrations/0008_simulation_results.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Generated by Django 4.1 on 2023-09-13 15:11

from django.db import migrations, models
import django_extensions.db.fields


class Migration(migrations.Migration):
dependencies = [
('core', '0007_charts'),
]

operations = [
migrations.CreateModel(
name='SimulationResult',
fields=[
(
'id',
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name='ID'
),
),
(
'created',
django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True, verbose_name='created'
),
),
(
'modified',
django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, verbose_name='modified'
),
),
('simulation_id', models.IntegerField()),
('input_args', models.JSONField(blank=True, null=True)),
('output_data', models.JSONField(blank=True, null=True)),
],
options={
'get_latest_by': 'modified',
'abstract': False,
},
),
]
6 changes: 6 additions & 0 deletions uvdat/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,9 @@ class Chart(models.Model):
metadata = models.JSONField(blank=True, null=True)
style = models.JSONField(blank=True, null=True)
clearable = models.BooleanField(default=False)


class SimulationResult(TimeStampedModel, models.Model):
simulation_id = models.IntegerField()
input_args = models.JSONField(blank=True, null=True)
output_data = models.JSONField(blank=True, null=True)
8 changes: 7 additions & 1 deletion uvdat/core/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from rest_framework import serializers

from uvdat.core.models import Chart, City, Dataset, NetworkNode
from uvdat.core.models import Chart, City, Dataset, NetworkNode, SimulationResult


class NetworkNodeSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -42,3 +42,9 @@ def get_center(self, obj):
class Meta:
model = City
fields = '__all__'


class SimulationResultSerializer(serializers.ModelSerializer):
class Meta:
model = SimulationResult
fields = '__all__'
2 changes: 1 addition & 1 deletion uvdat/core/tasks/conversion.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
import json
import os
from pathlib import Path
import tempfile
import zipfile
Expand Down
140 changes: 140 additions & 0 deletions uvdat/core/tasks/simulations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
from celery import shared_task

import json
import large_image
from django_large_image import tilesource
from pathlib import Path

import re
import shapely
import tempfile

from uvdat.core.models import Dataset, SimulationResult
from uvdat.core.serializers import DatasetSerializer, SimulationResultSerializer


def get_network_node_elevations(network_nodes, elevation_dataset):
with tempfile.TemporaryDirectory() as tmp:
raster_path = Path(tmp, 'raster')
with open(raster_path, 'wb') as raster_file:
raster_file.write(elevation_dataset.raster_file.read())
source = large_image.open(raster_path)
data, data_format = source.getRegion(format='numpy')
data = data[:, :, 0]
metadata = tilesource.get_metadata(source)
source_bounds = metadata.get('bounds')

elevations = {}
for network_node in network_nodes:
# same logic as client-side tooltip
location = network_node.location
x_proportion = (location[0] - source_bounds.get('xmin')) / (
source_bounds.get('xmax') - source_bounds.get('xmin')
)
y_proportion = (location[1] - source_bounds.get('ymin')) / (
source_bounds.get('ymax') - source_bounds.get('ymin')
)
x_index = int(x_proportion * len(data[0]))
y_index = int(y_proportion * len(data))
elevations[network_node.id] = data[y_index, x_index]
return elevations


@shared_task
def flood_scenario_1(simulation_result_id, network_dataset, elevation_dataset, flood_dataset):
result = SimulationResult.objects.get(id=simulation_result_id)
try:
network_dataset = Dataset.objects.get(id=network_dataset)
elevation_dataset = Dataset.objects.get(id=elevation_dataset)
flood_dataset = Dataset.objects.get(id=flood_dataset)
except Dataset.DoesNotExist:
return []

disabled_nodes = []
network_nodes = network_dataset.network_nodes.all()
flood_geodata = json.loads(flood_dataset.geodata_file.open().read().decode())
flood_areas = [
shapely.geometry.shape(feature['geometry']) for feature in flood_geodata['features']
]
for network_node in network_nodes:
node_point = shapely.geometry.Point(*network_node.location)
if any(flood_area.contains(node_point) for flood_area in flood_areas):
disabled_nodes.append(network_node)

node_elevations = get_network_node_elevations(network_nodes, elevation_dataset)
disabled_nodes.sort(key=lambda n: node_elevations[n.id])

result.output_data = [n.id for n in disabled_nodes]
result.save()


AVAILABLE_SIMULATIONS = [
{
'id': 1,
'name': 'Flood Scenario 1',
'description': '''
Provide a network dataset, elevation dataset, and flood dataset
to determine which network nodes go out of service
when the target flood occurs.
''',
'output_type': 'node_failure_animation',
'func': flood_scenario_1,
'args': [
{
'name': 'network_dataset',
'type': Dataset,
'options_query': {'network': True},
},
{
'name': 'elevation_dataset',
'type': Dataset,
'options_query': {'category': 'elevation'},
},
{
'name': 'flood_dataset',
'type': Dataset,
'options_query': {'category': 'flood'},
},
],
}
]


def get_available_simulations(city_id: int):
sims = []
for available in AVAILABLE_SIMULATIONS:
available = available.copy()
available['description'] = re.sub(r'\n\s+', ' ', available['description'])
available['args'] = [
{
'name': a['name'],
'options': list(
DatasetSerializer(d).data
for d in a['type']
.objects.filter(
city__id=city_id,
**a['options_query'],
)
.all()
),
}
for a in available['args']
]
del available['func']
sims.append(available)
return sims


def run_simulation(simulation_id: int, **kwargs):
simulation_matches = [s for s in AVAILABLE_SIMULATIONS if s['id'] == simulation_id]
if len(simulation_matches) > 0:
sim_result, created = SimulationResult.objects.get_or_create(
simulation_id=simulation_id, input_args=kwargs
)
sim_result.output_data = None
sim_result.save()

simulation = simulation_matches[0]
simulation['func'].delay(sim_result.id, **kwargs)
return SimulationResultSerializer(sim_result).data
return f"No simulation found with id {simulation_id}."
62 changes: 58 additions & 4 deletions uvdat/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,18 @@
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet, ModelViewSet, mixins

from uvdat.core.models import Chart, City, Dataset, Region
from uvdat.core.models import Chart, City, Dataset, Region, SimulationResult
from uvdat.core.serializers import (
ChartSerializer,
CitySerializer,
DatasetSerializer,
NetworkNodeSerializer,
SimulationResultSerializer,
)
from uvdat.core.tasks.charts import add_gcc_chart_datum
from uvdat.core.tasks.conversion import convert_raw_data
from uvdat.core.tasks.networks import network_gcc
from uvdat.core.tasks.simulations import get_available_simulations, run_simulation


class CityViewSet(ModelViewSet):
Expand All @@ -29,10 +31,16 @@ class CityViewSet(ModelViewSet):


class DatasetViewSet(ModelViewSet, LargeImageFileDetailMixin):
queryset = Dataset.objects.all()
serializer_class = DatasetSerializer
FILE_FIELD_NAME = 'raster_file'

def get_queryset(self):
city_id = self.request.query_params.get('city')
if city_id:
return Dataset.objects.filter(city__id=city_id)
else:
return Dataset.objects.all()

@action(detail=True, methods=['get'])
def regions(self, request, **kwargs):
dataset = self.get_object()
Expand Down Expand Up @@ -158,8 +166,7 @@ def get_queryset(self, **kwargs):
return Chart.objects.filter(city__id=city_id)
return Chart.objects.all()

# TODO: This should be POST once rest authentication is implemented
@action(detail=True, methods=['get'])
@action(detail=True, methods=['post'])
def clear(self, request, **kwargs):
chart = self.get_object()
if not chart.clearable:
Expand All @@ -169,3 +176,50 @@ def clear(self, request, **kwargs):
chart.chart_data = {}
chart.save()
return HttpResponse(status=200)


class SimulationViewSet(GenericViewSet):
# Not based on a database model;
# Available Simulations must be hard-coded
# and associated with a function

@action(
detail=False,
methods=['get'],
url_path="available",
)
def list_available(self, request, **kwargs):
city_id = request.query_params.get('city')
sims = get_available_simulations(city_id)
return HttpResponse(
json.dumps(sims),
status=200,
)

@action(
detail=False,
methods=['get'],
url_path=r'(?P<simulation_id>[\d*]+)/results',
)
def list_results(self, request, simulation_id: int, **kwargs):
return HttpResponse(
json.dumps(
list(
SimulationResultSerializer(s).data
for s in SimulationResult.objects.filter(simulation_id=int(simulation_id)).all()
)
),
status=200,
)

@action(
detail=False,
methods=['post'],
url_path=r'run/(?P<simulation_id>[\d*]+)',
)
def run(self, request, simulation_id: int, **kwargs):
result = run_simulation(int(simulation_id), **request.data)
return HttpResponse(
json.dumps({'result': result}),
status=200,
)
8 changes: 8 additions & 0 deletions uvdat/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ def mutate_configuration(configuration: ComposedConfiguration) -> None:
's3_file_field',
]

# Disable authentication requirements for REST
# TODO: configure authentication and remove this workaround
configuration.REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [],
'DEFAULT_PERMISSION_CLASSES': [],
'APPEND_SLASH': False,
}

configuration.DATABASES = {
'default': {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
Expand Down
1 change: 1 addition & 0 deletions uvdat/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
router.register(r'datasets', views.DatasetViewSet, basename='datasets')
router.register(r'cities', views.CityViewSet, basename='cities')
router.register(r'charts', views.ChartViewSet, basename='charts')
router.register(r'simulations', views.SimulationViewSet, basename='simulations')

urlpatterns = [
path('accounts/', include('allauth.urls')),
Expand Down
Loading