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
This commit is contained in:
Chris Hill-Scott
2021-03-12 09:17:42 +00:00
parent fb1345494c
commit 738ac1d818
8 changed files with 130 additions and 9 deletions

View File

@@ -1,3 +1,5 @@
import math
from notifications_utils.formatters import formatted_list
from notifications_utils.polygons import Polygons
from notifications_utils.serialised_model import SerialisedModelCollection
@@ -49,6 +51,10 @@ class BroadcastArea(SortableMixin):
BroadcastAreasRepository().get_simple_polygons_for_area(self.id)
)
@cached_property
def simple_polygons_with_bleed(self):
return self.simple_polygons.bleed_by(self.estimated_bleed_in_degrees)
@cached_property
def sub_areas(self):
return [
@@ -68,6 +74,20 @@ class BroadcastArea(SortableMixin):
# https://www.pivotaltracker.com/story/show/174837293
return self._count_of_phones or 0
@property
def phone_density(self):
return self.count_of_phones / self.polygons.estimated_area
@property
def estimated_bleed_in_m(self):
if self.id.endswith(CITY_OF_LONDON.WARDS):
return 500
return 5_900 - (math.log(self.phone_density, 10) * 1_250)
@property
def estimated_bleed_in_degrees(self):
return self.estimated_bleed_in_m / Polygons.approx_metres_to_degree
@cached_property
def parents(self):
return list(filter(None, self._parents_iterator))
@@ -109,6 +129,13 @@ class CustomBroadcastArea:
simple_polygons = polygons
@cached_property
def simple_polygons_with_bleed(self):
# We dont yet have a way of working out the population density
# of a custom area, so for now we have to use an average number
# to estimate the amount of bleed
return self.simple_polygons.bleed_by(Polygons.approx_bleed_in_degrees)
class CustomBroadcastAreas(SerialisedModelCollection):
model = CustomBroadcastArea

View File

@@ -117,6 +117,17 @@ class BroadcastMessage(JSONModel):
def simple_polygons(self):
return self.get_simple_polygons(areas=self.areas)
@cached_property
def simple_polygons_with_bleed(self):
polygons = Polygons(
list(itertools.chain(*(
area.simple_polygons_with_bleed for area in self.areas
)))
)
# If weve added multiple areas then we need to re-simplify the
# combined shapes to keep the point count down
return polygons.smooth.simplify if len(self.areas) > 1 else polygons
@property
def reference(self):
if self.template_id:
@@ -163,7 +174,7 @@ class BroadcastMessage(JSONModel):
@property
def count_of_phones_likely(self):
area_estimate = self.simple_polygons.estimated_area
bleed_area_estimate = self.simple_polygons.bleed.estimated_area - area_estimate
bleed_area_estimate = self.simple_polygons_with_bleed.estimated_area - area_estimate
return round_to_significant_figures(
self.count_of_phones + (self.count_of_phones * bleed_area_estimate / area_estimate),
1

View File

@@ -11,7 +11,7 @@
</li>
<li class="area-list-key area-list-key--likely">
<span class="visually-hidden">
An extra area of {{ "{:,.1f}".format(broadcast_message.simple_polygons.bleed.estimated_area - broadcast_message.simple_polygons.estimated_area) }} square miles is
An extra area of {{ "{:,.1f}".format(broadcast_message.simple_polygons_with_bleed.estimated_area - broadcast_message.simple_polygons.estimated_area) }} square miles is
</span>
Likely to get
<span class="visually-hidden">

View File

@@ -2,7 +2,7 @@
<script>
var polygons = []
{% for polygon in broadcast_message.simple_polygons.bleed.as_coordinate_pairs_lat_long %}
{% for polygon in broadcast_message.simple_polygons_with_bleed.as_coordinate_pairs_lat_long %}
polygons.push(
L.polygon({{polygon}}, {
opacity: 1,

View File

@@ -24,7 +24,7 @@ Shapely==1.7.1
awscli-cwlogs>=1.4,<1.5
itsdangerous==1.1.0
git+https://github.com/alphagov/notifications-utils.git@43.8.3#egg=notifications-utils==43.8.3
git+https://github.com/alphagov/notifications-utils.git@44.0.0#egg=notifications-utils==44.0.0
git+https://github.com/alphagov/govuk-frontend-jinja.git@v0.5.8-alpha#egg=govuk-frontend-jinja==0.5.8-alpha
# gds-metrics requires prometheseus 0.2.0, override that requirement as later versions bring significant performance gains

View File

@@ -110,7 +110,7 @@ mistune==0.8.4
# via notifications-utils
notifications-python-client==6.0.2
# via -r requirements.in
git+https://github.com/alphagov/notifications-utils.git@43.8.3#egg=notifications-utils==43.8.3
git+https://github.com/alphagov/notifications-utils.git@44.0.0#egg=notifications-utils==44.0.0
# via -r requirements.in
openpyxl==3.0.6
# via pyexcel-xlsx

View File

@@ -1,3 +1,5 @@
from math import isclose
import pytest
from app.broadcast_areas import (
@@ -10,6 +12,10 @@ from app.broadcast_areas.populations import (
)
def close_enough(a, b):
return isclose(a, b, rel_tol=0.001) # Within 0.1% difference
def test_loads_libraries():
assert [
(library.id, library.name, library.is_group) for library in sorted(broadcast_area_libraries)
@@ -272,3 +278,71 @@ def test_estimate_number_of_smartphones_for_population(
assert estimate_number_of_smartphones_for_population(
population
) == expected_estimate
@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', 488, 0.00439
),
(
# 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_095, 0.0458
),
))
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,
)

View File

@@ -623,7 +623,7 @@ def test_broadcast_page(
'Scotland remove',
], [
'An area of 177,439.8 square miles Will get the alert',
'An extra area of 3,058.9 square miles is Likely to get the alert',
'An extra area of 6,392.3 square miles is Likely to get the alert',
'40,000,000 phones estimated',
]),
([
@@ -640,8 +640,17 @@ def test_broadcast_page(
'Penrith West remove',
], [
'An area of 6.3 square miles Will get the alert',
'An extra area of 14.4 square miles is Likely to get the alert',
'9,000 to 30,000 phones',
'An extra area of 22.6 square miles is Likely to get the alert',
'9,000 to 40,000 phones',
]),
([
'lad20-E09000019',
], [
'Islington remove',
], [
'An area of 9.7 square miles Will get the alert',
'An extra area of 4.6 square miles is Likely to get the alert',
'200,000 to 300,000 phones',
]),
))
def test_preview_broadcast_areas_page(
@@ -726,7 +735,7 @@ def test_preview_broadcast_areas_page_with_custom_polygons(
for item in page.select('ul li.area-list-key')
] == [
'An area of 722.3 square miles Will get the alert',
'An extra area of 1,402.5 square miles is Likely to get the alert',
'An extra area of 1,498.5 square miles is Likely to get the alert',
'Unknown number of phones',
]