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");
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");
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");
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 theorient_axisof the area. The default isNone, in which case theorientationparameter 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");
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");
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");
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");
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");
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");
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 aradius(naturally) that controls how large the survey plots will be.The
min_time_per_unitcan 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");
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");
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");
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");
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");
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)
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.