Coverage

The Coverage represents the areas searched by the Team in a survey. Any Feature objects located within the Coverage might be discovered and collected; any Feature objects located outside the Coverage will definitely not be discovered and collected (at least in the current configuration of prospect). Like the Assemblage, the Coverage requires an Area object be passed as a parameter of the creation methods.

The shape and location of these areas are governed by the survey strategy adopted in the simulation. For example, the chosen spacing between transects (and the choice of transects themselves) can be represented in the Coverage building block.

Creating a Coverage

A single Coverage is created from a list of SurveyUnit objects. These SurveyUnit objects can be created manually, but because these units are typically regularly-shaped, regularly-spaced, and sharing the same min_time_per_unit parameter, there are a some convenience methods provided to create them in bulk and add them to the Coverage.

From a list of SurveyUnit objects

First, however, let us create some small square SurveyUnit objects manually and use them to create a Coverage.

from shapely.geometry import Point
import prospect

radius = 10
circ1 = Point(10, 10).buffer(radius)  # create circle
su1 = prospect.SurveyUnit(
        name="surveyunit1",
        coverage_name="demo_coverage",
        shape=circ1,
        surveyunit_type="radial",
        length=None,
        radius=radius,
        min_time_per_unit=prospect.utils.truncnorm(mean=20, sd=8, lower=0, upper=100),
)

circ2 = Point(50, 50).buffer(radius)  # create circle
su2 = prospect.SurveyUnit(
        name="surveyunit2",
        coverage_name="demo_coverage",
        shape=circ2,
        surveyunit_type="radial",
        length=None,
        radius=radius,
        min_time_per_unit=prospect.utils.truncnorm(mean=20, sd=8, lower=0, upper=100),
)

