Cumulative occupancy and corridor-scale discovery of perennial pepperweed (Lepidium latifolium) along the Lower Owens River Project
Technical data report · Inyo County Water Department · LORP rapid assessment survey, 2018–2025
Author
Affiliation
Inyo County Water Department
Inyo County Water Department, Independence, California, United States
Published
July 21, 2026
Modified
July 21, 2026
Abstract
Perennial pepperweed (Lepidium latifolium) is a high-impact riparian invader whose corridor-scale rates of spread and discovery along the Lower Owens River Project (LORP) have remained largely unquantified. This technical report summarizes ArcGIS Online occurrence records from rapid assessment survey (RAS) monitoring (2018–2025), maps spatial patterns by reach and river mile, and estimates cumulative known occupancy at 0.1-mile and whole-mile resolutions. Smooth (linear, logistic, recent-trajectory) and step-pulse (fast 6-year vs low-spread 12-year) forecasts describe the pace of known-occupancy infill under continued survey; a 0.1-mi cumulative heatmap shows corridor discovery geography. Interpretation emphasizes waterway-mediated pulse recruitment after flooding, with discovery lagged when flood years limit access. The report also identifies unoccupied corridor segments for the FY 2026–27 RAS workplan. Herbicide efficacy is outside the scope of this analysis.
Lepidium latifolium L. (perennial pepperweed, tall whitetop) is a Eurasian mustard that invades wetlands, floodplains, irrigation infrastructure, and riparian corridors across the western United States (Young et al. 1995, 1998, California Invasive Plant Council 2024). Dense stands displace desirable vegetation, alter soil chemistry, and are difficult to eradicate once established (Young et al. 1998, CABI 2024). Propagules move as seed and root fragments; stands also expand vegetatively from creeping roots (Leininger and Foin 2009, Renz and DiTomaso 2012).
Along river corridors, two processes generate new occupied patches at different scales. Local kernels—vegetative edge advance (~0.85 m yr⁻¹ under undisturbed conditions) and seed fall near mother plants—densify existing infestations (Renz and DiTomaso 2012). Landscape-level jumps require long-distance transport by water (especially floods), animals, or machinery (Young et al. 1995, Leininger and Foin 2009). Widespread flooding can therefore produce pulse recruitment: a step increase in occupied corridor, followed by quieter intervals until the next major hydrologic pulse. Flood years may also limit survey access, so known occupancy often steps upward in subsequent accessible seasons.
Rewatering and expanded wetland habitat under the Lower Owens River Project (LORP) were recognized early as factors that could increase pepperweed susceptibility by creating wetted surfaces and facilitating downstream transport (Los Angeles Department of Water and Power 2004). Inyo County Water Department and partners conduct rapid assessment survey (RAS) monitoring of occurrences along the corridor. Until the multi-year series summarized here, corridor-scale rates of pepperweed discovery and known occupancy along the LORP remained largely unquantified in management reporting (Los Angeles Department of Water and Power 2024, 2026).
This technical report (1) documents spatial patterns of RAS detections for 2018–2025, (2) estimates cumulative known occupancy at 0.1-mile and whole-mile resolutions, (3) presents operational forecasts of occupancy infill, and (4) identifies unoccupied corridor miles for FY 2026–27 RAS priorities (approved workplan PDF). The estimand is the rate and geography of known-occupancy infill, not herbicide efficacy.
“Perennial pepperweed (Lepidium latifolium), an introduced plant from southeastern Europe and Asia, is invasive throughout the western United States… both the California Department of Food and Agriculture (CDFA) and California Invasive Plant Council (Cal-IPC) list it as a noxious weed of great ecological concern.” — UC IPM
Point observations of perennial pepperweed were obtained from the Inyo County ArcGIS Online Noxious Weeds feature service and restricted to the LORP study extent (south of the northernmost 2025 detection). Display year uses the observation date when available and the feature creation date otherwise. Field abundance is recorded in classes 5, 15, 25, 100, 200, where 200 denotes ≥200 plants (dense stands may contain thousands of individuals).
2018 as a cumulative baseline. Records labeled 2018 are not a single-season snapshot: they accumulate detections from surveys in 2018 and earlier years that were consolidated into the modern feature service. Later calendar years (2019–2025) more closely reflect that season’s survey effort and new discoveries. Cumulative occupancy from 2018 onward therefore treats 2018 as the start-of-series known stock, not as an annual recruitment pulse.
Bank side. Pre-2018 field forms sometimes recorded east/west bank explicitly. That attribute is largely redundant for corridor analysis: bank can be derived from each point’s XY location relative to the river channel / mile centerline (as in the 2026 east/west gap lines). This report does not rely on a stored bank field.
Corridor occupancy
For occupancy and monitoring priorities, sites more than 250 m from the river-mile centerline are excluded so off-channel ditch or field points do not falsely occupy a corridor mile. Cumulative known occupancy is the share of (a) 0.1-mile segments and (b) whole river miles (RM 0–60) that have ever had a corridor detection by a given year. Once detected, a unit remains in the cumulative total—isolating discovery/infill from year-to-year variation in survey intensity and from resurvey avoidance of known stands.
Forecasts
Linear, asymptotic, logistic, and recent-trajectory models are fit to the cumulative series as operational descriptions of known-occupancy infill (not stem-density invasion models). Step-pulse scenarios encode flood-then-plateau structure: a fast regime (~6-year discovery pulses after the recent 2017/2023 flood cluster) and a low-spread regime (12-year pulses with quieter between-pulse drift). Calendar milestones are rounded to whole years. A 0.1-mi cumulative heatmap along the corridor visualizes discovery geography without false gaps from resurvey avoidance.
Limitations
Records represent documented RAS observations only; absence of a point does not prove absence of plants.
The 2018 year class pools pre-2018 historical detections with 2018 surveys; year-to-year increments after 2018 are the appropriate scale for annual discovery rates.
Survey timing, access (including floods), and observer effort affect annual maps more than cumulative discovery.
Abundance classes are field estimates, not stem counts.
Forecasts describe known occupancy under continued survey and do not attribute residuals to herbicide treatment intensity.
Dataset overview
The filtered dataset includes 1647 observations across 8 years (2018–2025), with peak survey activity in 2025 (426 records).
Metric
Value
Total observations
1647
Peak survey year
2025 (426 records)
Recent period (2022–2025)
852
Earlier period (2018–2021)
795
Records with usable year
100%
Spatial occurrence
Interactive map of pepperweed detections (2018–2025). Toggle years in the layer control. Creation dates substitute when observation dates are missing. The 2018 & earlier layer is the cumulative baseline (pre-2018 plus 2018), not a single field season. Reference layers: purple river channels; 2026 East gap lines (solid sky blue) and 2026 West gap lines (dashed orange) mark bank stretches ≥400 m without corridor detections—east/west assigned from point XY relative to the channel, not from a legacy bank attribute.
Accessible map summary: Points are colored by survey year (2018–2025). Parallel gap lines mark east (solid blue) and west (dashed orange) banks without detections for ≥400 m — the 2026 RAS walk targets. Equivalent data are in the downloadable gap-line files and the monitoring recommendation tables. Screen-reader users can rely on those tables rather than the interactive map canvas.
Complete dataset with all 1647 pepperweed observations
Study Area Boundaries
North: 36.9747°N
South: 36.5478°N
East: -117.981°W
West: -118.2343°W
Center: 36.7612°N, -118.1077°W
Re-reading with feature count reset from 1 to 0
Abundance structure
Field-estimated plants per site in the most recent survey year, and recent low-abundance sites that may warrant early intervention.
Field abundance is recorded in discrete estimate classes (5, 15, 25, 100, 200). A value of 200 means ≥200 plants — a lower bound for dense stands that may contain hundreds to thousands of individuals (not an exact count of 200).
The following chart shows the distribution of these abundance classes for pepperweed sites documented in 2025:
2025 site distribution
Total Sites: 426
Sites below 200 class: 325 ( 76.3 %)
Sites in 200+ class (≥200 plants; may be thousands): 99 ( 23.2 %)
Detailed abundance distribution
5 plants (estimate class): 57 sites
15 plants (estimate class): 59 sites
25 plants (estimate class): 76 sites
100 plants (estimate class): 133 sites
200+ plants (≥200; may be thousands): 99 sites
Abundance coding
Field classes used: 5, 15, 25, 100, 200
200 = 200 or more — dense patches are not counted stem-by-stem and can represent thousands of individuals
The interactive map above provides the primary visualization of pepperweed distribution patterns.
Recent Sites
Reach-scale patterns
LORP reach distribution summary
Total Reaches Analyzed: 6
Years Covered: 2018 - 2025
Reaches with Data: 6
Reach activity summary
LORP Reach 2: 999 sites
LORP Reach 3: 423 sites
LORP Reach 1: 163 sites
LORP Reach 4: 47 sites
LORP Reach 5: 12 sites
LORP Reach 6: 3 sites
Corridor occupancy and discovery forecasts
Overall occupancy analysis
Resolution: 1/10 mile segments (600 total segments across 60 miles)
Overall occupancy by year
Number of 1/10 mile segments occupied by pepperweed
Year
Segments Occupied
Total Segments
Occupancy %
2018
15
600
2.5
2019
74
600
12.3
2020
86
600
14.3
2021
21
600
3.5
2022
37
600
6.2
2023
1
600
0.2
2024
87
600
14.5
2025
145
600
24.2
Reach-specific occupancy
Percentage of each reach occupied by pepperweed (by year)
LORP Reach
2018
2019
2020
2021
2022
2023
2024
2025
LORP Reach 1
23.7
52.6
39.5
2.6
NA
NA
10.5
55.3
LORP Reach 2
3.1
17.6
23.9
5.0
15.7
NA
21.4
40.3
LORP Reach 3
0.7
17.6
20.3
5.4
7.4
0.7
29.1
31.8
LORP Reach 4
NA
NA
8.3
11.1
2.8
NA
19.4
11.1
LORP Reach 5
NA
NA
NA
NA
NA
NA
2.9
17.6
LORP Reach 6
NA
NA
NA
NA
NA
NA
NA
8.1
Occupancy summary statistics
Average Annual Occupancy: 9.7 %
Highest Occupancy Year: 2025 ( 24.2 %)
Lowest Occupancy Year: 2023 ( 0.2 %)
Most Occupied Reach: LORP Reach 1 ( 30.7 % average)
Least Occupied Reach: LORP Reach 6 ( 8.1 % average)
Cumulative discovery and occupancy forecasts
Annual point maps fluctuate with survey effort and with avoidance of known dense or treated stands. A more informative estimand is cumulative discovery: the share of corridor units that have ever had a detection. Pepperweed is already widespread on adjacent conveyances and upper-river reaches; the planning question is how quickly known occupancy along the LORP corridor approaches saturation under continued RAS and waterway-mediated spread.
Cumulative Discovery by Year
Once a segment or river mile has a detection, it remains in the cumulative total. This isolates discovery/spread from year-to-year survey intensity.
Cumulative pepperweed discovery along the LORP corridor (0.1-mi segments and whole river miles)
Year
Segments Known
New Segments
Segment %
Miles Known
New Miles
Mile %
2018
15
15
2.5
8
8
13.1
2019
75
60
12.5
18
10
29.5
2020
100
25
16.7
22
4
36.1
2021
107
7
17.8
25
3
41.0
2022
127
20
21.2
29
4
47.5
2023
128
1
21.3
30
1
49.2
2024
159
31
26.5
35
5
57.4
2025
204
45
34.0
44
9
72.1
Forecast to Corridor Saturation
Models fit to cumulative occupancy and projected forward:
Linear — constant annual discovery rate; reaches 100% in finite time.
Logistic — S-curve to a 100% carrying capacity; slows as unoccupied corridor shrinks.
Recent trajectory (whole miles) — slope from the last two survey years only; tests the hypothesis that pepperweed is already more ubiquitous than the full-series fit implies, and limited annual effort is still revealing occupied miles quickly.
Pepperweed is already common on nearby ditches, streams, and upper-river reaches; these forecasts treat the LORP corridor as filling toward full known occupancy under continued survey and/or spread.
Projected calendar year when cumulative occupancy reaches each milestone (fit to 2018–2025 discovery)
Resolution
Occupancy Milestone
Linear
Asymptotic
Logistic
0.1-mi segments
50%
2030
2032
2028
0.1-mi segments
75%
2037
2047
2033
0.1-mi segments
90%
2041
2067
2037
0.1-mi segments
95%
2042
2083
2041
0.1-mi segments
99%
2043
2118
2048
0.1-mi segments
100% (linear only)
2044
NA
NA
Whole river miles
50%
2022
2022
2022
Whole river miles
75%
2026
2028
2026
Whole river miles
90%
2028
2035
2030
Whole river miles
95%
2029
2041
2032
Whole river miles
99%
2029
2053
2038
Whole river miles
100% (linear only)
2029
NA
NA
Infill lag (whole mile → 0.1-mi segment)
The horizontal gap between the blue (whole-mile) and red (0.1-mi) logistic curves is the years of additional discovery needed to occupy every subsegment once whole-mile occupancy has already reached that level.
Time lag between whole-mile and subsegment occupancy (logistic forecasts)
Occupancy level
Whole-mile year
0.1-mi year
Lag (yr)
Interpretation
50%
2022
2028
6
~6 yr to fill 0.1-mi gaps after whole miles hit 50%
75%
2026
2033
7
~7 yr to fill 0.1-mi gaps after whole miles hit 75%
90%
2030
2037
7
~7 yr to fill 0.1-mi gaps after whole miles hit 90%
Model Fit Summary
Segment linear: +3.7 percentage points/year → 100% in ~2044
Mile linear (full series): +7.1 percentage points/year → 100% in ~2029
Current cumulative occupancy (2025): 34.0% of 0.1-mi segments; 72.1% of whole river miles.
Interpretation
Whole-mile occupancy is already high (~72.1% known), consistent with coarse-scale presence along much of the corridor.
0.1-mile occupancy remains lower (~34%). Annotated horizontal gaps estimate years of additional surveying or local fill before every subsegment is known occupied at the same occupancy level.
Recent-trajectory scenario: using only 2023–2025 whole-mile discovery (+11.5 pp/yr), cumulative mile occupancy reaches 100% around 2028. This upper-bound case assumes the last two years’ discovery pace continues.
Full-series linear and logistic forecasts are slower and more consistent with a sampling or pulse–plateau process; the recent trajectory is not a separate biological invasion model.
Flood years with limited access (e.g., 2023) can depress annual detections while cumulative occupancy remains unchanged; subsequent accessible seasons may show discovery pulses.
Increased RAS effort in currently empty miles (see Monitoring applications) accelerates cumulative discovery even without new colonization.
Smooth curves average across hydrologic structure. Flood years 2017 and 2023 limited RAS access; large discovery steps followed in 2019 and 2025. The fast step-pulse scenario assumes that ~6-year flood/discovery rhythm continues, with between-pulse drift equal to the mean of all non-pulse years. The low-spread scenario relaxes pulse frequency to 12 years and uses quieter between-pulse drift (years with ≤3 new whole miles)—in case 2017–2023 was an unusually wet cluster.
Calibrated rates
Fast plateau drift: +5.58 pp/yr (miles), +2.80 pp/yr (0.1-mi); pulse jumps +15.55 / +8.75 pp; next discovery pulse ~2031.
Locked-style 2026 one-step (non-pulse year): miles 77.7% (fast) vs 75.4% (low); segments 36.8% vs 34.6%.
River-mile patterns
Reach boundaries (northern)
Northern boundaries (minimum river miles) for each LORP reach
LORP Reach
Min River Mile
Max River Mile
Reach Number
Label Position
LORP Reach 1
0.1
3.9
1
0.1
LORP Reach 2
3.7
19.6
2
3.7
LORP Reach 3
19.7
34.5
3
19.7
LORP Reach 4
34.5
38.1
4
34.5
LORP Reach 5
39.0
42.4
5
39.0
LORP Reach 6
49.7
53.4
6
49.7
River mile distribution summary
Total River Miles Analyzed: 61
Years Covered: 2018 - 2025
River Mile Range: 0 - 60
Top 10 most active river miles
River Mile 10: 269 sites
River Mile 4: 196 sites
River Mile 7: 157 sites
River Mile 5: 99 sites
River Mile 6: 94 sites
River Mile 28: 83 sites
River Mile 3: 77 sites
River Mile 30: 64 sites
River Mile 34: 59 sites
River Mile 29: 51 sites
Cumulative known occupancy at 0.1-mi resolution
Annual site maps can show “gaps” in later years where crews skip known dense or treated stands. The heatmap below is cumulative: once a 0.1-mi corridor segment is detected, it remains colored in subsequent years (fill = maximum annual site count observed through that year). White = never detected by that year. Only sites within 250 m of the river-mile centerline are included.
Monitoring applications (2026 RAS)
The FY 2026–2027 LORP work plan directs summer RAS monitoring toward areas without known pepperweed populations, consistent with prioritizing satellite populations and leading edges (Renz and DiTomaso 2012). Tables below use 2018–2025 corridor detections binned to whole river miles (see River-mile patterns).
Priority Survey Areas
River-mile segments with no pepperweed detections on the river corridor in any survey year (2018–2025). Only sites within 250 m of a river-mile marker count toward occupancy (off-channel ditch/field points are excluded so they do not falsely mark a mile as occupied). These align with the workplan objective to survey unoccupied corridor.
Recommended 2026 RAS survey segments without known pepperweed (n = 20 river miles, 32.8% of 0–60 RM corridor)
Priority
River Mile Range
Length (mi)
Reach
Population Status
Bank Guidance
High
RM 54–60
7
R6
No known populations (all years)
Survey both banks
High
RM 43–48
6
R6
No known populations (all years)
Survey both banks
Medium
RM 21–22
2
R3
No known populations (all years)
Survey both banks
Medium
RM 35–36
2
R4
No known populations (all years)
Survey both banks
Medium
RM 51–52
2
R6
No known populations (all years)
Survey both banks
Low
RM 24
1
R3
No known populations (all years)
Survey both banks
Summary: 20 of 61 river miles (32.8%) have no recorded pepperweed on the corridor (within 250 m of a river-mile marker).
East / West Bank Planning Notes
Bank side for planning is taken from each site’s coordinates relative to the river centerline (local channel direction at each river mile), which matches how east/west can be derived from XY even when a legacy bank attribute exists on older forms. Sites are classified as east or west of the centerline for gap targeting.
Unoccupied miles (table above): survey both banks — absence of detections does not indicate which bank was visited.
Partially occupied miles (table below): where all detections fall on one bank, the opposite bank may warrant targeted survey even though populations are known nearby.
River miles with detections on a single bank only (2018–2025)
Reach 3 gap miles (RM 21–22, 24) — short unoccupied segments within otherwise occupied reach; efficient to cover during Reach 3 transit.
Single-bank follow-up — optional targeted passes on the undetected bank at miles listed above, if crew capacity allows.
Access and Route Planning (next step)
RAS is on foot (no boats). In a dry year, crews will mostly follow dry banks, levees, roads, and known walking routes rather than secondary-channel networks.
Field Maps navigation (preferred for split crews): use the east/west gap lines exported below — parallel to the mainstem, drawn only where that bank has no corridor detections for ≥400 m (0.1-mi bins dissolved). Five+ people on different routes each open the same web map / offline area; they do not each need Tracker licenses (those are only for recording tracks).
Bank
Color
Pattern
Meaning
East
Sky blue #0077BB
Solid
Walk/survey the east side — no detections on that bank for this stretch
West
Orange #EE7733
Dashed
Walk/survey the west side — no detections on that bank for this stretch
Colors follow a colorblind-safe pair; pattern differs too so meaning is not color-only (ADA). If detections exist only on the west, the east line still draws for that stretch.
Optional tracks (handheld GPS or phone GPX) still help refine entry points later; do not block planning on them. Use the river-mile tables plus these gap lines until then.
Field Maps Navigation Layer
East/west gap lines = polylines parallel to the river-mile centerline (±50 m), one color/pattern per bank, only where that bank has no pepperweed detections in the 250 m corridor for a contiguous stretch of ≥400 m (built from 0.1-mi segments). If only the west bank has detections, the east line still appears for that reach. The multithreaded river is included as a reference layer.
Draft for Field Maps — review against the river-mile tables before treating as final.
Download — east/west gap survey lines
38 lines (~59.8 mi): parallel to the mainstem on bank sides with no corridor pepperweed detections for ≥400 m. East = solid sky blue; West = dashed orange.
Pepperweed observations with field photos are available from the AGOL feature service attachments. (Photo gallery chunk is currently skipped at render time to avoid slow attachment checks; set eval: true on field-images / field-images-display to refresh.)
Conclusions
Occurrence records for 2018–2025 document progressive filling of known pepperweed occupancy along the LORP corridor, with coarse (whole-mile) occupancy substantially ahead of fine (0.1-mi) occupancy. Cumulative discovery isolates this infill process from year-to-year survey intensity and from resurvey avoidance of known stands. Smooth forecasts provide planning benchmarks for when known occupancy may approach saturation; a pulse-recruitment reading—flood-driven steps, quieter intervals, and lagged discovery after high-water years—remains a process-motivated alternative to purely linear discovery.
Operationally, FY 2026–27 RAS priorities target corridor miles and bank stretches still lacking detections, consistent with managing leading edges and satellite populations (Renz and DiTomaso 2012). This report will update as new observations enter the live feature service. A companion manuscript track will lock and later evaluate formal one-year-ahead occupancy forecasts.
Data disclaimer
Data are collected for monitoring and management by the Inyo County Water Department. Field conditions, survey timing, and observer experience affect quality. Users should verify critical information independently before management decisions.
Young, J. A., C. E. Turner, and L. F. James. 1995. Perennial pepperweed. Rangelands 17:121–123.
Citation
BibTeX citation:
@report{inyo_county_water_department,
author = {{Inyo County Water Department}},
publisher = {Inyo County Water Department},
title = {Cumulative Occupancy and Corridor-Scale Discovery of
Perennial Pepperweed {(*Lepidium} Latifolium*) Along the {Lower}
{Owens} {River} {Project}},
date = {},
url = {https://inyo-gov.github.io/noxious-weeds},
langid = {en},
abstract = {Perennial pepperweed (*Lepidium latifolium*) is a
high-impact riparian invader whose corridor-scale rates of spread
and discovery along the Lower Owens River Project (LORP) have
remained largely unquantified. This technical report summarizes
ArcGIS Online occurrence records from rapid assessment survey (RAS)
monitoring (2018–2025), maps spatial patterns by reach and river
mile, and estimates cumulative known occupancy at 0.1-mile and
whole-mile resolutions. Smooth (linear, logistic, recent-trajectory)
and step-pulse (fast 6-year vs low-spread 12-year) forecasts
describe the pace of known-occupancy infill under continued survey;
a 0.1-mi cumulative heatmap shows corridor discovery geography.
Interpretation emphasizes waterway-mediated pulse recruitment after
flooding, with discovery lagged when flood years limit access. The
report also identifies unoccupied corridor segments for the FY
2026–27 RAS workplan. Herbicide efficacy is outside the scope of
this analysis.}
}
---title: "Cumulative occupancy and corridor-scale discovery of perennial pepperweed (*Lepidium latifolium*) along the Lower Owens River Project"subtitle: "Technical data report · Inyo County Water Department · LORP rapid assessment survey, 2018–2025"author: - name: literal: "Inyo County Water Department" affiliations: - name: "Inyo County Water Department, Independence, California, United States"abstract: | Perennial pepperweed (*Lepidium latifolium*) is a high-impact riparian invader whose corridor-scale rates of spread and discovery along the Lower Owens River Project (LORP) have remained largely unquantified. This technical report summarizes ArcGIS Online occurrence records from rapid assessment survey (RAS) monitoring (2018–2025), maps spatial patterns by reach and river mile, and estimates cumulative known occupancy at 0.1-mile and whole-mile resolutions. Smooth (linear, logistic, recent-trajectory) and step-pulse (fast 6-year vs low-spread 12-year) forecasts describe the pace of known-occupancy infill under continued survey; a 0.1-mi cumulative heatmap shows corridor discovery geography. Interpretation emphasizes waterway-mediated pulse recruitment after flooding, with discovery lagged when flood years limit access. The report also identifies unoccupied corridor segments for the FY 2026–27 RAS workplan. Herbicide efficacy is outside the scope of this analysis.keywords: - Lepidium latifolium - perennial pepperweed - Lower Owens River Project - cumulative occupancy - riparian invasion - rapid assessment surveytbl-cap-location: topdate: todaydate-modified: todaycitation: type: report container-title: "Technical Data Report" publisher: "Inyo County Water Department" issued: today url: https://inyo-gov.github.io/noxious-weedsgoogle-scholar: truebibliography: references.bibcsl: ecology.cslparams: current_year: 2025 start_year: 2018format: html: theme: litera toc: true toc-depth: 3 toc-location: right toc-title: "Contents" code-fold: true code-tools: true css: styles.css docx: toc: true toc-depth: 3 number-sections: true reference-doc: custom-reference.docxexecute: echo: false warning: false message: falseprefer-html: true---```{r setup}#| message: false#| warning: falselibrary(sf)library(dplyr)library(tidyr)library(leaflet)library(leaflet.extras2)library(lubridate)library(htmltools)library(knitr)library(DT)library(ggplot2)library(scales)library(httr)# Field abundance codes: 5, 15, 25, 100, 200 where 200 means ≥200 (often thousands)format_abundance <-function(x) {ifelse(is.na(x), "Unknown",ifelse(x >=200, "200+ (may be thousands)", as.character(x)))}```## Introduction*Lepidium latifolium* L. (perennial pepperweed, tall whitetop) is a Eurasian mustard that invades wetlands, floodplains, irrigation infrastructure, and riparian corridors across the western United States [@young1995; @young1998; @calipc2024]. Dense stands displace desirable vegetation, alter soil chemistry, and are difficult to eradicate once established [@young1998; @cabi2024]. Propagules move as seed and root fragments; stands also expand vegetatively from creeping roots [@renz2012; @leininger2009].Along river corridors, two processes generate new occupied patches at different scales. Local kernels—vegetative edge advance (~0.85 m yr⁻¹ under undisturbed conditions) and seed fall near mother plants—densify existing infestations [@renz2012]. Landscape-level jumps require long-distance transport by water (especially floods), animals, or machinery [@young1995; @leininger2009]. Widespread flooding can therefore produce pulse recruitment: a step increase in occupied corridor, followed by quieter intervals until the next major hydrologic pulse. Flood years may also limit survey access, so *known* occupancy often steps upward in subsequent accessible seasons.Rewatering and expanded wetland habitat under the Lower Owens River Project (LORP) were recognized early as factors that could increase pepperweed susceptibility by creating wetted surfaces and facilitating downstream transport [@ladwp2004]. Inyo County Water Department and partners conduct rapid assessment survey (RAS) monitoring of occurrences along the corridor. Until the multi-year series summarized here, corridor-scale rates of pepperweed discovery and known occupancy along the LORP remained largely unquantified in management reporting [@ladwp2024; @ladwp2026].This technical report (1) documents spatial patterns of RAS detections for `r params$start_year`–`r params$current_year`, (2) estimates cumulative known occupancy at 0.1-mile and whole-mile resolutions, (3) presents operational forecasts of occupancy infill, and (4) identifies unoccupied corridor miles for FY 2026–27 RAS priorities ([approved workplan PDF](https://inyowater.org/wp-content/uploads/2026/06/2026-2027-LORP-Work-Plan-and-Budget_draft_062226.pdf)). The estimand is the rate and geography of known-occupancy infill, not herbicide efficacy.> "Perennial pepperweed (*Lepidium latifolium*), an introduced plant from southeastern Europe and Asia, is invasive throughout the western United States… both the California Department of Food and Agriculture (CDFA) and California Invasive Plant Council (Cal-IPC) list it as a noxious weed of great ecological concern." — [UC IPM](https://ipm.ucanr.edu/home-and-landscape/perennial-pepperweed/#gsc.tab=0)Regulatory context. Cal-IPC rating: High. CDFA: listed under CCR Section 4500 (State Noxious Weeds).```{r data-loading}#| message: false#| warning: false# ArcGIS Feature Service URLbase_url <-"https://services.arcgis.com/0jRlQ17Qmni5zEMr/arcgis/rest/services/Noxious_Weeds_2025_view/FeatureServer/0/query?"# Query parameters for all dataquery_params <-paste0("where=1=1","&outFields=*","&f=geojson","&outSR=4326")# Construct full query URLquery_url <-paste0(base_url, query_params)# Load data from ArcGIS feature servicepepper_data <-st_read(query_url, quiet =TRUE)# Process dates - convert Unix timestamps to datespepper_data <- pepper_data %>%mutate(Date_Observed =as.POSIXct(Date/1000, origin='1970-01-01'),Date_Created =as.POSIXct(CreationDate/1000, origin='1970-01-01'),Year_Observed =year(Date_Observed),Year_Created =year(Date_Created),# Use creation year as fallback when observation year is missingYear_Display =ifelse(is.na(Year_Observed), Year_Created, Year_Observed),Date_Display =as.POSIXct(ifelse(is.na(Date_Observed), as.numeric(Date_Created), as.numeric(Date_Observed)), origin='1970-01-01') )# Filter to LORP area based on northernmost current year data pointpepper_current_year_for_bounds <- pepper_data %>%filter(Year_Display == params$current_year)if(nrow(pepper_current_year_for_bounds) >0) {# Get the northernmost latitude from current year data northernmost_lat <-max(st_coordinates(pepper_current_year_for_bounds)[,2])# Filter all data to LORP area (south of northernmost current year point) pepper_data <- pepper_data %>%filter(st_coordinates(.)[,2] <= northernmost_lat)}# Include all records (no year filtering)# Separate data by display year (observation year with creation year fallback)pepper_2018 <- pepper_data %>%filter(Year_Display ==2018)pepper_2019 <- pepper_data %>%filter(Year_Display ==2019)pepper_2020 <- pepper_data %>%filter(Year_Display ==2020)pepper_2021 <- pepper_data %>%filter(Year_Display ==2021)pepper_2022 <- pepper_data %>%filter(Year_Display ==2022)pepper_2023 <- pepper_data %>%filter(Year_Display ==2023)pepper_2024 <- pepper_data %>%filter(Year_Display ==2024)pepper_2025 <- pepper_data %>%filter(Year_Display ==2025)# Create recent vs historical comparisonpepper_recent <- pepper_data %>%filter(Year_Display >=2022)pepper_historical <- pepper_data %>%filter(Year_Display >=2018& Year_Display <=2021)# Summary statistics for all years (using display year with fallback)summary_stats <- pepper_data %>%filter(!is.na(Year_Display)) %>%group_by(Year_Display) %>%summarise(Total_Observations =n(),Avg_Abundance =round(mean(Abundance, na.rm =TRUE), 1),Avg_Height =round(mean(Height, na.rm =TRUE), 1),Max_Abundance =max(Abundance, na.rm =TRUE),Records_With_Obs_Date =sum(!is.na(Year_Observed)),Records_With_Creation_Date_Only =sum(is.na(Year_Observed) &!is.na(Year_Created)),.groups ='drop' ) %>%arrange(Year_Display)```## Methods### Occurrence dataPoint observations of perennial pepperweed were obtained from the [Inyo County ArcGIS Online Noxious Weeds feature service](https://services.arcgis.com/0jRlQ17Qmni5zEMr/arcgis/rest/services/Noxious_Weeds_2025_view/FeatureServer/0) and restricted to the LORP study extent (south of the northernmost `r params$current_year` detection). Display year uses the observation date when available and the feature creation date otherwise. Field abundance is recorded in classes 5, 15, 25, 100, 200, where 200 denotes ≥200 plants (dense stands may contain thousands of individuals).2018 as a cumulative baseline. Records labeled 2018 are not a single-season snapshot: they accumulate detections from surveys in 2018 and earlier years that were consolidated into the modern feature service. Later calendar years (2019–`r params$current_year`) more closely reflect that season’s survey effort and new discoveries. Cumulative occupancy from 2018 onward therefore treats 2018 as the start-of-series known stock, not as an annual recruitment pulse.Bank side. Pre-2018 field forms sometimes recorded east/west bank explicitly. That attribute is largely redundant for corridor analysis: bank can be derived from each point’s XY location relative to the river channel / mile centerline (as in the 2026 east/west gap lines). This report does not rely on a stored bank field.### Corridor occupancyFor occupancy and monitoring priorities, sites more than 250 m from the river-mile centerline are excluded so off-channel ditch or field points do not falsely occupy a corridor mile. Cumulative known occupancy is the share of (a) 0.1-mile segments and (b) whole river miles (RM 0–60) that have ever had a corridor detection by a given year. Once detected, a unit remains in the cumulative total—isolating discovery/infill from year-to-year variation in survey intensity and from resurvey avoidance of known stands.### ForecastsLinear, asymptotic, logistic, and recent-trajectory models are fit to the cumulative series as operational descriptions of known-occupancy infill (not stem-density invasion models). Step-pulse scenarios encode flood-then-plateau structure: a fast regime (~6-year discovery pulses after the recent 2017/2023 flood cluster) and a low-spread regime (12-year pulses with quieter between-pulse drift). Calendar milestones are rounded to whole years. A 0.1-mi cumulative heatmap along the corridor visualizes discovery geography without false gaps from resurvey avoidance.### Limitations- Records represent documented RAS observations only; absence of a point does not prove absence of plants.- The 2018 year class pools pre-2018 historical detections with 2018 surveys; year-to-year increments after 2018 are the appropriate scale for annual discovery rates.- Survey timing, access (including floods), and observer effort affect annual maps more than cumulative discovery.- Abundance classes are field estimates, not stem counts.- Forecasts describe *known* occupancy under continued survey and do not attribute residuals to herbicide treatment intensity.### Dataset overviewThe filtered dataset includes `r nrow(pepper_data)` observations across `r length(unique(pepper_data$Year_Display[!is.na(pepper_data$Year_Display)]))` years (`r params$start_year`–`r params$current_year`), with peak survey activity in `r names(sort(table(pepper_data$Year_Display, useNA='ifany'), decreasing=TRUE)[1])` (`r max(table(pepper_data$Year_Display, useNA='ifany'))` records).| Metric | Value || ---| ---:|| Total observations |`r nrow(pepper_data)`|| Peak survey year |`r names(sort(table(pepper_data$Year_Display, useNA='ifany'), decreasing=TRUE)[1])` (`r max(table(pepper_data$Year_Display, useNA='ifany'))` records) || Recent period (2022–2025) |`r nrow(pepper_recent)`|| Earlier period (2018–2021) |`r nrow(pepper_historical)`|| Records with usable year |`r round((nrow(pepper_data) - sum(is.na(pepper_data$Year_Display))) / nrow(pepper_data) * 100, 1)`% |## Spatial occurrenceInteractive map of pepperweed detections (`r params$start_year`–`r params$current_year`). Toggle years in the layer control. Creation dates substitute when observation dates are missing. The 2018 & earlier layer is the cumulative baseline (pre-2018 plus 2018), not a single field season. Reference layers: purple river channels; 2026 East gap lines (solid sky blue) and 2026 West gap lines (dashed orange) mark bank stretches ≥400 m without corridor detections—east/west assigned from point XY relative to the channel, not from a legacy bank attribute.::: {.map-a11y-summary}Accessible map summary: Points are colored by survey year (`r params$start_year`–`r params$current_year`). Parallel gap lines mark east (solid blue) and west (dashed orange) banks without detections for ≥400 m — the 2026 RAS walk targets. Equivalent data are in the downloadable gap-line files and the monitoring recommendation tables. Screen-reader users can rely on those tables rather than the interactive map canvas.:::<div class="download-section"><h3>Data Downloads</h3><div class="download-links"><a href="pepperweed_data.geojson">Download GeoJSON</a><a href="pepperweed_data.kml">Download KML</a></div><p>Complete dataset with all `r nrow(pepper_data)` pepperweed observations</p></div>```{r generate-geojson}#| message: false#| warning: false#| echo: false#| results: hide# Generate GeoJSON file for downloadst_write(pepper_data, "pepperweed_data.geojson", driver ="GeoJSON", delete_dsn =TRUE, quiet =TRUE)# Generate KML file for Google Earth (will be zipped to KMZ)st_write(pepper_data, "pepperweed_data.kml", driver ="KML", delete_dsn =TRUE, quiet =TRUE)# Calculate spatial extentbbox <-st_bbox(pepper_data)extent_info <-list(north = bbox$ymax,south = bbox$ymin, east = bbox$xmax,west = bbox$xmin,center_lat = (bbox$ymax + bbox$ymin) /2,center_lon = (bbox$xmax + bbox$xmin) /2)```<div class="spatial-extent"><h4>Study Area Boundaries</h4><ul><li>North: `r round(extent_info$north, 4)`°N</li><li>South: `r round(extent_info$south, 4)`°N</li><li>East: `r round(extent_info$east, 4)`°W</li><li>West: `r round(extent_info$west, 4)`°W</li><li>Center: `r round(extent_info$center_lat, 4)`°N, `r round(extent_info$center_lon, 4)`°W</li></ul></div>```{r interactive-map}#| message: false#| warning: false# Add reach information to pepper_data if availableif(exists("pepper_data_with_reaches")) {# Merge reach information back to main dataset pepper_data <- pepper_data %>%left_join( pepper_data_with_reaches %>%st_drop_geometry() %>%select(OBJECTID, Name),by ="OBJECTID" )}# Load water quality stations as reference pointswq_url <-"https://services.arcgis.com/0jRlQ17Qmni5zEMr/arcgis/rest/services/lorp_wq_view/FeatureServer/0/query?where=1=1&outFields=*&f=geojson&outSR=4326"wq_stations <-st_read(wq_url, quiet =TRUE)# Load river reaches for labeling (if not already loaded)if(!exists("river_reaches")) { reach_url <-"https://inyocounty.maps.arcgis.com/sharing/rest/content/items/90e5870bd5914a928bd97b023f07b807/data" river_reaches <-st_read(reach_url, quiet =TRUE) river_reaches <-st_transform(river_reaches, st_crs(pepper_data))}# Get reach centroids for labeling - only LORP reaches 1-6reach_centroids <- river_reaches %>%filter(Name %in%c("LORP Reach 1", "LORP Reach 2", "LORP Reach 3", "LORP Reach 4", "LORP Reach 5", "LORP Reach 6")) %>%st_centroid() %>%mutate(Reach_Label =gsub("LORP Reach ", "R", Name))# Gap survey lines + multithreaded river (reference layers)gap_geojson <-"exports/survey_nav_2026/ras_2026_survey_nav.geojson"ch_geojson <-"exports/survey_nav_2026/owens_river_channels.geojson"if ((!file.exists(gap_geojson) ||!file.exists(ch_geojson)) &&file.exists("export_survey_nav.R")) {system2("Rscript", "export_survey_nav.R", stdout =FALSE, stderr =FALSE)}gap_lines <-NULLriver_channels <-NULLif (file.exists(gap_geojson)) { gap_lines <-st_read(gap_geojson, quiet =TRUE) %>%st_transform(4326)}if (file.exists(ch_geojson)) { river_channels <-st_read(ch_geojson, quiet =TRUE) %>%st_transform(4326)}# Create comprehensive map with all yearscombined_map <-leaflet(pepper_data) %>%addProviderTiles(providers$Esri.WorldImagery, group ="World Imagery") %>%addProviderTiles(providers$OpenStreetMap, group ="OpenStreetMap")# 2026 east/west gap survey lines (parallel to mainstem)if (!is.null(gap_lines) &&nrow(gap_lines) >0) { gap_east <- gap_lines %>%filter(Bank =="East") gap_west <- gap_lines %>%filter(Bank =="West")if (nrow(gap_east) >0) { combined_map <- combined_map %>%addPolylines(data = gap_east,color ="#0077BB",weight =5,opacity =0.95,dashArray =NULL,popup =~paste0("<b>East bank gap</b> (solid sky blue)<br>", RM_Label, " · ", Length_mi, " mi<br>",ifelse(is.na(Reach), "", paste0(Reach, "<br>")),"No corridor detections on this bank ≥400 m" ),group ="2026 East gap lines",options =pathOptions(lineCap ="round") ) }if (nrow(gap_west) >0) { combined_map <- combined_map %>%addPolylines(data = gap_west,color ="#EE7733",weight =5,opacity =0.95,dashArray ="10, 10",popup =~paste0("<b>West bank gap</b> (dashed orange)<br>", RM_Label, " · ", Length_mi, " mi<br>",ifelse(is.na(Reach), "", paste0(Reach, "<br>")),"No corridor detections on this bank ≥400 m" ),group ="2026 West gap lines",options =pathOptions(lineCap ="round") ) }}# Multithreaded Owens River channelsif (!is.null(river_channels) &&nrow(river_channels) >0) { combined_map <- combined_map %>%addPolylines(data = river_channels,color ="#4527a0",weight =3,opacity =0.95,popup =~paste0("<b>Owens River channel</b><br>", ifelse(is.na(NAME), "", NAME)),group ="River channels" )}combined_map <- combined_map %>%# 2018 layer = cumulative baseline (pre-2018 + 2018)addCircleMarkers(data = pepper_2018,radius =5,color ="black",fillColor ="black",fillOpacity =0.7,weight =2,popup =~paste0("<b>2018 & earlier (baseline)</b><br>","Date: ", format(Date_Display, "%Y-%m-%d"), "<br>","Date Source: ", ifelse(is.na(Year_Observed), "Creation Date", "Observation Date"), "<br>","Species: ", Species, "<br>","Abundance: ", format_abundance(Abundance), "<br>","Height: ", ifelse(is.na(Height), "Unknown", paste(Height, "m")), "<br>",ifelse(exists("Name") &&!is.na(Name), paste0("LORP Reach: ", Name, "<br>"), ""),"Notes: ", ifelse(is.na(Notes), "None", Notes) ),group ="2018 & earlier" ) %>%# Add 2019 markersaddCircleMarkers(data = pepper_2019,radius =5,color ="darkred",fillColor ="darkred",fillOpacity =0.7,weight =2,popup =~paste0("<b>2019 Data</b><br>","Date: ", format(Date_Display, "%Y-%m-%d"), "<br>","Date Source: ", ifelse(is.na(Year_Observed), "Creation Date", "Observation Date"), "<br>","Species: ", Species, "<br>","Abundance: ", format_abundance(Abundance), "<br>","Height: ", ifelse(is.na(Height), "Unknown", paste(Height, "m")), "<br>","Notes: ", ifelse(is.na(Notes), "None", Notes) ),group ="2019 Data" ) %>%# Add 2020 markersaddCircleMarkers(data = pepper_2020,radius =5,color ="red",fillColor ="red",fillOpacity =0.7,weight =2,popup =~paste0("<b>2020 Data</b><br>","Date: ", format(Date_Display, "%Y-%m-%d"), "<br>","Date Source: ", ifelse(is.na(Year_Observed), "Creation Date", "Observation Date"), "<br>","Species: ", Species, "<br>","Abundance: ", format_abundance(Abundance), "<br>","Height: ", ifelse(is.na(Height), "Unknown", paste(Height, "m")), "<br>","Notes: ", ifelse(is.na(Notes), "None", Notes) ),group ="2020 Data" ) %>%# Add 2021 markersaddCircleMarkers(data = pepper_2021,radius =5,color ="yellow",fillColor ="yellow",fillOpacity =0.7,weight =2,popup =~paste0("<b>2021 Data</b><br>","Date: ", format(Date_Display, "%Y-%m-%d"), "<br>","Date Source: ", ifelse(is.na(Year_Observed), "Creation Date", "Observation Date"), "<br>","Species: ", Species, "<br>","Abundance: ", format_abundance(Abundance), "<br>","Height: ", ifelse(is.na(Height), "Unknown", paste(Height, "m")), "<br>","Notes: ", ifelse(is.na(Notes), "None", Notes) ),group ="2021 Data" ) %>%# Add 2022 markersaddCircleMarkers(data = pepper_2022,radius =5,color ="orange",fillColor ="orange",fillOpacity =0.7,weight =2,popup =~paste0("<b>2022 Data</b><br>","Date: ", format(Date_Display, "%Y-%m-%d"), "<br>","Date Source: ", ifelse(is.na(Year_Observed), "Creation Date", "Observation Date"), "<br>","Species: ", Species, "<br>","Abundance: ", format_abundance(Abundance), "<br>","Height: ", ifelse(is.na(Height), "Unknown", paste(Height, "m")), "<br>","Notes: ", ifelse(is.na(Notes), "None", Notes) ),group ="2022 Data" ) %>%# Add 2023 markersaddCircleMarkers(data = pepper_2023,radius =5,color ="green",fillColor ="green",fillOpacity =0.7,weight =2,popup =~paste0("<b>2023 Data</b><br>","Date: ", format(Date_Display, "%Y-%m-%d"), "<br>","Date Source: ", ifelse(is.na(Year_Observed), "Creation Date", "Observation Date"), "<br>","Species: ", Species, "<br>","Abundance: ", format_abundance(Abundance), "<br>","Height: ", ifelse(is.na(Height), "Unknown", paste(Height, "m")), "<br>","Notes: ", ifelse(is.na(Notes), "None", Notes) ),group ="2023 Data" ) %>%# Add 2024 markersaddCircleMarkers(data = pepper_2024,radius =5,color ="blue",fillColor ="blue",fillOpacity =0.8,weight =3,popup =~paste0("<b>2024 Data</b><br>","Date: ", format(Date_Display, "%Y-%m-%d"), "<br>","Date Source: ", ifelse(is.na(Year_Observed), "Creation Date", "Observation Date"), "<br>","Species: ", Species, "<br>","Abundance: ", format_abundance(Abundance), "<br>","Height: ", ifelse(is.na(Height), "Unknown", paste(Height, "m")), "<br>","Notes: ", ifelse(is.na(Notes), "None", Notes) ),group ="2024 Data" ) %>%# Add 2025 markersaddCircleMarkers(data = pepper_2025,radius =5,color ="purple",fillColor ="purple",fillOpacity =0.9,weight =3,popup =~paste0("<b>2025 Data</b><br>","Date: ", format(Date_Display, "%Y-%m-%d"), "<br>","Date Source: ", ifelse(is.na(Year_Observed), "Creation Date", "Observation Date"), "<br>","Species: ", Species, "<br>","Abundance: ", format_abundance(Abundance), "<br>","Height: ", ifelse(is.na(Height), "Unknown", paste(Height, "m")), "<br>",ifelse(exists("Name") &&!is.na(Name), paste0("LORP Reach: ", Name, "<br>"), ""),"Notes: ", ifelse(is.na(Notes), "None", Notes) ),group ="2025 Data" ) %>%# Add water quality stations as reference points (if data available) {if(nrow(wq_stations) >0) {addCircleMarkers(data = wq_stations,radius =4,color ="white",fillColor ="blue",fillOpacity =0.8,weight =2,popup =~paste0("<b>Water Quality Station</b><br>Reference Point"),group ="Water Quality Stations" ) } else { . # No stations to add }} %>%# Add reach labels (centroids)addLabelOnlyMarkers(data = reach_centroids,label =~Reach_Label,labelOptions =labelOptions(noHide =TRUE,direction ="center",textOnly =TRUE,style =list("color"="white","font-size"="14px","font-weight"="bold","text-shadow"="1px 1px 2px rgba(0,0,0,0.8)" ) ),group ="Reach Labels" )overlay_groups <-c(if (!is.null(gap_lines) &&nrow(gap_lines) >0) c("2026 East gap lines", "2026 West gap lines"),if (!is.null(river_channels) &&nrow(river_channels) >0) "River channels","2018 & earlier", "2019 Data", "2020 Data", "2021 Data", "2022 Data","2023 Data", "2024 Data", "2025 Data", "Reach Labels",if (nrow(wq_stations) >0) "Water Quality Stations")combined_map <- combined_map %>%addLayersControl(baseGroups =c("World Imagery", "OpenStreetMap"),overlayGroups = overlay_groups,options =layersControlOptions(collapsed =FALSE) ) %>%addLegend(position ="bottomleft",colors =c("#0077BB", "#EE7733", "black", "darkred", "red", "yellow", "orange", "green", "blue", "purple", if(nrow(wq_stations) >0) "blue"),labels =c("2026 East gap (solid)", "2026 West gap (dashed)", "2018", "2019", "2020", "2021", "2022", "2023", "2024", "2025", if(nrow(wq_stations) >0) "WQ Stations"),title ="Gap lines & detections by year" )combined_map <- combined_map %>% htmlwidgets::onRender("function(el, x) { el.setAttribute('role', 'img'); el.setAttribute( 'aria-label', 'Interactive map of perennial pepperweed detections along the Lower Owens River Project corridor, colored by survey year. Use the layer control to toggle years. Equivalent data are in the downloadable files and tables in this report.' ); }" )combined_map```## Abundance structureField-estimated plants per site in the most recent survey year, and recent low-abundance sites that may warrant early intervention.Field abundance is recorded in discrete estimate classes (5, 15, 25, 100, 200). A value of 200 means ≥200 plants — a lower bound for dense stands that may contain hundreds to thousands of individuals (not an exact count of 200).The following chart shows the distribution of these abundance classes for pepperweed sites documented in `r params$current_year`:```{r abundance-histogram}#| message: false#| warning: false#| results: 'asis'#| fig-alt: "Bar chart of perennial pepperweed sites by field abundance class (5, 15, 25, 100, 200+) for the current survey year. Class 200+ means 200 or more plants and may represent thousands."# Filter for current year data onlypepper_current_year <- pepper_data %>%filter(Year_Display == params$current_year)# Create abundance histogramif(nrow(pepper_current_year) >0) {# Use field estimate classes (200 = 200+) abundance_data <- pepper_current_year %>%filter(!is.na(Abundance)) %>%mutate(Abundance_Bin =case_when( Abundance <=5~"5", Abundance <=15~"15", Abundance <=25~"25", Abundance <=100~"100",TRUE~"200+" ),Abundance_Bin =factor(Abundance_Bin,levels =c("5", "15", "25", "100", "200+"),ordered =TRUE) )# Only include bins that have data abundance_counts <- abundance_data %>%count(Abundance_Bin) %>%filter(n >0)# Create bar chart with only populated bins hist_plot <-ggplot(abundance_counts, aes(x = Abundance_Bin, y = n)) +geom_bar(stat ="identity", fill ="#2E86AB", color ="white", alpha =0.8, width =0.7) +labs(title =paste("Distribution of Pepperweed Abundance in", params$current_year),subtitle =paste("Total sites:", nrow(pepper_current_year),"· Class 200+ = ≥200 plants (may be thousands)"),x ="Abundance class (field estimate)",y ="Number of Sites" ) +theme_minimal() +theme(plot.title =element_text(size =16, face ="bold", color ="#2E86AB"),plot.subtitle =element_text(size =12, color ="#666666"),axis.title =element_text(size =12),axis.text.x =element_text(angle =0, hjust =0.5),panel.grid.minor =element_blank(),panel.grid.major.x =element_blank() ) +scale_y_continuous(breaks = scales::pretty_breaks(n =8))print(hist_plot)# Management categories small_pops <-sum(pepper_current_year$Abundance <200, na.rm =TRUE) large_pops <-sum(pepper_current_year$Abundance >=200, na.rm =TRUE)cat("\n\n### ", params$current_year, " site distribution\n\n", sep ="")cat("- Total Sites:", nrow(pepper_current_year), "\n")cat("- Sites below 200 class:", small_pops, "(", round(small_pops/nrow(pepper_current_year)*100, 1), "%)\n")cat("- Sites in 200+ class (≥200 plants; may be thousands):", large_pops, "(", round(large_pops/nrow(pepper_current_year)*100, 1), "%)\n")# Detailed bin countscat("\n\n### Detailed abundance distribution\n\n")for(i in1:nrow(abundance_counts)) { bin_name <-as.character(abundance_counts$Abundance_Bin[i]) bin_count <- abundance_counts$n[i] label <-if (bin_name =="200+") "200+ plants (≥200; may be thousands)"elsepaste0(bin_name, " plants (estimate class)")cat("- ", label, ": ", bin_count, " sites\n", sep ="") }# Range informationcat("\n\n### Abundance coding\n\n")cat("- Field classes used: 5, 15, 25, 100, 200\n")cat("- 200 = 200 or more — dense patches are not counted stem-by-stem and can represent thousands of individuals\n\n")} else {cat("\n\nNo ", params$current_year, " data available for abundance analysis.\n\n", sep ="")}```The interactive map above provides the primary visualization of pepperweed distribution patterns. #### Recent Sites```{r new-sites}#| message: false#| warning: false# Show all recent sites (2024-2025) - prioritize small sites for early intervention# Sort by numeric abundance, then format for display (200 = 200+)recent_sites <- pepper_data %>%filter(Year_Display >=2024) %>%arrange(Abundance, desc(Year_Display)) %>%mutate(Abundance =format_abundance(Abundance)) %>%select(Date_Display, Abundance, Height, Notes, Year_Display)if(nrow(recent_sites) >0) {datatable(recent_sites,caption ="Recent Pepperweed Sites (2024-2025) - Prioritize Small Sites for Early Intervention",options =list(pageLength =15, scrollX =TRUE))} else {cat("No recent sites found.")}```## Reach-scale patterns```{r river-reach-analysis}#| message: false#| warning: false#| results: 'asis'#| fig-width: 16#| fig-height: 15#| fig-alt: "Faceted bar charts showing pepperweed site counts by year for each LORP reach (Reaches 1 through 6)."# Read river reach polygons from ArcGIS Online and filter to LORP reaches 1-6reach_url <-"https://inyocounty.maps.arcgis.com/sharing/rest/content/items/90e5870bd5914a928bd97b023f07b807/data"river_reaches <-st_read(reach_url, quiet =TRUE)# Filter to only LORP reaches 1-6 - explicit selectiondesired_lorp_reaches <-c("LORP Reach 1", "LORP Reach 2", "LORP Reach 3", "LORP Reach 4", "LORP Reach 5", "LORP Reach 6")river_reaches <- river_reaches %>%filter(Name %in% desired_lorp_reaches)# Ensure both datasets have the same CRS (WGS84)river_reaches <-st_transform(river_reaches, st_crs(pepper_data))# Perform spatial join to determine which reach each pepperweed site is closest to# Use nearest neighbor join since sites might not fall exactly within reach polygonspepper_data_with_reaches <- pepper_data %>%st_join(river_reaches, join = st_nearest_feature)# Filter to only sites within reachespepper_data_with_reaches <- pepper_data_with_reaches %>%filter(!is.na(Name)) # Only keep sites that fall within a reach# Create summary data for reach analysisreach_summary <- pepper_data_with_reaches %>%st_drop_geometry() %>%group_by(Year_Display, Name) %>%summarise(Site_Count =n(), .groups ='drop') %>%# Ensure all year-reach combinations are representedcomplete(Year_Display =unique(Year_Display), Name =unique(Name), fill =list(Site_Count =0))# Create faceted histogram for reachesif(nrow(reach_summary) >0) {# Cap sites at 20 for better small site visibility reach_summary_capped <- reach_summary %>%mutate(Site_Count_Capped =pmin(Site_Count, 20))# Create ordered factor for reaches (1 at top, 6 at bottom) reach_summary_capped <- reach_summary_capped %>%mutate(Name_Ordered =factor(Name, levels =rev(c("LORP Reach 1", "LORP Reach 2", "LORP Reach 3", "LORP Reach 4", "LORP Reach 5", "LORP Reach 6")))) reach_plot <-ggplot(reach_summary_capped, aes(x = Site_Count_Capped, y = Name_Ordered)) +geom_col(fill ="red", alpha =0.8, width =0.7) +facet_wrap(~ Year_Display, ncol =8, scales ="fixed") +# Single row, fixed scalesscale_x_continuous(limits =c(0, 20), breaks =c(0, 5, 10, 15, 20)) +# Cap at 20scale_y_discrete() +# Reach nameslabs(title ="Pepperweed Sites by LORP Reach and Year",subtitle ="Distribution of sites within LORP river reaches (capped at 20 sites)",x ="Number of Sites (capped at 20)",y ="LORP Reach" ) +theme_minimal() +theme(plot.title =element_text(size =18, face ="bold", color ="#2E86AB"),plot.subtitle =element_text(size =16, color ="#666666"),axis.title =element_text(size =14),axis.text.y =element_text(size =11),axis.text.x =element_text(size =11),strip.text =element_text(size =13, face ="bold"),panel.grid.minor =element_blank(),panel.grid.major.y =element_blank(),plot.margin =margin(25, 25, 25, 25) )print(reach_plot)# Summary statisticscat("\n\n### LORP reach distribution summary\n\n")cat("- Total Reaches Analyzed:", length(unique(reach_summary$Name)), "\n")cat("- Years Covered:", min(reach_summary$Year_Display), "-", max(reach_summary$Year_Display), "\n")cat("- Reaches with Data:", length(unique(reach_summary$Name[reach_summary$Site_Count >0])), "\n")# Most active reaches top_reaches <- reach_summary %>%group_by(Name) %>%summarise(Total_Sites =sum(Site_Count), .groups ='drop') %>%arrange(desc(Total_Sites))cat("\n\n### Reach activity summary\n\n")for(i in1:nrow(top_reaches)) {cat("- ", top_reaches$Name[i], ": ", top_reaches$Total_Sites[i], " sites\n", sep ="") }# Sites not in any reach sites_outside_reaches <-nrow(pepper_data) -nrow(pepper_data_with_reaches)if(sites_outside_reaches >0) {cat("\n\n### Sites outside LORP reaches\n\n")cat("- Sites outside reach polygons:", sites_outside_reaches, "\n")cat("- Percentage of total sites:", round(sites_outside_reaches/nrow(pepper_data)*100, 1), "%\n\n") }} else {cat("\n\nNo reach data available for analysis.\n\n")}```## Corridor occupancy and discovery forecasts```{r occupancy-analysis}#| message: false#| warning: false#| results: 'asis'# Load river miles data if not already loadedif(!exists("rivermiles")) { rivermiles <-st_read('data/LORP_RiverMiles_revised.shp', quiet =TRUE) rivermiles <- rivermiles %>%mutate(RiverMile =as.numeric(NAME)) rivermiles <-st_transform(rivermiles, st_crs(pepper_data))}# Create 1/10 mile segments for detailed occupancy analysis# Total of 600 segments across 60 miles (0.1 mile resolution)segment_size <-0.1total_miles <-60total_segments <- total_miles / segment_size # 600 segments# Create segment boundariessegment_boundaries <-data.frame(Segment_ID =1:total_segments,Mile_Start =seq(0, total_miles - segment_size, by = segment_size),Mile_End =seq(segment_size, total_miles, by = segment_size))# Join pepperweed data with river miles and reachespepper_data_with_miles_and_reaches <- pepper_data %>%st_join(rivermiles, join = st_nearest_feature) %>%st_join(river_reaches, join = st_nearest_feature) %>%mutate(RiverMile =as.numeric(NAME)) %>%filter(!is.na(Name)) # Only sites with reach assignments# Determine which segment each pepperweed site falls intopepper_data_with_segments <- pepper_data_with_miles_and_reaches %>%st_drop_geometry() %>%mutate(Segment_ID =floor(RiverMile / segment_size) +1,# Ensure segments are within boundsSegment_ID =pmin(Segment_ID, total_segments) )# Overall occupancy by yearoverall_occupancy <- pepper_data_with_segments %>%group_by(Year_Display) %>%summarise(Segments_Occupied =n_distinct(Segment_ID),Total_Segments = total_segments,Occupancy_Percent =round(Segments_Occupied / Total_Segments *100, 1),.groups ='drop' ) %>%arrange(Year_Display)# Reach-specific occupancyreach_occupancy <- pepper_data_with_segments %>%group_by(Year_Display, Name) %>%summarise(Segments_Occupied =n_distinct(Segment_ID),.groups ='drop' ) %>%# Calculate total segments per reachleft_join( pepper_data_with_miles_and_reaches %>%st_drop_geometry() %>%group_by(Name) %>%summarise(Reach_Mile_Start =min(RiverMile, na.rm =TRUE),Reach_Mile_End =max(RiverMile, na.rm =TRUE),Reach_Total_Segments = (Reach_Mile_End - Reach_Mile_Start) / segment_size,.groups ='drop' ),by ="Name" ) %>%mutate(Occupancy_Fraction =round(Segments_Occupied / Reach_Total_Segments, 3),Occupancy_Percent =round(Occupancy_Fraction *100, 1) ) %>%arrange(Year_Display, Name)# Display resultscat("\n\n### Overall occupancy analysis\n\n")cat("Resolution: 1/10 mile segments (600 total segments across 60 miles)\n\n")# Overall occupancy tablecat("#### Overall occupancy by year\n\n")print(kable(overall_occupancy, col.names =c("Year", "Segments Occupied", "Total Segments", "Occupancy %"),align =c("c", "c", "c", "c"),caption ="Number of 1/10 mile segments occupied by pepperweed"))# Reach occupancy tablecat("\n\n#### Reach-specific occupancy\n\n")reach_occupancy_wide <- reach_occupancy %>%select(Year_Display, Name, Occupancy_Percent) %>%pivot_wider(names_from = Year_Display, values_from = Occupancy_Percent, names_prefix ="Year_") %>%arrange(Name)print(kable(reach_occupancy_wide, col.names =c("LORP Reach", "2018", "2019", "2020", "2021", "2022", "2023", "2024", "2025"),align =c("l", rep("c", 8)),caption ="Percentage of each reach occupied by pepperweed (by year)"))# Summary statisticscat("\n\n### Occupancy summary statistics\n\n")cat("- Average Annual Occupancy:", round(mean(overall_occupancy$Occupancy_Percent), 1), "%\n")cat("- Highest Occupancy Year:", overall_occupancy$Year_Display[which.max(overall_occupancy$Occupancy_Percent)],"(", max(overall_occupancy$Occupancy_Percent), "%)\n")cat("- Lowest Occupancy Year:", overall_occupancy$Year_Display[which.min(overall_occupancy$Occupancy_Percent)],"(", min(overall_occupancy$Occupancy_Percent), "%)\n")# Most/least occupied reachesreach_summary <- reach_occupancy %>%group_by(Name) %>%summarise(Avg_Occupancy =round(mean(Occupancy_Percent), 1),Max_Occupancy =max(Occupancy_Percent),.groups ='drop' ) %>%arrange(desc(Avg_Occupancy))cat("\n\n- Most Occupied Reach:", reach_summary$Name[1], "(", reach_summary$Avg_Occupancy[1], "% average)\n")cat("- Least Occupied Reach:", reach_summary$Name[nrow(reach_summary)], "(", reach_summary$Avg_Occupancy[nrow(reach_summary)], "% average)\n\n")```### Cumulative discovery and occupancy forecastsAnnual point maps fluctuate with survey effort and with avoidance of known dense or treated stands. A more informative estimand is cumulative discovery: the share of corridor units that have ever had a detection. Pepperweed is already widespread on adjacent conveyances and upper-river reaches; the planning question is how quickly *known* occupancy along the LORP corridor approaches saturation under continued RAS and waterway-mediated spread.#### Cumulative Discovery by YearOnce a segment or river mile has a detection, it remains in the cumulative total. This isolates discovery/spread from year-to-year survey intensity.```{r occupancy-forecast}#| message: false#| warning: false#| results: 'asis'# Cumulative discovery: first year each 0.1-mi segment was detectedfirst_detect_seg <- pepper_data_with_segments %>%group_by(Segment_ID) %>%summarise(FirstYear =min(Year_Display), .groups ="drop")# Cumulative discovery at whole-mile resolutionfirst_detect_mile <- pepper_data_with_segments %>%mutate(Mile =floor(RiverMile)) %>%group_by(Mile) %>%summarise(FirstYear =min(Year_Display), .groups ="drop")obs_years <-sort(unique(pepper_data_with_segments$Year_Display))n_miles_corridor <-length(0:60) # 61 whole milescum_occupancy <-data.frame(Year = obs_years,Segments_Known =sapply(obs_years, function(y) sum(first_detect_seg$FirstYear <= y)),Miles_Known =sapply(obs_years, function(y) sum(first_detect_mile$FirstYear <= y))) %>%mutate(Seg_Pct =round(Segments_Known / total_segments *100, 1),Mile_Pct =round(Miles_Known / n_miles_corridor *100, 1),New_Segments = Segments_Known -lag(Segments_Known, default =0),New_Miles = Miles_Known -lag(Miles_Known, default =0) )print(kable(cum_occupancy %>%select(Year, Segments_Known, New_Segments, Seg_Pct, Miles_Known, New_Miles, Mile_Pct),col.names =c("Year", "Segments Known", "New Segments", "Segment %", "Miles Known", "New Miles", "Mile %"),align =c("c", "c", "c", "c", "c", "c", "c"),caption ="Cumulative pepperweed discovery along the LORP corridor (0.1-mi segments and whole river miles)"))# --- Forecast helpers ---# Linear: p = a + b * year (extrapolate to 100)# Asymptotic (negative exponential to K=100): p = K * (1 - exp(-r * (year - t0)))# Logistic to K=100: p = K / (1 + exp(-r * (year - t_mid)))fit_linear_to_100 <-function(years, pct, label) { fit <-lm(pct ~ years) a <-coef(fit)[1] b <-coef(fit)[2] year_100 <-if (b >0) (100- a) / b elseInflist(label = label, model ="Linear", a = a, b = b, year_100 = year_100, fit = fit,predict =function(y) pmin(100, pmax(0, a + b * y)))}fit_asymptotic_to_100 <-function(years, pct, label, K =100) {# p = K * (1 - exp(-r * (t - t0))); constrain t0 near first year, fit r obj <-function(par) { r <-exp(par[1]) # > 0 t0 <- par[2] pred <- K * (1-exp(-r * (years - t0)))sum((pct - pred)^2) } starts <-list(c(log(0.05), min(years) -1),c(log(0.08), min(years) -2),c(log(0.03), min(years)),c(log(0.1), min(years) -3) ) best <-NULL best_sse <-Inffor (s in starts) { opt <-tryCatch(optim(s, obj, method ="L-BFGS-B",lower =c(log(1e-4), min(years) -20),upper =c(log(1), max(years))),error =function(e) NULL )if (!is.null(opt) && opt$value < best_sse) { best_sse <- opt$value best <- opt } } r <-exp(best$par[1]) t0 <- best$par[2]# year when p reaches target: t = t0 - log(1 - target/K) / r year_at <-function(target) t0 -log(1- target / K) / rlist(label = label, model ="Asymptotic (→100%)", r = r, t0 = t0, year_100 =Inf,year_99 =year_at(99), year_95 =year_at(95), year_90 =year_at(90),predict =function(y) K * (1-exp(-r * (y - t0))))}fit_logistic_to_100 <-function(years, pct, label, K =100) {# p = K / (1 + exp(-r * (t - t_mid))) obj <-function(par) { r <-exp(par[1]) t_mid <- par[2] pred <- K / (1+exp(-r * (years - t_mid)))sum((pct - pred)^2) }# Multiple starts — early-phase series are sensitive to initial midpoint starts <-list(c(log(0.2), mean(years)),c(log(0.15), max(years) +3),c(log(0.25), max(years) +5),c(log(0.1), max(years) +10),c(log(0.3), max(years)) ) best <-NULL best_sse <-Inffor (s in starts) { opt <-tryCatch(optim(s, obj, method ="L-BFGS-B",lower =c(log(1e-4), min(years) -5),upper =c(log(1), max(years) +80)),error =function(e) NULL )if (!is.null(opt) && opt$value < best_sse) { best_sse <- opt$value best <- opt } } r <-exp(best$par[1]) t_mid <- best$par[2] year_at <-function(target) t_mid -log(K / target -1) / rlist(label = label, model ="Logistic (K=100%)", r = r, t_mid = t_mid, sse = best_sse,year_50 =year_at(50), year_90 =year_at(90), year_95 =year_at(95),year_99 =year_at(99), year_100 =Inf,predict =function(y) K / (1+exp(-r * (y - t_mid))))}# Fit both resolutionsseg_lin <-fit_linear_to_100(cum_occupancy$Year, cum_occupancy$Seg_Pct, "0.1-mi segments")seg_asy <-fit_asymptotic_to_100(cum_occupancy$Year, cum_occupancy$Seg_Pct, "0.1-mi segments")seg_log <-fit_logistic_to_100(cum_occupancy$Year, cum_occupancy$Seg_Pct, "0.1-mi segments")mile_lin <-fit_linear_to_100(cum_occupancy$Year, cum_occupancy$Mile_Pct, "Whole river miles")mile_asy <-fit_asymptotic_to_100(cum_occupancy$Year, cum_occupancy$Mile_Pct, "Whole river miles")mile_log <-fit_logistic_to_100(cum_occupancy$Year, cum_occupancy$Mile_Pct, "Whole river miles")# Horizon years for plotlast_obs <-max(cum_occupancy$Year)# Recent-trajectory scenario: whole-mile slope over last ~2 years# (hypothesis: limited annual survey understates ubiquity; recent discovery rate continues)recent_miles <- cum_occupancy %>%filter(Year >= last_obs -2)mile_recent_fit <-lm(Mile_Pct ~ Year, data = recent_miles)mile_recent_rate <-coef(mile_recent_fit)[2]mile_recent_a <-coef(mile_recent_fit)[1]mile_recent_year_100 <-if (mile_recent_rate >0) (100- mile_recent_a) / mile_recent_rate elseInfmile_recent_predict <-function(y) pmin(100, pmax(0, mile_recent_a + mile_recent_rate * y))# Extend far enough to show approach to 100% for mile-scale logistic/linearhorizon_end <-max(ceiling(mile_lin$year_100) +2,ceiling(seg_lin$year_100) +2,ceiling(mile_log$year_99) +5,ceiling(seg_log$year_99) +5,ceiling(mile_recent_year_100) +2, last_obs +25)horizon_end <-min(horizon_end, last_obs +80) # cap plot rangeforecast_years <-seq(min(cum_occupancy$Year), horizon_end, by =1)# Build forecast table of milestone yearsmilestone_row <-function(fit_lin, fit_asy, fit_log, resolution) { year_lin <-function(target) { y <- (target - fit_lin$a) / fit_lin$bif (!is.finite(y) || fit_lin$b <=0) return(NA_real_) y }data.frame(Resolution = resolution,Metric =c("50%", "75%", "90%", "95%", "99%", "100% (linear only)"),Linear =round(c(year_lin(50), year_lin(75), year_lin(90), year_lin(95), year_lin(99), fit_lin$year_100), 0),Asymptotic =round(c( fit_asy$t0 -log(1-50/100) / fit_asy$r, fit_asy$t0 -log(1-75/100) / fit_asy$r, fit_asy$year_90, fit_asy$year_95, fit_asy$year_99,NA_real_ ), 0),Logistic =round(c( fit_log$year_50, fit_log$t_mid -log(100/75-1) / fit_log$r, fit_log$year_90, fit_log$year_95, fit_log$year_99,NA_real_ ), 0) )}milestones <-bind_rows(milestone_row(seg_lin, seg_asy, seg_log, "0.1-mi segments"),milestone_row(mile_lin, mile_asy, mile_log, "Whole river miles"))# Horizontal lag: years until segment occupancy catches a given mile occupancy level# (time to "infill" every 0.1-mi subsegment once whole miles reach that level)year_at_logistic <-function(fit, target, K =100) {if (target >= K) return(Inf)if (target <=0) return(-Inf) fit$t_mid -log(K / target -1) / fit$r}infill_levels <-c(50, 75, 90)infill_gaps <-data.frame(Occupancy =paste0(infill_levels, "%"),Year_WholeMile =round(vapply(infill_levels, function(p) year_at_logistic(mile_log, p), numeric(1)), 0),Year_Segment =round(vapply(infill_levels, function(p) year_at_logistic(seg_log, p), numeric(1)), 0)) %>%mutate(Lag_Years =round(Year_Segment - Year_WholeMile, 1),Interpretation =paste0("~", Lag_Years, " yr to fill 0.1-mi gaps after whole miles hit ", Occupancy) )# Model fit summary text (emitted after tables)fit_summary_md <-paste0("\n##### Model Fit Summary\n\n",sprintf("- Segment linear: +%.1f percentage points/year → 100%% in ~%.0f\n", seg_lin$b, seg_lin$year_100),sprintf("- Mile linear (full series): +%.1f percentage points/year → 100%% in ~%.0f\n", mile_lin$b, mile_lin$year_100),sprintf("- Mile recent trajectory (last 2 yr): +%.1f percentage points/year → 100%% in ~%.0f\n", mile_recent_rate, mile_recent_year_100),sprintf("- Segment logistic: midpoint (50%%) ~%.0f; 95%% ~%.0f\n", seg_log$year_50, seg_log$year_95),sprintf("- Mile logistic: midpoint (50%%) ~%.0f; 95%% ~%.0f\n", mile_log$year_50, mile_log$year_95),sprintf("\nCurrent cumulative occupancy (%.0f): %.1f%% of 0.1-mi segments; %.1f%% of whole river miles.\n", last_obs, cum_occupancy$Seg_Pct[cum_occupancy$Year == last_obs], cum_occupancy$Mile_Pct[cum_occupancy$Year == last_obs]))```#### Forecast to Corridor SaturationModels fit to cumulative occupancy and projected forward:- Linear — constant annual discovery rate; reaches 100% in finite time.- Asymptotic — approaches 100% with diminishing returns (classic discovery / sampling plateau).- Logistic — S-curve to a 100% carrying capacity; slows as unoccupied corridor shrinks.- Recent trajectory (whole miles) — slope from the last two survey years only; tests the hypothesis that pepperweed is already more ubiquitous than the full-series fit implies, and limited annual effort is still revealing occupied miles quickly.Pepperweed is already common on nearby ditches, streams, and upper-river reaches; these forecasts treat the LORP corridor as filling toward full known occupancy under continued survey and/or spread.```{r occupancy-forecast-tables}#| message: false#| warning: false#| results: 'asis'print(kable(milestones,col.names =c("Resolution", "Occupancy Milestone", "Linear", "Asymptotic", "Logistic"),align =c("l", "l", "c", "c", "c"),caption ="Projected calendar year when cumulative occupancy reaches each milestone (fit to 2018–2025 discovery)"))```##### Infill lag (whole mile → 0.1-mi segment)The horizontal gap between the blue (whole-mile) and red (0.1-mi) logistic curves is the years of additional discovery needed to occupy every subsegment once whole-mile occupancy has already reached that level.```{r occupancy-forecast-infill}#| message: false#| warning: false#| results: 'asis'print(kable(infill_gaps,col.names =c("Occupancy level", "Whole-mile year", "0.1-mi year", "Lag (yr)", "Interpretation"),align =c("c", "c", "c", "c", "l"),caption ="Time lag between whole-mile and subsegment occupancy (logistic forecasts)"))cat(fit_summary_md)``````{r occupancy-forecast-plot}#| message: false#| warning: false#| fig-width: 12#| fig-height: 7#| fig-alt: "Line chart of cumulative pepperweed occupancy over time for whole river miles and 0.1-mile segments, with linear, logistic, and recent-trajectory forecasts toward 100 percent known occupancy."plot_df_obs <- cum_occupancy %>%select(Year, Seg_Pct, Mile_Pct) %>%pivot_longer(-Year, names_to ="Series", values_to ="Pct") %>%mutate(Series =recode(Series, Seg_Pct ="0.1-mi segments (observed)", Mile_Pct ="Whole miles (observed)"),Type ="Observed")make_forecast_df <-function(years, pred_fn, series_name) {data.frame(Year = years, Pct =pred_fn(years), Series = series_name, Type ="Forecast")}# Recent scenario only from last observed year forward (avoid rewriting history)recent_years <-seq(last_obs, max(horizon_end, ceiling(mile_recent_year_100) +1), by =1)plot_df_fc <-bind_rows(make_forecast_df(forecast_years, seg_lin$predict, "0.1-mi segments — linear"),make_forecast_df(forecast_years, seg_log$predict, "0.1-mi segments — logistic"),make_forecast_df(forecast_years, mile_lin$predict, "Whole miles — linear"),make_forecast_df(forecast_years, mile_log$predict, "Whole miles — logistic"),make_forecast_df(recent_years, mile_recent_predict, "Whole miles — recent trajectory"))# Annotation segments for infill lag at selected occupancy levelsannot_levels <-c(50, 75)annot_df <-data.frame(Pct = annot_levels,x_mile =vapply(annot_levels, function(p) year_at_logistic(mile_log, p), numeric(1)),x_seg =vapply(annot_levels, function(p) year_at_logistic(seg_log, p), numeric(1))) %>%mutate(lag = x_seg - x_mile,label =sprintf("+%.0f yr to fill\n0.1-mi gaps", lag),x_mid = (x_mile + x_seg) /2 )forecast_plot <-ggplot() +geom_hline(yintercept =100, linetype ="dotted", color ="grey50") +geom_hline(yintercept = annot_levels, linetype ="dotted", color ="grey80") +# Infill-lag brackets (horizontal distance between logistic curves)geom_segment(data = annot_df,aes(x = x_mile, xend = x_seg, y = Pct, yend = Pct),color ="#5d6d7e",linewidth =0.7,arrow =arrow(ends ="both", length =unit(0.12, "inches")) ) +geom_text(data = annot_df,aes(x = x_mid, y = Pct, label = label),vjust =-0.4,size =3.2,color ="#34495e",fontface ="italic",lineheight =0.95 ) +geom_line(data = plot_df_fc %>%filter(grepl("linear", Series) &!grepl("recent", Series)),aes(x = Year, y = Pct, color = Series), linewidth =0.9, linetype ="dashed") +geom_line(data = plot_df_fc %>%filter(grepl("logistic", Series)),aes(x = Year, y = Pct, color = Series), linewidth =0.9, linetype ="solid") +geom_line(data = plot_df_fc %>%filter(grepl("recent", Series)),aes(x = Year, y = Pct, color = Series), linewidth =1.15, linetype ="dotdash") +geom_point(data = plot_df_obs, aes(x = Year, y = Pct, color = Series), size =2.5) +geom_line(data = plot_df_obs, aes(x = Year, y = Pct, color = Series), linewidth =1.1) +# Mark recent-scenario 100% year {if (is.finite(mile_recent_year_100) && mile_recent_year_100 <= horizon_end) {list(geom_vline(xintercept = mile_recent_year_100, linetype ="dotted", color ="#6c3483", alpha =0.7),annotate("text",x = mile_recent_year_100,y =8,label =sprintf("Recent traj.\n→ 100%% ~%.0f", mile_recent_year_100),hjust =-0.05,size =3,color ="#6c3483" ) ) } else {list() } } +scale_y_continuous(limits =c(0, 105), breaks =seq(0, 100, 25),labels =function(x) paste0(x, "%")) +scale_x_continuous(breaks =pretty(forecast_years, n =8)) +scale_color_manual(values =c("0.1-mi segments (observed)"="#c0392b","Whole miles (observed)"="#1a5276","0.1-mi segments — linear"="#e74c3c","0.1-mi segments — logistic"="#922b21","Whole miles — linear"="#3498db","Whole miles — logistic"="#1a5276","Whole miles — recent trajectory"="#6c3483" )) +labs(title ="Cumulative Pepperweed Occupancy — Observed and Forecast to 100%",subtitle ="Solid = logistic; dashed = full-series linear; dot-dash = recent 2-yr whole-mile trajectory\nGrey brackets = years to fill 0.1-mi subsegments after whole miles reach that occupancy",x ="Year",y ="Cumulative occupancy",color =NULL ) +theme_minimal(base_size =13) +theme(legend.position ="bottom",legend.direction ="vertical",plot.title =element_text(face ="bold"),plot.subtitle =element_text(size =10),panel.grid.minor =element_blank() ) +guides(color =guide_legend(ncol =2))print(forecast_plot)```#### Interpretation`r paste0("- Whole-mile occupancy is already high (~", cum_occupancy$Mile_Pct[cum_occupancy$Year == last_obs], "% known), consistent with coarse-scale presence along much of the corridor.")``r paste0("- 0.1-mile occupancy remains lower (~", cum_occupancy$Seg_Pct[cum_occupancy$Year == last_obs], "%). Annotated horizontal gaps estimate years of additional surveying or local fill before every subsegment is known occupied at the same occupancy level.")``r sprintf("- Recent-trajectory scenario: using only %.0f–%.0f whole-mile discovery (+%.1f pp/yr), cumulative mile occupancy reaches 100%% around %.0f. This upper-bound case assumes the last two years’ discovery pace continues.", min(recent_miles$Year), max(recent_miles$Year), mile_recent_rate, mile_recent_year_100)`- Full-series linear and logistic forecasts are slower and more consistent with a sampling or pulse–plateau process; the recent trajectory is not a separate biological invasion model.- Flood years with limited access (e.g., 2023) can depress annual detections while cumulative occupancy remains unchanged; subsequent accessible seasons may show discovery pulses.- Increased RAS effort in currently empty miles (see Monitoring applications) accelerates cumulative discovery even without new colonization.- These forecasts intentionally do not attribute residuals to herbicide intensity [@young1998; @renz2006; @ucipm2024].#### Step-pulse scenarios (fast vs low spread)Smooth curves average across hydrologic structure. Flood years 2017 and 2023 limited RAS access; large *discovery* steps followed in 2019 and 2025. The fast step-pulse scenario assumes that ~6-year flood/discovery rhythm continues, with between-pulse drift equal to the mean of all non-pulse years. The low-spread scenario relaxes pulse frequency to 12 years and uses quieter between-pulse drift (years with ≤3 new whole miles)—in case 2017–2023 was an unusually wet cluster.```{r step-pulse-forecast}#| message: false#| warning: false#| results: 'asis'#| fig-width: 11#| fig-height: 7.5#| fig-alt: "Step-pulse forecasts of cumulative pepperweed occupancy for whole river miles and 0.1-mile segments under fast 6-year and low-spread 12-year scenarios, compared with linear reference and observed 2018–2025 points. Flood years 2017 and 2023 marked."obs <- cum_occupancy %>%arrange(Year) %>%transmute( Year, Seg_Pct, Mile_Pct, New_Miles,New_Seg = New_Segments )discovery_pulse <-c(2019, 2025)flood_years <-c(2017, 2023)mile_d <-diff(obs$Mile_Pct)seg_d <-diff(obs$Seg_Pct)years_end <- obs$Year[-1]new_miles_end <- obs$New_Miles[-1]mile_pulse_jump <-mean(mile_d[years_end %in% discovery_pulse])seg_pulse_jump <-mean(seg_d[years_end %in% discovery_pulse])fast_period <-6Lmile_plateau_fast <-mean(mile_d[!(years_end %in% discovery_pulse)])seg_plateau_fast <-mean(seg_d[!(years_end %in% discovery_pulse)])low_period <-12Lquiet <-!(years_end %in% discovery_pulse) & (new_miles_end <=3)mile_plateau_low <-mean(mile_d[quiet])seg_plateau_low <-mean(seg_d[quiet])step_project <-function(y0, p0, plateau, pulse_jump, last_pulse_year, period) { out <-numeric(length(y0)) out[1] <- p0for (i in2:length(y0)) { y <- y0[i] is_pulse <- ((y - last_pulse_year) %% period ==0) && (y > last_pulse_year) out[i] <-min(100, out[i -1] +if (is_pulse) pulse_jump else plateau) } out}last_obs_yr <-max(obs$Year)p0_mile <- obs$Mile_Pct[obs$Year == last_obs_yr]p0_seg <- obs$Seg_Pct[obs$Year == last_obs_yr]proj_years <- last_obs_yr:min(last_obs_yr +20, 2045)mile_fast <-step_project(proj_years, p0_mile, mile_plateau_fast, mile_pulse_jump, last_obs_yr, fast_period)mile_low <-step_project(proj_years, p0_mile, mile_plateau_low, mile_pulse_jump, last_obs_yr, low_period)seg_fast <-step_project(proj_years, p0_seg, seg_plateau_fast, seg_pulse_jump, last_obs_yr, fast_period)seg_low <-step_project(proj_years, p0_seg, seg_plateau_low, seg_pulse_jump, last_obs_yr, low_period)mile_lin_ref <-pmin(100, p0_mile + mile_lin$b * (proj_years - last_obs_yr))pred_2026_fast_m <-min(100, p0_mile + mile_plateau_fast)pred_2026_low_m <-min(100, p0_mile + mile_plateau_low)pred_2026_fast_s <-min(100, p0_seg + seg_plateau_fast)pred_2026_low_s <-min(100, p0_seg + seg_plateau_low)fc <-bind_rows(data.frame(Year = proj_years, Pct = mile_fast, Series ="Miles — fast step-pulse (6 yr)", Kind ="step"),data.frame(Year = proj_years, Pct = mile_low, Series ="Miles — low step-pulse (12 yr)", Kind ="step"),data.frame(Year = proj_years, Pct = seg_fast, Series ="0.1-mi — fast step-pulse (6 yr)", Kind ="step"),data.frame(Year = proj_years, Pct = seg_low, Series ="0.1-mi — low step-pulse (12 yr)", Kind ="step"),data.frame(Year = proj_years, Pct = mile_lin_ref, Series ="Miles — linear (reference)", Kind ="linear"))obs_long <-bind_rows(data.frame(Year = obs$Year, Pct = obs$Mile_Pct, Series ="Whole miles (observed)"),data.frame(Year = obs$Year, Pct = obs$Seg_Pct, Series ="0.1-mi segments (observed)"))marks <-bind_rows(data.frame(Year = flood_years, Lty ="flood"),data.frame(Year = discovery_pulse, Lty ="discovery"),data.frame(Year = last_obs_yr + fast_period *1:3, Lty ="fast"),data.frame(Year = last_obs_yr + low_period, Lty ="low"))step_pulse_plot <-ggplot() +geom_vline(data = marks %>%filter(Lty =="flood"),aes(xintercept = Year), linetype ="longdash", color ="#2874a6", alpha =0.7 ) +geom_vline(data = marks %>%filter(Lty =="discovery"),aes(xintercept = Year), linetype ="dotted", color ="grey40" ) +geom_vline(data = marks %>%filter(Lty =="fast"),aes(xintercept = Year), linetype ="dotted", color ="#6c3483", alpha =0.45 ) +geom_vline(data = marks %>%filter(Lty =="low"),aes(xintercept = Year), linetype ="dotdash", color ="#1e8449", alpha =0.55 ) +geom_line(data = fc %>%filter(Kind =="linear"),aes(Year, Pct, color = Series), linewidth =0.85, linetype ="dashed" ) +geom_step(data = fc %>%filter(Kind =="step"),aes(Year, Pct, color = Series), linewidth =1.05, direction ="hv" ) +geom_point(data = obs_long, aes(Year, Pct, color = Series), size =2.6) +geom_line(data = obs_long, aes(Year, Pct, color = Series), linewidth =1.0) +annotate("text", x =2017.15, y =98, label ="Flood\n2017", hjust =0, size =3.0,color ="#2874a6", lineheight =0.9) +annotate("text", x =2023.15, y =98, label ="Flood\n2023", hjust =0, size =3.0,color ="#2874a6", lineheight =0.9) +scale_y_continuous(limits =c(0, 105), breaks =seq(0, 100, 25),labels =function(x) paste0(x, "%") ) +scale_x_continuous(breaks =seq(2016, max(proj_years), 4)) +scale_color_manual(values =c("0.1-mi segments (observed)"="#c0392b","Whole miles (observed)"="#1a5276","Miles — linear (reference)"="#85929e","Miles — fast step-pulse (6 yr)"="#6c3483","Miles — low step-pulse (12 yr)"="#1e8449","0.1-mi — fast step-pulse (6 yr)"="#922b21","0.1-mi — low step-pulse (12 yr)"="#196f3d" )) +labs(title ="Step-pulse occupancy scenarios — fast vs low spread",subtitle =paste0("Fast: 6-yr pulses + mean non-pulse drift; Low: 12-yr pulses + quiet-year drift. ",sprintf("2026 plateau preds — miles %.1f%% (fast) / %.1f%% (low); segments %.1f%% / %.1f%%.", pred_2026_fast_m, pred_2026_low_m, pred_2026_fast_s, pred_2026_low_s) ),x ="Year",y ="Cumulative occupancy",color =NULL,caption ="Blue long-dash = flood years (limited RAS access). Dotted = discovery pulses and projected fast pulses." ) +theme_minimal(base_size =13) +theme(legend.position ="bottom",legend.direction ="vertical",panel.grid.minor =element_blank(),plot.title =element_text(face ="bold"),plot.caption =element_text(size =9, hjust =0, color ="grey30") ) +guides(color =guide_legend(ncol =2))print(step_pulse_plot)cat("\n\n### Calibrated rates\n\n")cat(sprintf("- Fast plateau drift: +%.2f pp/yr (miles), +%.2f pp/yr (0.1-mi); pulse jumps +%.2f / +%.2f pp; next discovery pulse ~%d.\n", mile_plateau_fast, seg_plateau_fast, mile_pulse_jump, seg_pulse_jump, last_obs_yr + fast_period))cat(sprintf("- Low plateau drift: +%.2f pp/yr (miles), +%.2f pp/yr (0.1-mi); next discovery pulse ~%d.\n", mile_plateau_low, seg_plateau_low, last_obs_yr + low_period))cat(sprintf("- Locked-style 2026 one-step (non-pulse year): miles %.1f%% (fast) vs %.1f%% (low); segments %.1f%% vs %.1f%%.\n\n", pred_2026_fast_m, pred_2026_low_m, pred_2026_fast_s, pred_2026_low_s))```## River-mile patterns {#river-mile-analysis}```{r river-mile-analysis}#| message: false#| warning: false#| results: 'asis'#| fig-width: 16#| fig-height: 12#| fig-alt: "Faceted bar charts of pepperweed site counts by whole river mile (0 to 60) for each survey year along the LORP corridor."# Read river mile shapefilerivermiles <-st_read('data/LORP_RiverMiles_revised.shp', quiet =TRUE)# Convert NAME to numeric for proper sortingrivermiles <- rivermiles %>%mutate(RiverMile =as.numeric(NAME))# Ensure both datasets have the same CRS (WGS84)rivermiles <-st_transform(rivermiles, st_crs(pepper_data))# Perform spatial join to find closest river mile for each pepperweed sitepepper_data_with_rivermiles <- pepper_data %>%st_join(rivermiles, join = st_nearest_feature) %>%select(-LAYER, -sym, -gpxx.Displ, -RiverMile) %>%# Remove unnecessary columns and duplicate RiverMilerename(RiverMile = NAME)# Create summary data for faceted histogram - bin river miles into whole milesriver_mile_summary <- pepper_data_with_rivermiles %>%st_drop_geometry() %>%mutate(RiverMile_Binned =floor(as.numeric(RiverMile))) %>%# Bin to whole milesgroup_by(Year_Display, RiverMile_Binned) %>%summarise(Site_Count =n(), .groups ='drop') %>%# Ensure all year-rivermile combinations are represented (extend to mile 60)complete(Year_Display =min(Year_Display):max(Year_Display), RiverMile_Binned =0:60, fill =list(Site_Count =0))# Calculate actual reach boundaries from pepperweed data# Get the minimum river mile for each reach (northern boundary)pepper_data_with_reaches_and_miles <- pepper_data %>%st_join(rivermiles, join = st_nearest_feature) %>%st_join(river_reaches, join = st_nearest_feature) %>%mutate(RiverMile =as.numeric(NAME)) %>%filter(!is.na(Name)) # Only sites with reach assignmentsreach_boundaries <- pepper_data_with_reaches_and_miles %>%st_drop_geometry() %>%group_by(Name) %>%summarise(Min_RiverMile =min(RiverMile, na.rm =TRUE),Max_RiverMile =max(RiverMile, na.rm =TRUE),.groups ='drop' ) %>%mutate(Reach_Num =as.numeric(gsub("LORP Reach ", "", Name)),# Use minimum river mile as the northern boundary (top of reach)Label_Position = Min_RiverMile ) %>%# Sort by minimum river mile to ensure proper ordering (R1 at top, R6 at bottom)arrange(Min_RiverMile) %>%# Reassign reach numbers based on actual river mile order (1 at top, 6 at bottom)mutate(Reach_Num =row_number()) %>%# Create proper reach namesmutate(Name =paste0("LORP Reach ", Reach_Num))# Display the reach boundaries tablecat("### Reach boundaries (northern)\n\n")print(kable(reach_boundaries, col.names =c("LORP Reach", "Min River Mile", "Max River Mile", "Reach Number", "Label Position"),align =c("l", "c", "c", "c", "c"),caption ="Northern boundaries (minimum river miles) for each LORP reach"))# Create a simple table showing labels and river milesreach_labels_table <- reach_boundaries %>%select(Name, Label_Position) %>%arrange(Label_Position)# Create faceted histogramif(nrow(river_mile_summary) >0) {# Calculate consistent x-axis limits across all years max_sites <-max(river_mile_summary$Site_Count, na.rm =TRUE) x_limit <-ceiling(max_sites *1.1) # Add 10% padding# Cap sites at 20 for better small site visibility and reverse y-axis (0 at top) river_mile_summary_capped <- river_mile_summary %>%mutate(Site_Count_Capped =pmin(Site_Count, 20),RiverMile_Binned =factor(RiverMile_Binned, levels =sort(unique(RiverMile_Binned), decreasing =TRUE)))# Reach northern boundaries for plot overlays (whole-mile bins) reach_boundaries_plot <-data.frame(Name =c("Reach 1", "Reach 2", "Reach 3", "Reach 4", "Reach 5", "Reach 6"),Closest_Bin =c("0", "4", "20", "35", "39", "50") ) river_plot <-ggplot(river_mile_summary_capped, aes(x = Site_Count_Capped, y =factor(RiverMile_Binned))) +geom_col(fill ="red", alpha =0.8, width =0.7) +# Add horizontal lines for reach boundaries using closest binned valuesgeom_hline(data = reach_boundaries_plot, aes(yintercept = Closest_Bin), color ="blue", linetype ="solid", alpha =0.8, size =1) +# Add reach labels - one per reach, positioned below the lines and well within visible areageom_text(data = reach_boundaries_plot %>%mutate(Year_Display =2018), # Only show labels on 2018 panelaes(x =10, y = Closest_Bin, label = Name), hjust =0, vjust =1.5, color ="blue", size =6, fontface ="bold") +facet_wrap(~ Year_Display, ncol =8, scales ="fixed") +# Single row, fixed scalesscale_x_continuous(limits =c(0, 20), breaks =c(0, 5, 10, 15, 20)) +# Cap at 20scale_y_discrete(breaks =function(x) x[seq(1, length(x), by =5)]) +# Every 5 mileslabs(title ="Pepperweed Sites by River Mile and Year",subtitle ="Distribution of sites along the LORP river corridor (binned by mile, capped at 20 sites)",x ="Number of Sites (capped at 20)",y ="River Mile" ) +theme_minimal() +theme(plot.title =element_text(size =22, face ="bold", color ="#2E86AB"),plot.subtitle =element_text(size =18, color ="#666666"),axis.title =element_text(size =16, face ="bold"),axis.text.y =element_text(size =14),axis.text.x =element_text(size =14),strip.text =element_text(size =16, face ="bold"),panel.grid.minor =element_blank(),panel.grid.major.y =element_blank(),plot.margin =margin(25, 25, 25, 25) # Add margin for better spacing )print(river_plot)# Summary statisticscat("\n\n### River mile distribution summary\n\n")cat("- Total River Miles Analyzed:", length(unique(river_mile_summary$RiverMile_Binned)), "\n")cat("- Years Covered:", min(river_mile_summary$Year_Display), "-", max(river_mile_summary$Year_Display), "\n")cat("- River Mile Range:", min(river_mile_summary$RiverMile_Binned), "-", max(river_mile_summary$RiverMile_Binned), "\n")# Most active river miles top_river_miles <- river_mile_summary %>%group_by(RiverMile_Binned) %>%summarise(Total_Sites =sum(Site_Count), .groups ='drop') %>%arrange(desc(Total_Sites)) %>%head(10)cat("\n\n### Top 10 most active river miles\n\n")for(i in1:nrow(top_river_miles)) {cat("- River Mile ", top_river_miles$RiverMile_Binned[i], ": ", top_river_miles$Total_Sites[i], " sites\n", sep ="") }cat("\n")} else {cat("\n\nNo river mile data available for analysis.\n\n")}```### Cumulative known occupancy at 0.1-mi resolutionAnnual site maps can show “gaps” in later years where crews skip known dense or treated stands. The heatmap below is cumulative: once a 0.1-mi corridor segment is detected, it remains colored in subsequent years (fill = maximum annual site count observed through that year). White = never detected by that year. Only sites within 250 m of the river-mile centerline are included.```{r river-mile-01-heatmap}#| message: false#| warning: false#| fig-width: 9#| fig-height: 11#| fig-alt: "Heatmap of cumulative known pepperweed occupancy along the LORP corridor at 0.1-mile resolution by year from 2018 to 2025. White cells mean never detected; red intensity is cumulative maximum sites per segment. Blue lines mark LORP reach northern boundaries."if (!exists("rivermiles")) { rivermiles <-st_read("data/LORP_RiverMiles_revised.shp", quiet =TRUE) %>%mutate(RiverMile =as.numeric(NAME)) %>%st_transform(st_crs(pepper_data))}if (!exists("pepper_data_with_rivermiles")) { pepper_data_with_rivermiles <- pepper_data %>%st_join(rivermiles %>%select(NAME), join = st_nearest_feature) %>%rename(RiverMile = NAME)}rm_utm <-st_transform(rivermiles, 32611)pepper_utm <-st_transform(pepper_data_with_rivermiles, 32611)nn_idx <-st_nearest_feature(pepper_utm, rm_utm)corridor_dist_m <-as.numeric(st_distance(pepper_utm, rm_utm[nn_idx, ], by_element =TRUE))pepper_hm <- pepper_data_with_rivermiles %>%mutate(dist_m = corridor_dist_m, RiverMile =as.numeric(RiverMile)) %>%filter(dist_m <=250, !is.na(RiverMile), RiverMile >=0, RiverMile <60) %>%mutate(Mile_01 =floor(RiverMile /0.1) *0.1)years_hm <-seq(params$start_year, params$current_year)mile_grid <-seq(0, 59.9, by =0.1)cap_used <-10Lsummary_01 <- pepper_hm %>%st_drop_geometry() %>%group_by(Year_Display, Mile_01) %>%summarise(Site_Count =n(), .groups ="drop") %>%complete(Year_Display = years_hm, Mile_01 = mile_grid, fill =list(Site_Count =0)) %>%arrange(Mile_01, Year_Display) %>%group_by(Mile_01) %>%mutate(Cum_Max_Sites =cummax(Site_Count)) %>%ungroup() %>%mutate(Site_Fill =ifelse(Cum_Max_Sites ==0, NA_real_, pmin(Cum_Max_Sites, cap_used)))reach_bounds <-data.frame(Label =c("Reach 1", "Reach 2", "Reach 3", "Reach 4", "Reach 5", "Reach 6"),Mile =c(0, 4, 20, 35, 39, 50))heatmap_01 <-ggplot(summary_01, aes(x = Year_Display, y = Mile_01, fill = Site_Fill)) +geom_tile(width =0.92, height =0.1, color =NA) +geom_hline(data = reach_bounds, aes(yintercept = Mile),color ="#1a5276", linewidth =0.55, alpha =0.95 ) +geom_text(data = reach_bounds,aes(x = params$start_year -0.45, y = Mile, label = Label),inherit.aes =FALSE,hjust =0, vjust =-0.35, color ="#1a5276", size =3.4, fontface ="bold" ) +scale_y_reverse(breaks =seq(0, 60, by =5),expand =expansion(mult =c(0.005, 0.02)) ) +scale_x_continuous(breaks = years_hm,expand =expansion(add =c(0.85, 0.35)) ) +scale_fill_gradient(name ="Cum. max sites\n/ 0.1-mi\n(capped)",low ="#e74c3c",high ="#4a0e0e",limits =c(1, cap_used),breaks =c(1, 5, 10),na.value ="white" ) +labs(title ="Cumulative known occupancy at 0.1-mi resolution",subtitle ="Once detected, a segment stays colored (avoids false gaps from skipping known/treated stands)",x ="Year",y ="River mile",caption =paste0("White = never detected by that year. Blue lines = LORP reach northern boundaries. ","Corridor filter: ≤250 m from centerline." ) ) +theme_minimal(base_size =13) +theme(panel.background =element_rect(fill ="white", color =NA),panel.grid.major.y =element_line(color ="grey90", linewidth =0.3),panel.grid.minor.y =element_blank(),panel.grid.major.x =element_blank(),legend.position ="right",plot.title =element_text(face ="bold"),plot.caption =element_text(size =9, hjust =0, color ="grey30") )print(heatmap_01)```## Monitoring applications (2026 RAS)The [FY 2026–2027 LORP work plan](https://inyowater.org/wp-content/uploads/2026/06/2026-2027-LORP-Work-Plan-and-Budget_draft_062226.pdf) directs summer RAS monitoring toward areas without known pepperweed populations, consistent with prioritizing satellite populations and leading edges [@renz2012]. Tables below use `r params$start_year`–`r params$current_year` corridor detections binned to whole river miles (see [River-mile patterns](#river-mile-analysis)).### Priority Survey AreasRiver-mile segments with no pepperweed detections on the river corridor in any survey year (`r params$start_year`–`r params$current_year`). Only sites within 250 m of a river-mile marker count toward occupancy (off-channel ditch/field points are excluded so they do not falsely mark a mile as occupied). These align with the workplan objective to survey unoccupied corridor.```{r monitoring-recommendations}#| message: false#| warning: false#| results: 'asis'# Ensure river-mile assignments existif (!exists("pepper_data_with_rivermiles")) {if (!exists("rivermiles")) { rivermiles <-st_read("data/LORP_RiverMiles_revised.shp", quiet =TRUE) %>%mutate(RiverMile =as.numeric(NAME)) %>%st_transform(st_crs(pepper_data)) } pepper_data_with_rivermiles <- pepper_data %>%st_join(rivermiles %>%select(NAME), join = st_nearest_feature) %>%rename(RiverMile = NAME)}if (!exists("river_reaches")) { reach_url <-"https://inyocounty.maps.arcgis.com/sharing/rest/content/items/90e5870bd5914a928bd97b023f07b807/data" river_reaches <-st_read(reach_url, quiet =TRUE) %>%filter(Name %in%paste0("LORP Reach ", 1:6)) %>%st_transform(st_crs(pepper_data))}mile_range <-0:60corridor_max_m <-250# exclude off-channel sites snapped to a river mile from far away# Distance to nearest river-mile marker (meters)rm_utm <-st_transform(rivermiles, 32611)pepper_utm <-st_transform(pepper_data_with_rivermiles, 32611)nn_idx <-st_nearest_feature(pepper_utm, rm_utm)corridor_dist_m <-as.numeric(st_distance(pepper_utm, rm_utm[nn_idx, ], by_element =TRUE))pepper_corridor <- pepper_data_with_rivermiles %>%mutate(RiverMile_Binned =floor(as.numeric(RiverMile)),dist_m = corridor_dist_m ) %>%filter(dist_m <= corridor_max_m)detected_miles <- pepper_corridor %>%st_drop_geometry() %>%distinct(RiverMile_Binned) %>%pull(RiverMile_Binned)never_detected_miles <-setdiff(mile_range, detected_miles)group_contiguous_miles <-function(miles) {if (length(miles) ==0) {return(data.frame()) } miles <-sort(miles) starts <- miles[c(1, which(diff(miles) >1) +1)] ends <- miles[c(which(diff(miles) >1), length(miles))]data.frame(RM_Start = starts, RM_End = ends)}survey_segments <-group_contiguous_miles(never_detected_miles)# Assign reach from mile-marker polygonsmile_reach_lookup <- rivermiles %>%st_join(river_reaches %>%select(Name), join = st_nearest_feature) %>%st_drop_geometry() %>%select(RiverMile, Name)get_reach_for_mile <-function(mile) { idx <-which.min(abs(mile_reach_lookup$RiverMile - mile))gsub("LORP Reach ", "R", mile_reach_lookup$Name[idx])}if (nrow(survey_segments) >0) { survey_segments <- survey_segments %>%mutate(RM_Range =ifelse(RM_Start == RM_End,paste0("RM ", RM_Start),paste0("RM ", RM_Start, "–", RM_End)),Length_Miles = RM_End - RM_Start +1,Reach =vapply((RM_Start + RM_End) /2, get_reach_for_mile, character(1)),Priority =case_when( Length_Miles >=5~"High", Length_Miles >=2~"Medium",TRUE~"Low" ),Survey_Note ="No known populations (all years)",Bank_Guidance ="Survey both banks" ) %>%arrange(desc(Length_Miles), RM_Start) survey_table <- survey_segments %>%select(Priority, RM_Range, Length_Miles, Reach, Survey_Note, Bank_Guidance)print(kable(survey_table,col.names =c("Priority", "River Mile Range", "Length (mi)", "Reach", "Population Status", "Bank Guidance"),align =c("l", "l", "c", "c", "l", "l"),caption =paste0("Recommended 2026 RAS survey segments without known pepperweed (n = ",length(never_detected_miles), " river miles, ",round(100*length(never_detected_miles) /length(mile_range), 1), "% of 0–60 RM corridor)")))} else {cat("All river miles in the 0–60 corridor have at least one historical detection.\n")}````r if (exists("never_detected_miles") && length(never_detected_miles) > 0) paste0("Summary: ", length(never_detected_miles), " of ", length(mile_range), " river miles (", round(100 * length(never_detected_miles) / length(mile_range), 1), "%) have no recorded pepperweed on the corridor (within 250 m of a river-mile marker).")`### East / West Bank Planning NotesBank side for planning is taken from each site’s coordinates relative to the river centerline (local channel direction at each river mile), which matches how east/west can be derived from XY even when a legacy bank attribute exists on older forms. Sites are classified as east or west of the centerline for gap targeting.- Unoccupied miles (table above): survey both banks — absence of detections does not indicate which bank was visited.- Partially occupied miles (table below): where all detections fall on one bank, the opposite bank may warrant targeted survey even though populations are known nearby.```{r monitoring-one-bank}#| message: false#| warning: false#| results: 'asis'rivermiles_xy <- rivermiles %>%mutate(lon =st_coordinates(geometry)[, 1],lat =st_coordinates(geometry)[, 2] ) %>%st_drop_geometry() %>%arrange(RiverMile)get_bank <-function(site_lon, site_lat, mile) { idx <-which(rivermiles_xy$RiverMile == mile)if (length(idx) ==0) {return(NA_character_) } idx <- idx[1]if (idx <nrow(rivermiles_xy)) { dx <- rivermiles_xy$lon[idx +1] - rivermiles_xy$lon[idx] dy <- rivermiles_xy$lat[idx +1] - rivermiles_xy$lat[idx] } else { dx <- rivermiles_xy$lon[idx] - rivermiles_xy$lon[idx -1] dy <- rivermiles_xy$lat[idx] - rivermiles_xy$lat[idx -1] } vx <- site_lon - rivermiles_xy$lon[idx] vy <- site_lat - rivermiles_xy$lat[idx] cross <- dx * vy - dy * vxifelse(cross >0, "East", "West")}site_coords <-st_coordinates(pepper_corridor)bank_summary <- pepper_corridor %>%st_drop_geometry() %>%mutate(site_lon = site_coords[, 1],site_lat = site_coords[, 2],Bank =mapply(get_bank, site_lon, site_lat, RiverMile_Binned) ) %>%group_by(RiverMile_Binned) %>%summarise(Sites =n(),East =sum(Bank =="East", na.rm =TRUE),West =sum(Bank =="West", na.rm =TRUE),.groups ="drop" ) %>%mutate(Banks_Detected =case_when( East >0& West >0~"Both", East >0~"East only", West >0~"West only",TRUE~"Unknown" ),Opposite_Bank_To_Survey =case_when( East >0& West ==0~"West", West >0& East ==0~"East",TRUE~NA_character_ ),Reach =vapply(RiverMile_Binned, get_reach_for_mile, character(1)) )one_bank_gaps <- bank_summary %>%filter(!is.na(Opposite_Bank_To_Survey)) %>%arrange(RiverMile_Binned) %>%mutate(RM_Range =paste0("RM ", RiverMile_Binned),Planning_Note =paste0("Detections on ", tolower(Banks_Detected), "; consider ", Opposite_Bank_To_Survey, " bank") ) %>%select(RM_Range, Reach, Sites, Banks_Detected, Opposite_Bank_To_Survey, Planning_Note)if (nrow(one_bank_gaps) >0) {print(kable(one_bank_gaps,col.names =c("River Mile", "Reach", "Sites", "Banks w/ Detections", "Bank to Survey", "Planning Note"),align =c("l", "c", "c", "l", "l", "l"),caption ="River miles with detections on a single bank only (2018–2025)"))} else {cat("No single-bank detection patterns were identified.\n")}```### Suggested 2026 Survey Sequence1. Reach 6 lower corridor (RM 43–48, 51–52, 54–60) — largest unoccupied segments; highest workplan priority.2. Reach 3 gap miles (RM 21–22, 24) — short unoccupied segments within otherwise occupied reach; efficient to cover during Reach 3 transit.3. Single-bank follow-up — optional targeted passes on the undetected bank at miles listed above, if crew capacity allows.### Access and Route Planning (next step)RAS is on foot (no boats). In a dry year, crews will mostly follow dry banks, levees, roads, and known walking routes rather than secondary-channel networks.Field Maps navigation (preferred for split crews): use the east/west gap lines exported below — parallel to the mainstem, drawn only where that bank has no corridor detections for ≥400 m (0.1-mi bins dissolved). Five+ people on different routes each open the same web map / offline area; they do not each need Tracker licenses (those are only for recording tracks).| Bank | Color | Pattern | Meaning || ---| ---| ---| ---|| East | Sky blue `#0077BB`| Solid | Walk/survey the east side — no detections on that bank for this stretch || West | Orange `#EE7733`| Dashed | Walk/survey the west side — no detections on that bank for this stretch |Colors follow a colorblind-safe pair; pattern differs too so meaning is not color-only (ADA). If detections exist only on the west, the east line still draws for that stretch.Optional tracks (handheld GPS or phone GPX) still help refine entry points later; do not block planning on them. Use the river-mile tables plus these gap lines until then.### Field Maps Navigation LayerEast/west gap lines = polylines parallel to the river-mile centerline (±50 m), one color/pattern per bank, only where that bank has no pepperweed detections in the 250 m corridor for a contiguous stretch of ≥400 m (built from 0.1-mi segments). If only the west bank has detections, the east line still appears for that reach. The [multithreaded river](https://services.arcgis.com/0jRlQ17Qmni5zEMr/arcgis/rest/services/owens_river_feature/FeatureServer) is included as a reference layer.*Draft for Field Maps — review against the river-mile tables before treating as final.*```{r survey-nav-export}#| message: false#| warning: false#| results: 'asis'export_script <-"export_survey_nav.R"if (file.exists(export_script)) { status <-system2("Rscript", export_script, stdout =TRUE, stderr =TRUE)dir.create("docs/exports", showWarnings =FALSE, recursive =TRUE)for (f inc("ras_2026_survey_nav.geojson","ras_2026_survey_nav_shp.zip","owens_river_channels.geojson","lorp_rivermile_centerline.geojson" )) { src <-file.path("exports/survey_nav_2026", f)if (file.exists(src)) {file.copy(src, file.path("docs/exports", f), overwrite =TRUE) } }}nav_path <-"exports/survey_nav_2026/ras_2026_survey_nav.geojson"if (file.exists(nav_path)) { nav <- sf::st_read(nav_path, quiet =TRUE) nav_summary <- nav %>% sf::st_drop_geometry() %>%count(Bank, LineStyle, name ="Lines") %>%arrange(Bank) total_mi <-round(sum(nav$Length_mi, na.rm =TRUE), 1)cat("<div class=\"download-section\">\n")cat("<h3>Download — east/west gap survey lines</h3>\n")cat("<p>", nrow(nav), " lines (~", total_mi, " mi): parallel to the mainstem on bank sides with no corridor pepperweed detections for ≥400 m. East = solid sky blue; West = dashed orange.</p>\n", sep ="")cat("<div class=\"download-links\">\n")cat("<a href=\"exports/ras_2026_survey_nav_shp.zip\">Shapefile (zip) — Field Maps</a>\n")cat("<a href=\"exports/ras_2026_survey_nav.geojson\">Gap lines GeoJSON</a>\n")cat("<a href=\"exports/owens_river_channels.geojson\">River channels</a>\n")cat("<a href=\"exports/lorp_rivermile_centerline.geojson\">River-mile centerline</a>\n")cat("</div>\n")if (nrow(nav_summary) >0) {print(kable(nav_summary,col.names =c("Bank", "Line style", "Lines"),caption ="Gap lines by bank")) }cat("</div>\n\n")} else {cat("Navigation layer not found. Run `Rscript export_survey_nav.R`.\n")}```## Field documentation```{r field-images}#| message: false#| warning: false#| results: 'asis'#| eval: false# Function to check if a feature has attachmentscheck_feature_attachments <-function(objectid) { base_url <-"https://services.arcgis.com/0jRlQ17Qmni5zEMr/arcgis/rest/services/Noxious_Weeds_2025_view/FeatureServer/0" attachments_url <-paste0(base_url, "/", objectid, "/attachments?f=json") response <-GET(attachments_url)if(response$status_code ==200) { attachments_data <-content(response, 'parsed')return(length(attachments_data$attachmentInfos) >0) }return(FALSE)}# Function to get attachments for a featureget_feature_attachments <-function(objectid) { base_url <-"https://services.arcgis.com/0jRlQ17Qmni5zEMr/arcgis/rest/services/Noxious_Weeds_2025_view/FeatureServer/0" attachments_url <-paste0(base_url, "/", objectid, "/attachments?f=json") response <-GET(attachments_url)if(response$status_code ==200) { attachments_data <-content(response, 'parsed')return(attachments_data$attachmentInfos) } else {return(NULL) }}# Function to display an image from attachmentdisplay_attachment_image <-function(objectid, attachment_id, alt_text, width =500) { base_url <-"https://services.arcgis.com/0jRlQ17Qmni5zEMr/arcgis/rest/services/Noxious_Weeds_2025_view/FeatureServer/0" image_url <-paste0(base_url, "/", objectid, "/attachments/", attachment_id) alt_esc <- htmltools::htmlEscape(alt_text, attribute =TRUE)# Create HTML for image display html_content <-paste0('<div style="display: inline-block; margin: 15px; text-align: center;">','<img src="', image_url, '" alt="', alt_esc, '" style="max-width: ', width,'px; height: auto; border: 2px solid #ddd; border-radius: 8px;">','</div>' )# Use cat to output HTML directlycat(html_content)}```### Field Documentation ImagesPepperweed observations with field photos are available from the AGOL feature service attachments. (Photo gallery chunk is currently skipped at render time to avoid slow attachment checks; set `eval: true` on `field-images` / `field-images-display` to refresh.)```{r field-images-display}#| message: false#| warning: false#| results: 'asis'#| eval: false# Find features with attachments (check more features for up to 40 photos)features_to_check <- pepper_data %>%filter(Year_Display >= params$current_year -2) %>%# Include last 2 yearsarrange(desc(Year_Display), desc(Abundance)) %>%head(100) # Check more features to find ones with attachmentsfeatures_with_attachments <-c()for(i in1:nrow(features_to_check)) { objectid <- features_to_check$OBJECTID[i]if(check_feature_attachments(objectid)) { features_with_attachments <-c(features_with_attachments, objectid) }}# Display images for features that have attachments (up to 40)images_displayed <-0if(length(features_with_attachments) >0) {for(objectid in features_with_attachments[1:min(40, length(features_with_attachments))]) {# Get feature data feature <- pepper_data[pepper_data$OBJECTID == objectid,]# Get attachments attachments <-get_feature_attachments(objectid)if(!is.null(attachments) &&length(attachments) >0) {# Get the first image attachment first_attachment <- attachments[[1]] alt_text <-paste0("Field photo of perennial pepperweed, Feature ID ", objectid,", observed ", format(feature$Date_Display, "%Y-%m-%d"),", abundance ", format_abundance(feature$Abundance) )# Display image firstdisplay_attachment_image(objectid, first_attachment$id, alt_text)# Display metadata below the imagecat("<div style='text-align: center; margin-bottom: 30px;'>")cat("<p>Feature ID:", objectid, "| Date:", format(feature$Date_Display, "%Y-%m-%d"), "| Abundance:", format_abundance(feature$Abundance), "</p>")# Include Notes if availableif(!is.na(feature$Notes) && feature$Notes !="") {cat("<p>Notes:", feature$Notes, "</p>") }cat("</div>") images_displayed <- images_displayed +1 } }}if(images_displayed ==0) {cat("No field documentation images available for recent observations.\n")} else {cat("\n*Field documentation images provide visual confirmation of pepperweed sites and help verify site characteristics.*\n")}```## ConclusionsOccurrence records for `r params$start_year`–`r params$current_year` document progressive filling of known pepperweed occupancy along the LORP corridor, with coarse (whole-mile) occupancy substantially ahead of fine (0.1-mi) occupancy. Cumulative discovery isolates this infill process from year-to-year survey intensity and from resurvey avoidance of known stands. Smooth forecasts provide planning benchmarks for when known occupancy may approach saturation; a pulse-recruitment reading—flood-driven steps, quieter intervals, and lagged discovery after high-water years—remains a process-motivated alternative to purely linear discovery.Operationally, FY 2026–27 RAS priorities target corridor miles and bank stretches still lacking detections, consistent with managing leading edges and satellite populations [@renz2012]. This report will update as new observations enter the live feature service. A companion manuscript track will lock and later evaluate formal one-year-ahead occupancy forecasts.### Data disclaimerData are collected for monitoring and management by the Inyo County Water Department. Field conditions, survey timing, and observer experience affect quality. Users should verify critical information independently before management decisions.