Files
notifications-admin/tests/app/broadcast_areas/test_broadcast_area.py

402 lines
10 KiB
Python
Raw Normal View History

Vary bleed amount based on population density There are basically two kinds of 4G masts: Frequency | Range | Bandwidth ----------|-------------|---------------------------------- 800MHz | Long (500m) | Low (can handle a bit of traffic) 1800Mhz | Short (5km) | High (can handle lots of traffic) The 1800Mhz masts are better in terms of how much traffic they can handle and how fast a connection they provide. But because they have quite short range, it’s only economical to install them in very built up areas†. In more rural areas the 800MHz masts are better because they cover a wider area, and have enough bandwidth for the lower population density. The net effect of this is that cell broadcasts in rural areas are likely to bleed further, because the masts they are being broadcast from are less precise. We can use population density as a proxy for how likely it is to be covered by 1800Mhz masts, and therefore how much bleed we should expect. So this commit varies the amount of bleed shown based on the population density. I came up with the formula based on 3 fixed points: - The most remote areas (for example the Scottish Highlands) should have the highest average bleed, estimated at 5km - An town, like Crewe, should have about the same bleed as we were estimating before (1.5km) – Pete D thinks this is about right based on his knowledge of the area around his office in Crewe - The most built up areas, like London boroughs, could have as little as 500m of bleed Based on these three figures I came up with the following formula, which roughly gives the right bleed distance (`b`) for each of their population densities (`d`): ``` b = 5900 - (log10(d) × 1_250) ``` Plotted on a curve it looks like this: This is based on averages – remember that the UI shows where is _likely_ to receive the alert, based on bleed, not where it’s _possible_ to receive the alert. Here’s what it looks like on the map: --- †There are some additional subtleties which make this not strictly true: - The 800Mhz masts are also used in built up areas to fill in the gaps between the areas covered by the 1800Mhz masts - Switching between masts is inefficient, so if you’re moving fast through a built up area (for example on a train) your phone will only use the 800MHz masts so that you have to handoff from one mast to another less often
2021-03-12 09:17:42 +00:00
from math import isclose
import pytest
Estimate number of phones in an arbitrary polygon We want to know how many phones are in a user-supplied polygon, so we can show the impact of a broadcast, in the same way that we do when users pick areas from our library. We already know how many phones are in each electoral ward. But there are challenges with an arbitrary polygon: - where it does overlap a ward, the overlap could be partial - it could overlap more than one ward - finding out which wards it overlaps by brute force (looping through all the wards and seeing which ones intersect with our polygon) would be way to slow to do in real time Instead we can use a data structure called an R-tree[1] to build an index which provides a much, much faster way of looking up which polygons overlap another. We can build this tree in advance and save it somewhere, which means there’s a lot of computation we don’t need to do in real time. The R-tree returns a set of objects (ward IDs) which we can go and look up in our library of electoral wards. These wards will be the ones that might have some overlap with our custom polygon. Once we have this small set of wards which might overlap our ward, we can look at the size of the area of overlap (relative to the size of the whole ward) and multiply that by the known count of phones in that ward to get an approximation of the count of phones in the overlap area. Summing these approximations give an estimate for the whole area of the custom polygon. 1. https://en.wikipedia.org/wiki/R-tree
2021-03-18 23:02:32 +00:00
from custom_polygons import BRISTOL, SKYE
from app.broadcast_areas import (
BroadcastAreasRepository,
Estimate number of phones in an arbitrary polygon We want to know how many phones are in a user-supplied polygon, so we can show the impact of a broadcast, in the same way that we do when users pick areas from our library. We already know how many phones are in each electoral ward. But there are challenges with an arbitrary polygon: - where it does overlap a ward, the overlap could be partial - it could overlap more than one ward - finding out which wards it overlaps by brute force (looping through all the wards and seeing which ones intersect with our polygon) would be way to slow to do in real time Instead we can use a data structure called an R-tree[1] to build an index which provides a much, much faster way of looking up which polygons overlap another. We can build this tree in advance and save it somewhere, which means there’s a lot of computation we don’t need to do in real time. The R-tree returns a set of objects (ward IDs) which we can go and look up in our library of electoral wards. These wards will be the ones that might have some overlap with our custom polygon. Once we have this small set of wards which might overlap our ward, we can look at the size of the area of overlap (relative to the size of the whole ward) and multiply that by the known count of phones in that ward to get an approximation of the count of phones in the overlap area. Summing these approximations give an estimate for the whole area of the custom polygon. 1. https://en.wikipedia.org/wiki/R-tree
2021-03-18 23:02:32 +00:00
CustomBroadcastArea,
Add broadcast area model, loading from GeoJSON This commit adds a new model class which can be used by any app to interact with a broadcast area. A broadcast area is one or more polygons representing geographical areas. It also adds some models that make browsing collections of these areas more straightforward. So the hierarchy looks like: > **BroadcastAreaLibraries* > Contains multiple libraries of broadcast area > > **BroadcastAreaLibrary** > > A collection of geographic areas, all of the same type, for example > > counties or electoral wards > > **BroadcastArea** > > Contains one or more shapes that make up an area, for example > > England > > > **BroadcastArea.polygons[n]** > > > A single shape, for example the Isle of Wight or Lindisfarne > > > > **BroadcastArea.polygons[n][o]** > > > > A single coordinate along a polygons The classes support iteration, so all the areas in a library can be looped over, for example if `countries` is an instance of `BroadcastAreaLibrary` you can do: ```python for country in countries: print(country.name) ``` The `BroadcastAreaLibraries` class also provides some useful methods for quickly getting the polygons for an area or areas, for example to render them on a map. So if `libraries` is an instance of `BroadcastAreaLibraries` you can do: ```python libraries.get_polygons_for_areas_long_lat('england', 'wales') ``` This will give polygons for the Welsh mainland, the Isle of Wight, Anglesey, etc. The models load data from GeoJSON files, which is an open standard for serialising geographic data. I’ve added a few example files taken from http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
broadcast_area_libraries,
)
from app.broadcast_areas.populations import (
CITY_OF_LONDON,
estimate_number_of_smartphones_for_population,
)
Add broadcast area model, loading from GeoJSON This commit adds a new model class which can be used by any app to interact with a broadcast area. A broadcast area is one or more polygons representing geographical areas. It also adds some models that make browsing collections of these areas more straightforward. So the hierarchy looks like: > **BroadcastAreaLibraries* > Contains multiple libraries of broadcast area > > **BroadcastAreaLibrary** > > A collection of geographic areas, all of the same type, for example > > counties or electoral wards > > **BroadcastArea** > > Contains one or more shapes that make up an area, for example > > England > > > **BroadcastArea.polygons[n]** > > > A single shape, for example the Isle of Wight or Lindisfarne > > > > **BroadcastArea.polygons[n][o]** > > > > A single coordinate along a polygons The classes support iteration, so all the areas in a library can be looped over, for example if `countries` is an instance of `BroadcastAreaLibrary` you can do: ```python for country in countries: print(country.name) ``` The `BroadcastAreaLibraries` class also provides some useful methods for quickly getting the polygons for an area or areas, for example to render them on a map. So if `libraries` is an instance of `BroadcastAreaLibraries` you can do: ```python libraries.get_polygons_for_areas_long_lat('england', 'wales') ``` This will give polygons for the Welsh mainland, the Isle of Wight, Anglesey, etc. The models load data from GeoJSON files, which is an open standard for serialising geographic data. I’ve added a few example files taken from http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
Vary bleed amount based on population density There are basically two kinds of 4G masts: Frequency | Range | Bandwidth ----------|-------------|---------------------------------- 800MHz | Long (500m) | Low (can handle a bit of traffic) 1800Mhz | Short (5km) | High (can handle lots of traffic) The 1800Mhz masts are better in terms of how much traffic they can handle and how fast a connection they provide. But because they have quite short range, it’s only economical to install them in very built up areas†. In more rural areas the 800MHz masts are better because they cover a wider area, and have enough bandwidth for the lower population density. The net effect of this is that cell broadcasts in rural areas are likely to bleed further, because the masts they are being broadcast from are less precise. We can use population density as a proxy for how likely it is to be covered by 1800Mhz masts, and therefore how much bleed we should expect. So this commit varies the amount of bleed shown based on the population density. I came up with the formula based on 3 fixed points: - The most remote areas (for example the Scottish Highlands) should have the highest average bleed, estimated at 5km - An town, like Crewe, should have about the same bleed as we were estimating before (1.5km) – Pete D thinks this is about right based on his knowledge of the area around his office in Crewe - The most built up areas, like London boroughs, could have as little as 500m of bleed Based on these three figures I came up with the following formula, which roughly gives the right bleed distance (`b`) for each of their population densities (`d`): ``` b = 5900 - (log10(d) × 1_250) ``` Plotted on a curve it looks like this: This is based on averages – remember that the UI shows where is _likely_ to receive the alert, based on bleed, not where it’s _possible_ to receive the alert. Here’s what it looks like on the map: --- †There are some additional subtleties which make this not strictly true: - The 800Mhz masts are also used in built up areas to fill in the gaps between the areas covered by the 1800Mhz masts - Switching between masts is inefficient, so if you’re moving fast through a built up area (for example on a train) your phone will only use the 800MHz masts so that you have to handoff from one mast to another less often
2021-03-12 09:17:42 +00:00
def close_enough(a, b):
return isclose(a, b, rel_tol=0.001) # Within 0.1% difference
Add broadcast area model, loading from GeoJSON This commit adds a new model class which can be used by any app to interact with a broadcast area. A broadcast area is one or more polygons representing geographical areas. It also adds some models that make browsing collections of these areas more straightforward. So the hierarchy looks like: > **BroadcastAreaLibraries* > Contains multiple libraries of broadcast area > > **BroadcastAreaLibrary** > > A collection of geographic areas, all of the same type, for example > > counties or electoral wards > > **BroadcastArea** > > Contains one or more shapes that make up an area, for example > > England > > > **BroadcastArea.polygons[n]** > > > A single shape, for example the Isle of Wight or Lindisfarne > > > > **BroadcastArea.polygons[n][o]** > > > > A single coordinate along a polygons The classes support iteration, so all the areas in a library can be looped over, for example if `countries` is an instance of `BroadcastAreaLibrary` you can do: ```python for country in countries: print(country.name) ``` The `BroadcastAreaLibraries` class also provides some useful methods for quickly getting the polygons for an area or areas, for example to render them on a map. So if `libraries` is an instance of `BroadcastAreaLibraries` you can do: ```python libraries.get_polygons_for_areas_long_lat('england', 'wales') ``` This will give polygons for the Welsh mainland, the Isle of Wight, Anglesey, etc. The models load data from GeoJSON files, which is an open standard for serialising geographic data. I’ve added a few example files taken from http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
def test_loads_libraries():
assert [
(library.id, library.name, library.is_group) for library in sorted(broadcast_area_libraries)
Add broadcast area model, loading from GeoJSON This commit adds a new model class which can be used by any app to interact with a broadcast area. A broadcast area is one or more polygons representing geographical areas. It also adds some models that make browsing collections of these areas more straightforward. So the hierarchy looks like: > **BroadcastAreaLibraries* > Contains multiple libraries of broadcast area > > **BroadcastAreaLibrary** > > A collection of geographic areas, all of the same type, for example > > counties or electoral wards > > **BroadcastArea** > > Contains one or more shapes that make up an area, for example > > England > > > **BroadcastArea.polygons[n]** > > > A single shape, for example the Isle of Wight or Lindisfarne > > > > **BroadcastArea.polygons[n][o]** > > > > A single coordinate along a polygons The classes support iteration, so all the areas in a library can be looped over, for example if `countries` is an instance of `BroadcastAreaLibrary` you can do: ```python for country in countries: print(country.name) ``` The `BroadcastAreaLibraries` class also provides some useful methods for quickly getting the polygons for an area or areas, for example to render them on a map. So if `libraries` is an instance of `BroadcastAreaLibraries` you can do: ```python libraries.get_polygons_for_areas_long_lat('england', 'wales') ``` This will give polygons for the Welsh mainland, the Isle of Wight, Anglesey, etc. The models load data from GeoJSON files, which is an open standard for serialising geographic data. I’ve added a few example files taken from http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
] == [
(
'ctry19',
Add broadcast area model, loading from GeoJSON This commit adds a new model class which can be used by any app to interact with a broadcast area. A broadcast area is one or more polygons representing geographical areas. It also adds some models that make browsing collections of these areas more straightforward. So the hierarchy looks like: > **BroadcastAreaLibraries* > Contains multiple libraries of broadcast area > > **BroadcastAreaLibrary** > > A collection of geographic areas, all of the same type, for example > > counties or electoral wards > > **BroadcastArea** > > Contains one or more shapes that make up an area, for example > > England > > > **BroadcastArea.polygons[n]** > > > A single shape, for example the Isle of Wight or Lindisfarne > > > > **BroadcastArea.polygons[n][o]** > > > > A single coordinate along a polygons The classes support iteration, so all the areas in a library can be looped over, for example if `countries` is an instance of `BroadcastAreaLibrary` you can do: ```python for country in countries: print(country.name) ``` The `BroadcastAreaLibraries` class also provides some useful methods for quickly getting the polygons for an area or areas, for example to render them on a map. So if `libraries` is an instance of `BroadcastAreaLibraries` you can do: ```python libraries.get_polygons_for_areas_long_lat('england', 'wales') ``` This will give polygons for the Welsh mainland, the Isle of Wight, Anglesey, etc. The models load data from GeoJSON files, which is an open standard for serialising geographic data. I’ve added a few example files taken from http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
'Countries',
False,
Add broadcast area model, loading from GeoJSON This commit adds a new model class which can be used by any app to interact with a broadcast area. A broadcast area is one or more polygons representing geographical areas. It also adds some models that make browsing collections of these areas more straightforward. So the hierarchy looks like: > **BroadcastAreaLibraries* > Contains multiple libraries of broadcast area > > **BroadcastAreaLibrary** > > A collection of geographic areas, all of the same type, for example > > counties or electoral wards > > **BroadcastArea** > > Contains one or more shapes that make up an area, for example > > England > > > **BroadcastArea.polygons[n]** > > > A single shape, for example the Isle of Wight or Lindisfarne > > > > **BroadcastArea.polygons[n][o]** > > > > A single coordinate along a polygons The classes support iteration, so all the areas in a library can be looped over, for example if `countries` is an instance of `BroadcastAreaLibrary` you can do: ```python for country in countries: print(country.name) ``` The `BroadcastAreaLibraries` class also provides some useful methods for quickly getting the polygons for an area or areas, for example to render them on a map. So if `libraries` is an instance of `BroadcastAreaLibraries` you can do: ```python libraries.get_polygons_for_areas_long_lat('england', 'wales') ``` This will give polygons for the Welsh mainland, the Isle of Wight, Anglesey, etc. The models load data from GeoJSON files, which is an open standard for serialising geographic data. I’ve added a few example files taken from http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
),
(
'wd20-lad20-ctyua19',
'Local authorities',
True,
),
(
'test',
'Test areas',
False,
),
Add broadcast area model, loading from GeoJSON This commit adds a new model class which can be used by any app to interact with a broadcast area. A broadcast area is one or more polygons representing geographical areas. It also adds some models that make browsing collections of these areas more straightforward. So the hierarchy looks like: > **BroadcastAreaLibraries* > Contains multiple libraries of broadcast area > > **BroadcastAreaLibrary** > > A collection of geographic areas, all of the same type, for example > > counties or electoral wards > > **BroadcastArea** > > Contains one or more shapes that make up an area, for example > > England > > > **BroadcastArea.polygons[n]** > > > A single shape, for example the Isle of Wight or Lindisfarne > > > > **BroadcastArea.polygons[n][o]** > > > > A single coordinate along a polygons The classes support iteration, so all the areas in a library can be looped over, for example if `countries` is an instance of `BroadcastAreaLibrary` you can do: ```python for country in countries: print(country.name) ``` The `BroadcastAreaLibraries` class also provides some useful methods for quickly getting the polygons for an area or areas, for example to render them on a map. So if `libraries` is an instance of `BroadcastAreaLibraries` you can do: ```python libraries.get_polygons_for_areas_long_lat('england', 'wales') ``` This will give polygons for the Welsh mainland, the Isle of Wight, Anglesey, etc. The models load data from GeoJSON files, which is an open standard for serialising geographic data. I’ve added a few example files taken from http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
]
def test_loads_areas_from_library():
assert [
(area.id, area.name) for area in sorted(
broadcast_area_libraries.get('ctry19')
Add broadcast area model, loading from GeoJSON This commit adds a new model class which can be used by any app to interact with a broadcast area. A broadcast area is one or more polygons representing geographical areas. It also adds some models that make browsing collections of these areas more straightforward. So the hierarchy looks like: > **BroadcastAreaLibraries* > Contains multiple libraries of broadcast area > > **BroadcastAreaLibrary** > > A collection of geographic areas, all of the same type, for example > > counties or electoral wards > > **BroadcastArea** > > Contains one or more shapes that make up an area, for example > > England > > > **BroadcastArea.polygons[n]** > > > A single shape, for example the Isle of Wight or Lindisfarne > > > > **BroadcastArea.polygons[n][o]** > > > > A single coordinate along a polygons The classes support iteration, so all the areas in a library can be looped over, for example if `countries` is an instance of `BroadcastAreaLibrary` you can do: ```python for country in countries: print(country.name) ``` The `BroadcastAreaLibraries` class also provides some useful methods for quickly getting the polygons for an area or areas, for example to render them on a map. So if `libraries` is an instance of `BroadcastAreaLibraries` you can do: ```python libraries.get_polygons_for_areas_long_lat('england', 'wales') ``` This will give polygons for the Welsh mainland, the Isle of Wight, Anglesey, etc. The models load data from GeoJSON files, which is an open standard for serialising geographic data. I’ve added a few example files taken from http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
)
] == [
('ctry19-E92000001', 'England'),
('ctry19-N92000002', 'Northern Ireland'),
('ctry19-S92000003', 'Scotland'),
('ctry19-W92000004', 'Wales'),
Add broadcast area model, loading from GeoJSON This commit adds a new model class which can be used by any app to interact with a broadcast area. A broadcast area is one or more polygons representing geographical areas. It also adds some models that make browsing collections of these areas more straightforward. So the hierarchy looks like: > **BroadcastAreaLibraries* > Contains multiple libraries of broadcast area > > **BroadcastAreaLibrary** > > A collection of geographic areas, all of the same type, for example > > counties or electoral wards > > **BroadcastArea** > > Contains one or more shapes that make up an area, for example > > England > > > **BroadcastArea.polygons[n]** > > > A single shape, for example the Isle of Wight or Lindisfarne > > > > **BroadcastArea.polygons[n][o]** > > > > A single coordinate along a polygons The classes support iteration, so all the areas in a library can be looped over, for example if `countries` is an instance of `BroadcastAreaLibrary` you can do: ```python for country in countries: print(country.name) ``` The `BroadcastAreaLibraries` class also provides some useful methods for quickly getting the polygons for an area or areas, for example to render them on a map. So if `libraries` is an instance of `BroadcastAreaLibraries` you can do: ```python libraries.get_polygons_for_areas_long_lat('england', 'wales') ``` This will give polygons for the Welsh mainland, the Isle of Wight, Anglesey, etc. The models load data from GeoJSON files, which is an open standard for serialising geographic data. I’ve added a few example files taken from http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
]
def test_examples():
countries = broadcast_area_libraries.get('ctry19').get_examples()
assert countries == 'England, Northern Ireland, Scotland and Wales'
wards = broadcast_area_libraries.get('wd20-lad20-ctyua19').get_examples()
assert wards == 'Aberdeen City, Aberdeenshire, Adur and 391 more…'
Add broadcast area model, loading from GeoJSON This commit adds a new model class which can be used by any app to interact with a broadcast area. A broadcast area is one or more polygons representing geographical areas. It also adds some models that make browsing collections of these areas more straightforward. So the hierarchy looks like: > **BroadcastAreaLibraries* > Contains multiple libraries of broadcast area > > **BroadcastAreaLibrary** > > A collection of geographic areas, all of the same type, for example > > counties or electoral wards > > **BroadcastArea** > > Contains one or more shapes that make up an area, for example > > England > > > **BroadcastArea.polygons[n]** > > > A single shape, for example the Isle of Wight or Lindisfarne > > > > **BroadcastArea.polygons[n][o]** > > > > A single coordinate along a polygons The classes support iteration, so all the areas in a library can be looped over, for example if `countries` is an instance of `BroadcastAreaLibrary` you can do: ```python for country in countries: print(country.name) ``` The `BroadcastAreaLibraries` class also provides some useful methods for quickly getting the polygons for an area or areas, for example to render them on a map. So if `libraries` is an instance of `BroadcastAreaLibraries` you can do: ```python libraries.get_polygons_for_areas_long_lat('england', 'wales') ``` This will give polygons for the Welsh mainland, the Isle of Wight, Anglesey, etc. The models load data from GeoJSON files, which is an open standard for serialising geographic data. I’ve added a few example files taken from http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
@pytest.mark.parametrize('id', (
'ctry19-E92000001',
'ctry19-N92000002',
'ctry19-S92000003',
'ctry19-W92000004',
Add broadcast area model, loading from GeoJSON This commit adds a new model class which can be used by any app to interact with a broadcast area. A broadcast area is one or more polygons representing geographical areas. It also adds some models that make browsing collections of these areas more straightforward. So the hierarchy looks like: > **BroadcastAreaLibraries* > Contains multiple libraries of broadcast area > > **BroadcastAreaLibrary** > > A collection of geographic areas, all of the same type, for example > > counties or electoral wards > > **BroadcastArea** > > Contains one or more shapes that make up an area, for example > > England > > > **BroadcastArea.polygons[n]** > > > A single shape, for example the Isle of Wight or Lindisfarne > > > > **BroadcastArea.polygons[n][o]** > > > > A single coordinate along a polygons The classes support iteration, so all the areas in a library can be looped over, for example if `countries` is an instance of `BroadcastAreaLibrary` you can do: ```python for country in countries: print(country.name) ``` The `BroadcastAreaLibraries` class also provides some useful methods for quickly getting the polygons for an area or areas, for example to render them on a map. So if `libraries` is an instance of `BroadcastAreaLibraries` you can do: ```python libraries.get_polygons_for_areas_long_lat('england', 'wales') ``` This will give polygons for the Welsh mainland, the Isle of Wight, Anglesey, etc. The models load data from GeoJSON files, which is an open standard for serialising geographic data. I’ve added a few example files taken from http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
pytest.param('mercia', marks=pytest.mark.xfail(raises=KeyError)),
))
def test_loads_areas_from_libraries(id):
assert (
broadcast_area_libraries.get('ctry19').get(id)
Add broadcast area model, loading from GeoJSON This commit adds a new model class which can be used by any app to interact with a broadcast area. A broadcast area is one or more polygons representing geographical areas. It also adds some models that make browsing collections of these areas more straightforward. So the hierarchy looks like: > **BroadcastAreaLibraries* > Contains multiple libraries of broadcast area > > **BroadcastAreaLibrary** > > A collection of geographic areas, all of the same type, for example > > counties or electoral wards > > **BroadcastArea** > > Contains one or more shapes that make up an area, for example > > England > > > **BroadcastArea.polygons[n]** > > > A single shape, for example the Isle of Wight or Lindisfarne > > > > **BroadcastArea.polygons[n][o]** > > > > A single coordinate along a polygons The classes support iteration, so all the areas in a library can be looped over, for example if `countries` is an instance of `BroadcastAreaLibrary` you can do: ```python for country in countries: print(country.name) ``` The `BroadcastAreaLibraries` class also provides some useful methods for quickly getting the polygons for an area or areas, for example to render them on a map. So if `libraries` is an instance of `BroadcastAreaLibraries` you can do: ```python libraries.get_polygons_for_areas_long_lat('england', 'wales') ``` This will give polygons for the Welsh mainland, the Isle of Wight, Anglesey, etc. The models load data from GeoJSON files, which is an open standard for serialising geographic data. I’ve added a few example files taken from http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
) == (
broadcast_area_libraries.get_areas(id)[0]
)
def test_get_names_of_areas():
areas = broadcast_area_libraries.get_areas(
'ctry19-W92000004',
'lad20-W06000014',
'ctry19-E92000001',
Add broadcast area model, loading from GeoJSON This commit adds a new model class which can be used by any app to interact with a broadcast area. A broadcast area is one or more polygons representing geographical areas. It also adds some models that make browsing collections of these areas more straightforward. So the hierarchy looks like: > **BroadcastAreaLibraries* > Contains multiple libraries of broadcast area > > **BroadcastAreaLibrary** > > A collection of geographic areas, all of the same type, for example > > counties or electoral wards > > **BroadcastArea** > > Contains one or more shapes that make up an area, for example > > England > > > **BroadcastArea.polygons[n]** > > > A single shape, for example the Isle of Wight or Lindisfarne > > > > **BroadcastArea.polygons[n][o]** > > > > A single coordinate along a polygons The classes support iteration, so all the areas in a library can be looped over, for example if `countries` is an instance of `BroadcastAreaLibrary` you can do: ```python for country in countries: print(country.name) ``` The `BroadcastAreaLibraries` class also provides some useful methods for quickly getting the polygons for an area or areas, for example to render them on a map. So if `libraries` is an instance of `BroadcastAreaLibraries` you can do: ```python libraries.get_polygons_for_areas_long_lat('england', 'wales') ``` This will give polygons for the Welsh mainland, the Isle of Wight, Anglesey, etc. The models load data from GeoJSON files, which is an open standard for serialising geographic data. I’ve added a few example files taken from http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
)
assert [area.name for area in sorted(areas)] == [
'England', 'Vale of Glamorgan', 'Wales',
Add broadcast area model, loading from GeoJSON This commit adds a new model class which can be used by any app to interact with a broadcast area. A broadcast area is one or more polygons representing geographical areas. It also adds some models that make browsing collections of these areas more straightforward. So the hierarchy looks like: > **BroadcastAreaLibraries* > Contains multiple libraries of broadcast area > > **BroadcastAreaLibrary** > > A collection of geographic areas, all of the same type, for example > > counties or electoral wards > > **BroadcastArea** > > Contains one or more shapes that make up an area, for example > > England > > > **BroadcastArea.polygons[n]** > > > A single shape, for example the Isle of Wight or Lindisfarne > > > > **BroadcastArea.polygons[n][o]** > > > > A single coordinate along a polygons The classes support iteration, so all the areas in a library can be looped over, for example if `countries` is an instance of `BroadcastAreaLibrary` you can do: ```python for country in countries: print(country.name) ``` The `BroadcastAreaLibraries` class also provides some useful methods for quickly getting the polygons for an area or areas, for example to render them on a map. So if `libraries` is an instance of `BroadcastAreaLibraries` you can do: ```python libraries.get_polygons_for_areas_long_lat('england', 'wales') ``` This will give polygons for the Welsh mainland, the Isle of Wight, Anglesey, etc. The models load data from GeoJSON files, which is an open standard for serialising geographic data. I’ve added a few example files taken from http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
]
def test_get_areas_accepts_lists():
areas_from_list = broadcast_area_libraries.get_areas(
[
'ctry19-W92000004',
'ctry19-E92000001',
]
)
areas_from_args = broadcast_area_libraries.get_areas(
'ctry19-W92000004',
'ctry19-E92000001',
)
assert len(areas_from_args) == len(areas_from_list) == 2
assert areas_from_args == areas_from_list
Add broadcast area model, loading from GeoJSON This commit adds a new model class which can be used by any app to interact with a broadcast area. A broadcast area is one or more polygons representing geographical areas. It also adds some models that make browsing collections of these areas more straightforward. So the hierarchy looks like: > **BroadcastAreaLibraries* > Contains multiple libraries of broadcast area > > **BroadcastAreaLibrary** > > A collection of geographic areas, all of the same type, for example > > counties or electoral wards > > **BroadcastArea** > > Contains one or more shapes that make up an area, for example > > England > > > **BroadcastArea.polygons[n]** > > > A single shape, for example the Isle of Wight or Lindisfarne > > > > **BroadcastArea.polygons[n][o]** > > > > A single coordinate along a polygons The classes support iteration, so all the areas in a library can be looped over, for example if `countries` is an instance of `BroadcastAreaLibrary` you can do: ```python for country in countries: print(country.name) ``` The `BroadcastAreaLibraries` class also provides some useful methods for quickly getting the polygons for an area or areas, for example to render them on a map. So if `libraries` is an instance of `BroadcastAreaLibraries` you can do: ```python libraries.get_polygons_for_areas_long_lat('england', 'wales') ``` This will give polygons for the Welsh mainland, the Isle of Wight, Anglesey, etc. The models load data from GeoJSON files, which is an open standard for serialising geographic data. I’ve added a few example files taken from http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
def test_has_polygons():
england = broadcast_area_libraries.get_areas('ctry19-E92000001')[0]
scotland = broadcast_area_libraries.get_areas('ctry19-S92000003')[0]
Add broadcast area model, loading from GeoJSON This commit adds a new model class which can be used by any app to interact with a broadcast area. A broadcast area is one or more polygons representing geographical areas. It also adds some models that make browsing collections of these areas more straightforward. So the hierarchy looks like: > **BroadcastAreaLibraries* > Contains multiple libraries of broadcast area > > **BroadcastAreaLibrary** > > A collection of geographic areas, all of the same type, for example > > counties or electoral wards > > **BroadcastArea** > > Contains one or more shapes that make up an area, for example > > England > > > **BroadcastArea.polygons[n]** > > > A single shape, for example the Isle of Wight or Lindisfarne > > > > **BroadcastArea.polygons[n][o]** > > > > A single coordinate along a polygons The classes support iteration, so all the areas in a library can be looped over, for example if `countries` is an instance of `BroadcastAreaLibrary` you can do: ```python for country in countries: print(country.name) ``` The `BroadcastAreaLibraries` class also provides some useful methods for quickly getting the polygons for an area or areas, for example to render them on a map. So if `libraries` is an instance of `BroadcastAreaLibraries` you can do: ```python libraries.get_polygons_for_areas_long_lat('england', 'wales') ``` This will give polygons for the Welsh mainland, the Isle of Wight, Anglesey, etc. The models load data from GeoJSON files, which is an open standard for serialising geographic data. I’ve added a few example files taken from http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
assert len(england.polygons) == 35
assert len(scotland.polygons) == 195
Add broadcast area model, loading from GeoJSON This commit adds a new model class which can be used by any app to interact with a broadcast area. A broadcast area is one or more polygons representing geographical areas. It also adds some models that make browsing collections of these areas more straightforward. So the hierarchy looks like: > **BroadcastAreaLibraries* > Contains multiple libraries of broadcast area > > **BroadcastAreaLibrary** > > A collection of geographic areas, all of the same type, for example > > counties or electoral wards > > **BroadcastArea** > > Contains one or more shapes that make up an area, for example > > England > > > **BroadcastArea.polygons[n]** > > > A single shape, for example the Isle of Wight or Lindisfarne > > > > **BroadcastArea.polygons[n][o]** > > > > A single coordinate along a polygons The classes support iteration, so all the areas in a library can be looped over, for example if `countries` is an instance of `BroadcastAreaLibrary` you can do: ```python for country in countries: print(country.name) ``` The `BroadcastAreaLibraries` class also provides some useful methods for quickly getting the polygons for an area or areas, for example to render them on a map. So if `libraries` is an instance of `BroadcastAreaLibraries` you can do: ```python libraries.get_polygons_for_areas_long_lat('england', 'wales') ``` This will give polygons for the Welsh mainland, the Isle of Wight, Anglesey, etc. The models load data from GeoJSON files, which is an open standard for serialising geographic data. I’ve added a few example files taken from http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
assert england.polygons.as_coordinate_pairs_lat_long[0][0] == [
Add broadcast area model, loading from GeoJSON This commit adds a new model class which can be used by any app to interact with a broadcast area. A broadcast area is one or more polygons representing geographical areas. It also adds some models that make browsing collections of these areas more straightforward. So the hierarchy looks like: > **BroadcastAreaLibraries* > Contains multiple libraries of broadcast area > > **BroadcastAreaLibrary** > > A collection of geographic areas, all of the same type, for example > > counties or electoral wards > > **BroadcastArea** > > Contains one or more shapes that make up an area, for example > > England > > > **BroadcastArea.polygons[n]** > > > A single shape, for example the Isle of Wight or Lindisfarne > > > > **BroadcastArea.polygons[n][o]** > > > > A single coordinate along a polygons The classes support iteration, so all the areas in a library can be looped over, for example if `countries` is an instance of `BroadcastAreaLibrary` you can do: ```python for country in countries: print(country.name) ``` The `BroadcastAreaLibraries` class also provides some useful methods for quickly getting the polygons for an area or areas, for example to render them on a map. So if `libraries` is an instance of `BroadcastAreaLibraries` you can do: ```python libraries.get_polygons_for_areas_long_lat('england', 'wales') ``` This will give polygons for the Welsh mainland, the Isle of Wight, Anglesey, etc. The models load data from GeoJSON files, which is an open standard for serialising geographic data. I’ve added a few example files taken from http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
55.811085, -2.034358 # https://goo.gl/maps/wsf2LUWzYinwydMk8
]
def test_polygons_are_enclosed():
england = broadcast_area_libraries.get('ctry19').get('ctry19-E92000001')
Add broadcast area model, loading from GeoJSON This commit adds a new model class which can be used by any app to interact with a broadcast area. A broadcast area is one or more polygons representing geographical areas. It also adds some models that make browsing collections of these areas more straightforward. So the hierarchy looks like: > **BroadcastAreaLibraries* > Contains multiple libraries of broadcast area > > **BroadcastAreaLibrary** > > A collection of geographic areas, all of the same type, for example > > counties or electoral wards > > **BroadcastArea** > > Contains one or more shapes that make up an area, for example > > England > > > **BroadcastArea.polygons[n]** > > > A single shape, for example the Isle of Wight or Lindisfarne > > > > **BroadcastArea.polygons[n][o]** > > > > A single coordinate along a polygons The classes support iteration, so all the areas in a library can be looped over, for example if `countries` is an instance of `BroadcastAreaLibrary` you can do: ```python for country in countries: print(country.name) ``` The `BroadcastAreaLibraries` class also provides some useful methods for quickly getting the polygons for an area or areas, for example to render them on a map. So if `libraries` is an instance of `BroadcastAreaLibraries` you can do: ```python libraries.get_polygons_for_areas_long_lat('england', 'wales') ``` This will give polygons for the Welsh mainland, the Isle of Wight, Anglesey, etc. The models load data from GeoJSON files, which is an open standard for serialising geographic data. I’ve added a few example files taken from http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
first_polygon = england.polygons.as_coordinate_pairs_lat_long[0]
Add broadcast area model, loading from GeoJSON This commit adds a new model class which can be used by any app to interact with a broadcast area. A broadcast area is one or more polygons representing geographical areas. It also adds some models that make browsing collections of these areas more straightforward. So the hierarchy looks like: > **BroadcastAreaLibraries* > Contains multiple libraries of broadcast area > > **BroadcastAreaLibrary** > > A collection of geographic areas, all of the same type, for example > > counties or electoral wards > > **BroadcastArea** > > Contains one or more shapes that make up an area, for example > > England > > > **BroadcastArea.polygons[n]** > > > A single shape, for example the Isle of Wight or Lindisfarne > > > > **BroadcastArea.polygons[n][o]** > > > > A single coordinate along a polygons The classes support iteration, so all the areas in a library can be looped over, for example if `countries` is an instance of `BroadcastAreaLibrary` you can do: ```python for country in countries: print(country.name) ``` The `BroadcastAreaLibraries` class also provides some useful methods for quickly getting the polygons for an area or areas, for example to render them on a map. So if `libraries` is an instance of `BroadcastAreaLibraries` you can do: ```python libraries.get_polygons_for_areas_long_lat('england', 'wales') ``` This will give polygons for the Welsh mainland, the Isle of Wight, Anglesey, etc. The models load data from GeoJSON files, which is an open standard for serialising geographic data. I’ve added a few example files taken from http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
assert first_polygon[0] != first_polygon[1] != first_polygon[2]
assert first_polygon[0] == first_polygon[-1]
def test_lat_long_order():
england = broadcast_area_libraries.get_areas('ctry19-E92000001')[0]
lat_long = england.polygons.as_coordinate_pairs_lat_long
long_lat = england.polygons.as_coordinate_pairs_long_lat
Add broadcast area model, loading from GeoJSON This commit adds a new model class which can be used by any app to interact with a broadcast area. A broadcast area is one or more polygons representing geographical areas. It also adds some models that make browsing collections of these areas more straightforward. So the hierarchy looks like: > **BroadcastAreaLibraries* > Contains multiple libraries of broadcast area > > **BroadcastAreaLibrary** > > A collection of geographic areas, all of the same type, for example > > counties or electoral wards > > **BroadcastArea** > > Contains one or more shapes that make up an area, for example > > England > > > **BroadcastArea.polygons[n]** > > > A single shape, for example the Isle of Wight or Lindisfarne > > > > **BroadcastArea.polygons[n][o]** > > > > A single coordinate along a polygons The classes support iteration, so all the areas in a library can be looped over, for example if `countries` is an instance of `BroadcastAreaLibrary` you can do: ```python for country in countries: print(country.name) ``` The `BroadcastAreaLibraries` class also provides some useful methods for quickly getting the polygons for an area or areas, for example to render them on a map. So if `libraries` is an instance of `BroadcastAreaLibraries` you can do: ```python libraries.get_polygons_for_areas_long_lat('england', 'wales') ``` This will give polygons for the Welsh mainland, the Isle of Wight, Anglesey, etc. The models load data from GeoJSON files, which is an open standard for serialising geographic data. I’ve added a few example files taken from http://geoportal.statistics.gov.uk to show how it works.
2020-07-06 10:53:40 +01:00
assert len(lat_long[0]) == len(long_lat[0]) == 2082 # Coordinates in polygon
assert len(lat_long[0][0]) == len(long_lat[0][0]) == 2 # Axes in coordinates
assert lat_long[0][0] == list(reversed(long_lat[0][0]))
def test_includes_electoral_wards():
areas = broadcast_area_libraries.get_areas(['wd20-E05009289'])
assert len(areas) == 1
def test_electoral_wards_are_groupable_cardiff():
areas = broadcast_area_libraries.get_areas(['lad20-W06000015'])
assert len(areas) == 1
cardiff = areas[0]
assert len(cardiff.sub_areas) == 29
def test_electoral_wards_are_groupable_ealing():
areas = broadcast_area_libraries.get_areas(['lad20-E09000009'])
assert len(areas) == 1
ealing = areas[0]
assert len(ealing.sub_areas) == 23
def test_repository_has_all_libraries():
repo = BroadcastAreasRepository()
libraries = repo.get_libraries()
assert len(libraries) == 3
assert [
('Countries', 'country'),
('Test areas', 'test area'),
('Local authorities', 'local authority'),
] == [(name, name_singular) for _, name, name_singular, _is_group in libraries]
@pytest.mark.parametrize('library', (
broadcast_area_libraries
))
def test_every_area_has_count_of_phones(library):
for area in library:
if library.id == 'test':
assert area.count_of_phones == 0
else:
assert area.count_of_phones > 0
@pytest.mark.parametrize('area_id, area_name, expected_count', (
# Unitary authority
('ctyua19-E10000014', 'Hampshire', 853_594.48),
# District
('lad20-E07000087', 'Fareham', 81_970.06),
# Ward
('wd20-E05004516', 'Fareham East', 5_684.9),
# Unitary authority
('lad20-E09000012', 'Hackney', 222_578.0),
# Ward
('wd20-E05009373', 'Hackney Downs', 11_321.169999999998),
# Special case: ward with hard-coded population
('wd20-E05011090', 'Bryher', 76.44),
# Areas with missing data
('lad20-E07000008', 'Cambridge', 0),
('lad20-E07000084', 'Basingstoke and Deane', 0),
('lad20-E07000118', 'Chorley', 0),
('lad20-E07000178', 'Oxford', 0),
))
def test_count_of_phones_for_all_levels(area_id, area_name, expected_count):
area = broadcast_area_libraries.get_areas(area_id)[0]
assert area.name == area_name
assert area.count_of_phones == expected_count
def test_city_of_london_counts_are_not_derived_from_population():
city_of_london = broadcast_area_libraries.get_areas('lad20-E09000001')[0]
assert city_of_london.name == 'City of London'
assert len(city_of_london.sub_areas) == len(CITY_OF_LONDON.WARDS) == 25
for ward in city_of_london.sub_areas:
# The population of the whole City of London is 9,401, so an
# average of 300 per ward. What were asserting here is that the
# count of phones is much larger, because it isnt derived from
# the resident population.
assert ward.count_of_phones > 5_000
@pytest.mark.parametrize('population, expected_estimate', (
# Upper and lower bounds of each age range
(
[(0, 100)], 50,
),
(
[(16, 100)], 100
),
(
[(24, 100)], 100,
),
(
[(25, 100)], 97,
),
(
[(34, 100)], 97,
),
(
[(35, 100)], 91
),
(
[(44, 100)], 91,
),
(
[(45, 100)], 88
),
(
[(54, 100)], 88,
),
(
[(55, 100)], 73
),
(
[(64, 100)], 73,
),
(
[(65, 100)], 40
),
(
[(999, 100)], 40,
),
# Multiple different ages in a single popualtion
(
[(16, 100), (54, 100)], 188
),
(
[(1, 1000), (66, 100)], 540
),
))
def test_estimate_number_of_smartphones_for_population(
population, expected_estimate,
):
assert estimate_number_of_smartphones_for_population(
population
) == expected_estimate
Vary bleed amount based on population density There are basically two kinds of 4G masts: Frequency | Range | Bandwidth ----------|-------------|---------------------------------- 800MHz | Long (500m) | Low (can handle a bit of traffic) 1800Mhz | Short (5km) | High (can handle lots of traffic) The 1800Mhz masts are better in terms of how much traffic they can handle and how fast a connection they provide. But because they have quite short range, it’s only economical to install them in very built up areas†. In more rural areas the 800MHz masts are better because they cover a wider area, and have enough bandwidth for the lower population density. The net effect of this is that cell broadcasts in rural areas are likely to bleed further, because the masts they are being broadcast from are less precise. We can use population density as a proxy for how likely it is to be covered by 1800Mhz masts, and therefore how much bleed we should expect. So this commit varies the amount of bleed shown based on the population density. I came up with the formula based on 3 fixed points: - The most remote areas (for example the Scottish Highlands) should have the highest average bleed, estimated at 5km - An town, like Crewe, should have about the same bleed as we were estimating before (1.5km) – Pete D thinks this is about right based on his knowledge of the area around his office in Crewe - The most built up areas, like London boroughs, could have as little as 500m of bleed Based on these three figures I came up with the following formula, which roughly gives the right bleed distance (`b`) for each of their population densities (`d`): ``` b = 5900 - (log10(d) × 1_250) ``` Plotted on a curve it looks like this: This is based on averages – remember that the UI shows where is _likely_ to receive the alert, based on bleed, not where it’s _possible_ to receive the alert. Here’s what it looks like on the map: --- †There are some additional subtleties which make this not strictly true: - The 800Mhz masts are also used in built up areas to fill in the gaps between the areas covered by the 1800Mhz masts - Switching between masts is inefficient, so if you’re moving fast through a built up area (for example on a train) your phone will only use the 800MHz masts so that you have to handoff from one mast to another less often
2021-03-12 09:17:42 +00:00
@pytest.mark.parametrize('area, expected_phones_per_square_mile', (
(
# Islington (most dense in UK)
'lad20-E09000019', 21_348
),
(
# Cordwainer Ward (City of London)
# This is higher than Islington because we inflate the
# popualtion to account for daytime workers
'wd20-E05009300', 310_674
),
(
# Crewe East
'wd20-E05008621', 2_078),
(
# Eden (Cumbria, least dense in England)
'lad20-E07000030', 25.57
),
(
# Highland (least dense in UK)
'lad20-S12000017', 4.40
),
))
def test_phone_density(
area, expected_phones_per_square_mile,
):
assert close_enough(
broadcast_area_libraries.get_areas(area)[0].phone_density,
expected_phones_per_square_mile,
)
@pytest.mark.parametrize('area, expected_bleed_in_m, expected_bleed_in_degrees', (
(
# Islington (most dense in UK)
'lad20-E09000019', 500, 0.00449
Vary bleed amount based on population density There are basically two kinds of 4G masts: Frequency | Range | Bandwidth ----------|-------------|---------------------------------- 800MHz | Long (500m) | Low (can handle a bit of traffic) 1800Mhz | Short (5km) | High (can handle lots of traffic) The 1800Mhz masts are better in terms of how much traffic they can handle and how fast a connection they provide. But because they have quite short range, it’s only economical to install them in very built up areas†. In more rural areas the 800MHz masts are better because they cover a wider area, and have enough bandwidth for the lower population density. The net effect of this is that cell broadcasts in rural areas are likely to bleed further, because the masts they are being broadcast from are less precise. We can use population density as a proxy for how likely it is to be covered by 1800Mhz masts, and therefore how much bleed we should expect. So this commit varies the amount of bleed shown based on the population density. I came up with the formula based on 3 fixed points: - The most remote areas (for example the Scottish Highlands) should have the highest average bleed, estimated at 5km - An town, like Crewe, should have about the same bleed as we were estimating before (1.5km) – Pete D thinks this is about right based on his knowledge of the area around his office in Crewe - The most built up areas, like London boroughs, could have as little as 500m of bleed Based on these three figures I came up with the following formula, which roughly gives the right bleed distance (`b`) for each of their population densities (`d`): ``` b = 5900 - (log10(d) × 1_250) ``` Plotted on a curve it looks like this: This is based on averages – remember that the UI shows where is _likely_ to receive the alert, based on bleed, not where it’s _possible_ to receive the alert. Here’s what it looks like on the map: --- †There are some additional subtleties which make this not strictly true: - The 800Mhz masts are also used in built up areas to fill in the gaps between the areas covered by the 1800Mhz masts - Switching between masts is inefficient, so if you’re moving fast through a built up area (for example on a train) your phone will only use the 800MHz masts so that you have to handoff from one mast to another less often
2021-03-12 09:17:42 +00:00
),
(
# Cordwainer Ward (City of London)
# Special case because of inflated daytime population
'wd20-E05009300', 500, 0.00449
),
(
# Crewe East
'wd20-E05008621', 1_752, 0.01574
),
(
# Eden (Cumbria, least dense in England)
'lad20-E07000030', 4_140, 0.0372
),
(
# Highland (least dense in UK)
'lad20-S12000017', 5_000, 0.0449
Vary bleed amount based on population density There are basically two kinds of 4G masts: Frequency | Range | Bandwidth ----------|-------------|---------------------------------- 800MHz | Long (500m) | Low (can handle a bit of traffic) 1800Mhz | Short (5km) | High (can handle lots of traffic) The 1800Mhz masts are better in terms of how much traffic they can handle and how fast a connection they provide. But because they have quite short range, it’s only economical to install them in very built up areas†. In more rural areas the 800MHz masts are better because they cover a wider area, and have enough bandwidth for the lower population density. The net effect of this is that cell broadcasts in rural areas are likely to bleed further, because the masts they are being broadcast from are less precise. We can use population density as a proxy for how likely it is to be covered by 1800Mhz masts, and therefore how much bleed we should expect. So this commit varies the amount of bleed shown based on the population density. I came up with the formula based on 3 fixed points: - The most remote areas (for example the Scottish Highlands) should have the highest average bleed, estimated at 5km - An town, like Crewe, should have about the same bleed as we were estimating before (1.5km) – Pete D thinks this is about right based on his knowledge of the area around his office in Crewe - The most built up areas, like London boroughs, could have as little as 500m of bleed Based on these three figures I came up with the following formula, which roughly gives the right bleed distance (`b`) for each of their population densities (`d`): ``` b = 5900 - (log10(d) × 1_250) ``` Plotted on a curve it looks like this: This is based on averages – remember that the UI shows where is _likely_ to receive the alert, based on bleed, not where it’s _possible_ to receive the alert. Here’s what it looks like on the map: --- †There are some additional subtleties which make this not strictly true: - The 800Mhz masts are also used in built up areas to fill in the gaps between the areas covered by the 1800Mhz masts - Switching between masts is inefficient, so if you’re moving fast through a built up area (for example on a train) your phone will only use the 800MHz masts so that you have to handoff from one mast to another less often
2021-03-12 09:17:42 +00:00
),
(
# No population data available
'test-santa-claus-village-rovaniemi-a', 1_500, 0.01347
)
Vary bleed amount based on population density There are basically two kinds of 4G masts: Frequency | Range | Bandwidth ----------|-------------|---------------------------------- 800MHz | Long (500m) | Low (can handle a bit of traffic) 1800Mhz | Short (5km) | High (can handle lots of traffic) The 1800Mhz masts are better in terms of how much traffic they can handle and how fast a connection they provide. But because they have quite short range, it’s only economical to install them in very built up areas†. In more rural areas the 800MHz masts are better because they cover a wider area, and have enough bandwidth for the lower population density. The net effect of this is that cell broadcasts in rural areas are likely to bleed further, because the masts they are being broadcast from are less precise. We can use population density as a proxy for how likely it is to be covered by 1800Mhz masts, and therefore how much bleed we should expect. So this commit varies the amount of bleed shown based on the population density. I came up with the formula based on 3 fixed points: - The most remote areas (for example the Scottish Highlands) should have the highest average bleed, estimated at 5km - An town, like Crewe, should have about the same bleed as we were estimating before (1.5km) – Pete D thinks this is about right based on his knowledge of the area around his office in Crewe - The most built up areas, like London boroughs, could have as little as 500m of bleed Based on these three figures I came up with the following formula, which roughly gives the right bleed distance (`b`) for each of their population densities (`d`): ``` b = 5900 - (log10(d) × 1_250) ``` Plotted on a curve it looks like this: This is based on averages – remember that the UI shows where is _likely_ to receive the alert, based on bleed, not where it’s _possible_ to receive the alert. Here’s what it looks like on the map: --- †There are some additional subtleties which make this not strictly true: - The 800Mhz masts are also used in built up areas to fill in the gaps between the areas covered by the 1800Mhz masts - Switching between masts is inefficient, so if you’re moving fast through a built up area (for example on a train) your phone will only use the 800MHz masts so that you have to handoff from one mast to another less often
2021-03-12 09:17:42 +00:00
))
def test_estimated_bleed(
area, expected_bleed_in_m, expected_bleed_in_degrees,
):
assert close_enough(
broadcast_area_libraries.get_areas(area)[0].estimated_bleed_in_m,
expected_bleed_in_m,
)
assert close_enough(
broadcast_area_libraries.get_areas(area)[0].estimated_bleed_in_degrees,
expected_bleed_in_degrees,
)
Estimate number of phones in an arbitrary polygon We want to know how many phones are in a user-supplied polygon, so we can show the impact of a broadcast, in the same way that we do when users pick areas from our library. We already know how many phones are in each electoral ward. But there are challenges with an arbitrary polygon: - where it does overlap a ward, the overlap could be partial - it could overlap more than one ward - finding out which wards it overlaps by brute force (looping through all the wards and seeing which ones intersect with our polygon) would be way to slow to do in real time Instead we can use a data structure called an R-tree[1] to build an index which provides a much, much faster way of looking up which polygons overlap another. We can build this tree in advance and save it somewhere, which means there’s a lot of computation we don’t need to do in real time. The R-tree returns a set of objects (ward IDs) which we can go and look up in our library of electoral wards. These wards will be the ones that might have some overlap with our custom polygon. Once we have this small set of wards which might overlap our ward, we can look at the size of the area of overlap (relative to the size of the whole ward) and multiply that by the known count of phones in that ward to get an approximation of the count of phones in the overlap area. Summing these approximations give an estimate for the whole area of the custom polygon. 1. https://en.wikipedia.org/wiki/R-tree
2021-03-18 23:02:32 +00:00
@pytest.mark.parametrize('polygon, expected_possible_overlaps, expected_count_of_phones', (
(
BRISTOL,
[
'Ashley',
'Bedminster',
'Central',
'Clifton',
'Clifton Down',
'Cotham',
'Hotwells and Harbourside',
'Knowle',
'Lawrence Hill',
'Southville',
'Stoke Bishop',
'Windmill Hill',
],
73_496,
),
(
SKYE,
[
'Caol and Mallaig',
'Eilean á Chèo',
'Na Hearadh agus Ceann a Deas nan Loch',
'Wester Ross, Strathpeffer and Lochalsh',
],
3_517,
),
))
def test_count_of_phones_for_custom_area(
polygon,
expected_possible_overlaps,
expected_count_of_phones,
):
area = CustomBroadcastArea(
name='Example',
polygons=[polygon],
)
assert sorted(
overlap.name for overlap in area.overlapping_areas
) == expected_possible_overlaps
assert close_enough(area.count_of_phones, expected_count_of_phones)