circ3 = Point(90, 90).buffer(radius)  # create circle
su3 = prospect.SurveyUnit(
        name="surveyunit3",
        coverage_name="demo_coverage",
        shape=circ3,
        surveyunit_type="radial",
        length=None,
        radius=radius,
        min_time_per_unit=prospect.utils.truncnorm(mean=20, sd=8, lower=0, upper=100),
)
type(circ1)
shapely.geometry.polygon.Polygon
type(su1)
prospect.surveyunit.SurveyUnit
demo_area = prospect.Area.from_area_value(
    name='demo_area', 
    value=10000
)
coverage_from_list = prospect.Coverage(name="demo_coverage", surveyunit_list=[su1, su2, su3], orientation=None, spacing=None)
type(coverage_from_list)
prospect.coverage.Coverage
coverage_from_list.__dict__
{'name': 'demo_coverage',
 'surveyunit_list': [<prospect.surveyunit.SurveyUnit at 0x7f89ec8fc070>,
  <prospect.surveyunit.SurveyUnit at 0x7f89f480dee0>,
  <prospect.surveyunit.SurveyUnit at 0x7f89b553d640>],
 'orientation': None,
 'spacing': None,
 'sweep_width': None,
 'radius': None,
 'df':   surveyunit_name  coverage_name  \
 0     surveyunit1  demo_coverage   
 1     surveyunit2  demo_coverage   
 2     surveyunit3  demo_coverage   
 
                                                shape surveyunit_type  \
 0  POLYGON ((20.000 10.000, 19.952 9.020, 19.808 ...          radial   
 1  POLYGON ((60.000 50.000, 59.952 49.020, 59.808...          radial   
 2  POLYGON ((100.000 90.000, 99.952 89.020, 99.80...          radial   
 
    surveyunit_area length  radius  \
 0       313.654849   None      10   
 1       313.654849   None      10   
 2       313.654849   None      10   
 
                                    min_time_per_unit  
 0  <scipy.stats._distn_infrastructure.rv_frozen o...  
 1  <scipy.stats._distn_infrastructure.rv_frozen o...  
 2  <scipy.stats._distn_infrastructure.rv_frozen o...  }
coverage_from_list.df
surveyunit_name coverage_name shape surveyunit_type surveyunit_area length radius min_time_per_unit
0 surveyunit1 demo_coverage POLYGON ((20.000 10.000, 19.952 9.020, 19.808 ... radial 313.654849 None 10 <scipy.stats._distn_infrastructure.rv_frozen o...
1 surveyunit2 demo_coverage POLYGON ((60.000 50.000, 59.952 49.020, 59.808... radial 313.654849 None 10 <scipy.stats._distn_infrastructure.rv_frozen o...
2 surveyunit3 demo_coverage POLYGON ((100.000 90.000, 99.952 89.020, 99.80... radial 313.654849 None 10 <scipy.stats._distn_infrastructure.rv_frozen o...
type(coverage_from_list.df)
geopandas.geodataframe.GeoDataFrame
coverage_from_list.df.plot(ax=demo_area.df.plot(), color="orange");
../_images/Coverage_15_0.png

From a shapefile

The from_shapefile() method is useful for reading in existing surveys as Coverage objects. These could be the locations of survey units from a completed field survey or maybe survey units that are not shaped according to one of the built-in methods (transects and radial units).

area_from_shp = prospect.Area.from_shapefile(
    name="area_shp", 
    path="./data/demo_area.shp"
)

coverage_from_shp = prospect.Coverage.from_shapefile(
    "./data/demo_coverage.shp",
    name="demo_coverage_from_shp",
    area=area_from_shp,
    surveyunit_type="polygon",
    spacing=None,
    orient_axis=None,
    min_time_per_unit=20
)
coverage_from_shp.df
surveyunit_name coverage_name shape surveyunit_type surveyunit_area length radius min_time_per_unit
0 demo_coverage_from_shp_Index(['index', 'id', '... demo_coverage_from_shp POLYGON ((533601.766 4388853.524, 533595.418 4... polygon 1295.255792 None None 20
1 demo_coverage_from_shp_Index(['index', 'id', '... demo_coverage_from_shp POLYGON ((533675.077 4388864.631, 533708.717 4... polygon 905.067539 None None 20
2 demo_coverage_from_shp_Index(['index', 'id', '... demo_coverage_from_shp POLYGON ((533650.957 4388775.135, 533694.119 4... polygon 935.686338 None None 20
3 demo_coverage_from_shp_Index(['index', 'id', '... demo_coverage_from_shp POLYGON ((533768.382 4388875.422, 533769.016 4... polygon 641.383273 None None 20
coverage_from_shp.df.plot(ax=area_from_shp.df.plot(), color="orange");
../_images/Coverage_20_0.png

From a GeoDataFrame

The from_GeoDataFrame() method is intended to be used much like the from_shapefile() method: when you have the spatial properties of the survey units already defined (in this case in a GeoDataFrame), even if they are irregular, you can quickly create a Coverage object from them.

A GeoDataFrame can also be useful when your spatial data is stored in some format other than a shapefile. You can use any package (e.g., shapely) to read from the native format to something useable by geopandas (e.g., WKT format). geopandas can then read this to a GeoDataFrame for use in prospect.

For the sake of example, let’s load the shapefile we used above, convert it to well-known text (WKT) format, then load into a GeoDataFrame.

import fiona
from shapely.geometry import shape, Polygon

polys = fiona.open("./data/demo_coverage.shp")
collection_of_polys = [shape(item['geometry']) for item in polys]
units = [Polygon(poly.exterior.coords).wkt for poly in collection_of_polys]

Let’s look at an example of the units.

units[0]
'POLYGON ((533601.7657606293 4388853.523606064, 533595.4184860958 4388846.541604077, 533587.8017566557 4388877.643249291, 533624.6159489499 4388880.816886557, 533637.310498017 4388855.427788423, 533630.3284960302 4388846.541604077, 533612.5561273363 4388844.637421716, 533612.5561273363 4388844.637421716, 533612.5561273363 4388844.637421716, 533601.7657606293 4388853.523606064))'
type(units[0])
str

Next, we can put this string representation in a pandas DataFrame.

import pandas as pd

wkt_df = pd.DataFrame(
    {
        "names": ["unit1", "unit2", "unit3", "unit4"],
        "coordinates":units
    }
)
wkt_df
names coordinates
0 unit1 POLYGON ((533601.7657606293 4388853.523606064,...
1 unit2 POLYGON ((533675.0767814913 4388864.631336497,...
2 unit3 POLYGON ((533650.957138264 4388775.134765575, ...
3 unit4 POLYGON ((533768.3817171339 4388875.421703205,...

Confirm that the entries in the new column "coordinates" are strings.

type(wkt_df['coordinates'][0])
str
from shapely import wkt
import geopandas as gpd
wkt_df["geo_coordinates"] = wkt_df["coordinates"].apply(wkt.loads)
wkt_geodf = gpd.GeoDataFrame(wkt_df, geometry="geo_coordinates")
wkt_geodf
names coordinates geo_coordinates
0 unit1 POLYGON ((533601.7657606293 4388853.523606064,... POLYGON ((533601.766 4388853.524, 533595.418 4...
1 unit2 POLYGON ((533675.0767814913 4388864.631336497,... POLYGON ((533675.077 4388864.631, 533708.717 4...
2 unit3 POLYGON ((533650.957138264 4388775.134765575, ... POLYGON ((533650.957 4388775.135, 533694.119 4...
3 unit4 POLYGON ((533768.3817171339 4388875.421703205,... POLYGON ((533768.382 4388875.422, 533769.016 4...

"coordinates" and "geo_coordinates" look similar, but the latter now contains shapely Polygon objects instead of strings and the whole thing is a GeoDataFrame suitable for prospect.

type(wkt_geodf['geo_coordinates'][0])
shapely.geometry.polygon.Polygon
type(wkt_geodf)
geopandas.geodataframe.GeoDataFrame

Let’s complete the cycle by turning this into a prospect Coverage object.

area_from_shp = prospect.Area.from_shapefile(
    name="area_shp", 
    path="./data/demo_area.shp"
)

coverage_from_gdf = prospect.Coverage.from_GeoDataFrame(
    gdf=wkt_geodf,
    name="demo_coverage_from_gdf",
    surveyunit_type="polygon",
    min_time_per_unit=20
)
coverage_from_gdf.df.plot(ax=area_from_shp.df.plot(), color="orange");
../_images/Coverage_39_0.png

Bulk-create transects

Transects are probably the most common type of survey strategy in use in the field. As such, prospect includes a from_transects() method to easily create a transect-based survey approach.

This method for constructing Coverage objects comes with a few special parameters that help prospect create the appropriate transects for the given Area.

  • spacing: Distance between transects (the default is 10.0)

  • sweep_width: Buffer distance around transects (the default is 2.0)

  • orientation: Angle of the predominant axis of the transects (the default is 0.0)

  • optimize_orient_by ('area_coverage' or 'area_orient'): Metric to optimize in determining the orientation of transects. 'area_coverage' chooses the orientation that maximizes the area covered by the transects. 'area_orient' chooses the orientation that best parallels the orient_axis of the area. The default is None, in which case the orientation parameter is used directly.

  • orient_increment: Step size (in degrees) to use when testing different orientations. (the default is 5.0)

  • orient_axis ('long' or 'short'): Axis of the area along which to orient the survey units (the default is 'long', which creates rows parallel to the longest axis of the area’s minimum rotated rectangle)

  • min_time_per_unit: Minimum amount of time required to complete one “unit” of survey, given no surveyor speed penalty and no time penalty for recording features. The default is 0.0. Because transects can differ in length, transect coverages should specify this term as time per one unit of distance (e.g., seconds per meter).

The from_transects() method has a series of possible customizations. We will explore them one-by-one using the same Area object we have been using.

area_from_shp = prospect.Area.from_shapefile(
    name="area_shp", 
    path="./data/demo_area.shp"
)

Let’s start, however, with a simple version of a transect Coverage.

simple_transects = prospect.Coverage.from_transects(
    name="demo_simple_transects", 
    area=area_from_shp, 
    spacing=10, 
    sweep_width=2, 
    min_time_per_unit=20
)
simple_transects.df.plot(ax=area_from_shp.df.plot(), color="orange");
../_images/Coverage_46_0.png

We can vary the spacing and the width of the transects.

simple_transects_spacing = prospect.Coverage.from_transects(
    name="demo_simple_transects_spacing", 
    area=area_from_shp, 
    spacing=20, 
    sweep_width=1, 
    min_time_per_unit=20
)
simple_transects_spacing.df.plot(ax=area_from_shp.df.plot(), color="orange");
../_images/Coverage_49_0.png

Notice that by default, the transects are oriented directly north-south. If you know what orientation you would like to use, you can specify that directly with the orientation parameter. Here we rotate them 90 degrees to an east-west orientation.

transects_orientation = prospect.Coverage.from_transects(
    name="demo_transects_orientation", 
    area=area_from_shp,
    spacing=20, 
    sweep_width=1, 
    orientation=90,
    min_time_per_unit=20
)
transects_orientation.df.plot(ax=area_from_shp.df.plot(), color="orange");
../_images/Coverage_52_0.png

There are also ways to optimize the orientation of the transects. First, you can choose to find the orientation that maximizes the areal extent of the survey units ('area_coverage').

The orient_increment parameter determines the orientations that prospect will iterate through. This is useful to specify if you are planning to put a team in the field. It may be impractical to orient a team to fractions of a degree or even a single degree. You might prefer to stick to increments of 5 or 10 degrees. Below, we check every 5 degrees, which is the default.

transects_orientation_areal_cov = prospect.Coverage.from_transects(
    name="demo_transects_orientation_areal_cov", 
    area=area_from_shp,
    spacing=20, 
    sweep_width=1, 
    optimize_orient_by="area_coverage",
    orient_increment=5,
    min_time_per_unit=20
)
transects_orientation_areal_cov.df.plot(ax=area_from_shp.df.plot(), color="orange");
../_images/Coverage_55_0.png

You can then examine the orientation that was chosen via the orientation attribute

transects_orientation_areal_cov.orientation
130

The other optimization option is 'area_orient', which chooses the orientation that best parallels the orient_axis of the area. You can choose to follow the 'long' or 'short' axis.

transects_orientation_areal_orient = prospect.Coverage.from_transects(
    name="demo_transects_orientation_areal_orient", 
    area=area_from_shp,
    spacing=20, 
    sweep_width=1, 
    optimize_orient_by="area_orient",
    orient_axis="long",
    min_time_per_unit=20
)
transects_orientation_areal_orient.df.plot(ax=area_from_shp.df.plot(), color="orange");
../_images/Coverage_60_0.png
transects_orientation_areal_orient.orientation
-42.04645468087444

In this example, optimizing by the long axis yields an orientation of -42 degrees (e.g., 318 degrees).

transects_orientation_areal_orient_short = prospect.Coverage.from_transects(
    name="demo_transects_orientation_areal_orient_short", 
    area=area_from_shp,
    spacing=20, 
    sweep_width=1, 
    optimize_orient_by="area_orient",
    orient_axis="short",
    min_time_per_unit=20
)
transects_orientation_areal_orient_short.df.plot(ax=area_from_shp.df.plot(), color="orange");
../_images/Coverage_64_0.png
transects_orientation_areal_orient_short.orientation
47.95354531912556

Bulk-create radial units

The from_radials() method creates a regularly-spaced grid of circular survey plots. It has all of the same special parameters as the from_transects() method with the exception of the following:

  • Instead of sweep_width, radials have a radius (naturally) that controls how large the survey plots will be.

  • The min_time_per_unit can be specified as the minimum time it takes to survey one circular plot.

Let’s start, however, with a simple version of a radials Coverage. The default radius is 1.78, which gives you radials with roughly 10 square units of area.

simple_radials = prospect.Coverage.from_radials(
    name="demo_simple_radials", 
    area=area_from_shp, 
    spacing=10,
    radius=1.78,
    min_time_per_unit=20
)
simple_radials.df.plot(ax=area_from_shp.df.plot(), color="orange");
../_images/Coverage_70_0.png

Notice that, like the transects, by default, the transects are oriented directly north-south. If we were to rotate them 90 degrees as we did for the transects, we would get much the same result.

radials_orientation = prospect.Coverage.from_radials(
    name="demo_radials_orientation", 
    area=area_from_shp,
    spacing=10, 
    radius=1.78, 
    orientation=90,
    min_time_per_unit=20
)
radials_orientation.df.plot(ax=area_from_shp.df.plot(), color="orange");
../_images/Coverage_73_0.png
radials_orientation.orientation
90

The same orientation optimization parameters are available for radial survey units as were available for transects.

First, area_coverage:

radials_orientation_areal_cov = prospect.Coverage.from_radials(
    name="demo_radials_orientation_areal_cov", 
    area=area_from_shp,
    spacing=10, 
    radius=1.78, 
    optimize_orient_by="area_coverage",
    orient_increment=5,
    min_time_per_unit=20
)
radials_orientation_areal_cov.df.plot(ax=area_from_shp.df.plot(), color="orange");
../_images/Coverage_77_0.png

You can then examine the orientation that was chosen via the orientation attribute

radials_orientation_areal_cov.orientation
25

Likewise, area_orient:

radials_orientation_areal_orient = prospect.Coverage.from_radials(
    name="demo_radials_orientation_areal_orient", 
    area=area_from_shp,
    spacing=10, 
    radius=1.78, 
    optimize_orient_by="area_orient",
    orient_axis="long",
    min_time_per_unit=20
)
radials_orientation_areal_orient.df.plot(ax=area_from_shp.df.plot(), color="orange");
../_images/Coverage_82_0.png
radials_orientation_areal_orient.orientation
-42.04645468087444

Look!

This is the exact same value as we calculated for the transects!

Now for the short axis:

radials_orientation_areal_orient_short = prospect.Coverage.from_radials(
    name="demo_radials_orientation_areal_orient_short", 
    area=area_from_shp,
    spacing=10, 
    radius=1.78, 
    optimize_orient_by="area_orient",
    orient_axis="short",
    min_time_per_unit=20
)
radials_orientation_areal_orient_short.df.plot(ax=area_from_shp.df.plot(), color="orange");
../_images/Coverage_86_0.png
radials_orientation_areal_orient_short.orientation
47.95354531912556

Again, this value matches the one for the transects.

min_time_per_unit parameter

This parameter is used to model the base level of time it takes to survey one unit. For variable-length units like transects, this parameter needs to be an amount of time per unit of distance (e.g., 5 seconds per meter). For fixed-size units like radial units or square units, this can be a single base time.

This parameter should be specified under the following assumptions:

  • no surveyor speed penalty (from the Surveyor)

  • no time penalty for recording (from the Assemblage)

In other words, it represents only the search time for an expert surveyor who doesn’t stop to record any artifacts or features.

With enough prior experience, min_time_per_unit can be modeled as a single value constant. If min_time_per_unit is being modeled as a distribution, it makes most sense to have it bounded at zero.

Tip

The truncated normal distribution is a good choice for this.

import seaborn as sns
dist_trunc = prospect.utils.truncnorm(mean=30, sd=30, lower=0, upper=200)

hist_trunc = sns.distplot(dist_trunc.rvs(100000), kde=False)  # draw 100k random values and plot
hist_trunc.set_xlim(-100,150);
/usr/share/miniconda/envs/prospect-docs/lib/python3.8/site-packages/seaborn/distributions.py:2619: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms).
  warnings.warn(msg, FutureWarning)
../_images/Coverage_92_1.png

Note

While the truncated normal distribution is a good choice generally, prospect allows you to use whatever scipy distribution you think fits your case best.