@@ -88,13 +88,11 @@ code_lookup <- code_map |>
8888
8989PEcAn.logger :: logger.info(sprintf(" Resolved %d CADWR codes via crosswalk" , nrow(code_lookup )))
9090
91- # the event date is anchored to green-up (leafonday) from the gap-filled
92- # phenology product, observed where the satellite retrieval succeeded and
93- # crop-calendar filled otherwise, so this covers the full ~600k ag universe
94- # instead of the ~377k strict-matched subset. crop class per season comes
95- # from the CADWR Land Use crops product. the crops product's own emergence
96- # date is empty statewide, so the gap-filled green-up is the only populated
97- # anchor available.
91+ # the event date is the anchor itself; this workflow applies no offset. the
92+ # anchor transition is chosen per PFT so annuals are timed to planting and
93+ # perennials to leaf-on, matching the split the monitoring event products use.
94+ # both come from the gap-filled LandIQ to MSLSP match, which keys every
95+ # transition by (parcel_id, year, season), so cycles join on a real season key.
9896
9997years <- config [[" years" ]]
10098PEcAn.logger :: logger.info(" Reading crops and gap-filled phenology for years: " ,
@@ -117,50 +115,78 @@ crops <- DBI::dbGetQuery(con, sprintf(
117115 dplyr :: rename(year = " yr" ) | >
118116 dplyr :: mutate(code = paste0(.data $ CLASS , .data $ SUBCLASS ))
119117
120- # the phenology product has no season key, but from 2018 on it carries a second
121- # green-up for most double-crop parcels, so rank green-ups within a parcel-year
122- # and match the nth crop cycle to the nth green-up rather than collapsing to the
123- # earliest. phenology_source is carried through for audit.
124- phen_anchor_col <- config [[" phen_anchor_col" ]]
125- phen_raw <- DBI :: dbGetQuery(con , sprintf(
126- " SELECT * FROM read_parquet('%s') WHERE \" year\" IN (%s)" ,
118+ pft_anchor <- unlist(config [[" pft_anchor" ]])
119+ anchor_cols <- sort(unique(pft_anchor ))
120+
121+ # "**" is the LandIQ sentinel for subclass not specified. it becomes NA on both
122+ # sides of the join, so those rows land on the class-level fallback
123+ pft_lookup <- readr :: read_csv(config [[" pft_lookup_path" ]],
124+ show_col_types = FALSE ) | >
125+ dplyr :: filter(! is.na(.data $ PFT ))
126+ pft_by_code <- pft_lookup | >
127+ dplyr :: transmute(CLASS = .data $ CLASS ,
128+ SUBCLASS = as.integer(dplyr :: na_if(as.character(.data $ SUBCLASS ), " **" )),
129+ pft_group = .data $ PFT ) | >
130+ dplyr :: distinct()
131+ pft_by_class <- pft_lookup | >
132+ dplyr :: count(.data $ CLASS , .data $ PFT ) | >
133+ dplyr :: slice_max(.data $ n , n = 1 , by = " CLASS" , with_ties = FALSE ) | >
134+ dplyr :: transmute(CLASS = .data $ CLASS , pft_group_class = .data $ PFT )
135+
136+ phen <- DBI :: dbGetQuery(con , sprintf(
137+ " SELECT CAST(parcel_id AS INTEGER) AS parcel_id, CAST(\" year\" AS INTEGER) AS year,
138+ CAST(season AS INTEGER) AS season, gapfill_date_source, %s
139+ FROM read_parquet('%s') WHERE \" year\" IN (%s)" ,
140+ paste(anchor_cols , collapse = " , " ),
127141 file.path(config [[" phen_dir" ]], config [[" phen_glob" ]]), yr_list ))
128- phen_id_col <- if (" parcel_id" %in% names(phen_raw )) " parcel_id" else " site_id"
129- phen_source_col <- if (" phenology_source" %in% names(phen_raw )) {
130- " phenology_source"
131- } else if (" gapfill_date_source" %in% names(phen_raw )) {
132- " gapfill_date_source"
133- } else {
134- NA_character_
135- }
136- phen <- phen_raw | >
137- dplyr :: transmute(
138- parcel_id = as.integer(.data [[phen_id_col ]]),
139- year = as.integer(.data $ year ),
140- date = as.Date(.data [[phen_anchor_col ]]),
141- phenology_source = if (is.na(phen_source_col )) {
142- NA_character_
143- } else {
144- as.character(.data [[phen_source_col ]])
145- }
146- ) | >
147- dplyr :: filter(! is.na(.data $ date )) | >
148- dplyr :: arrange(.data $ parcel_id , .data $ year , .data $ date ) | >
149- dplyr :: mutate(phen_rank = dplyr :: row_number(), .by = c(" parcel_id" , " year" ))
150142
151- # where a parcel-year has fewer green-ups than crop cycles, the later cycles
152- # reuse the last available one
153- phen_max <- phen | >
154- dplyr :: summarize(max_rank = max(.data $ phen_rank ), .by = c(" parcel_id" , " year" ))
143+ missing_cols <- setdiff(anchor_cols , names(phen ))
144+ if (length(missing_cols ) > 0 ) {
145+ PEcAn.logger :: logger.severe(
146+ " phenology product has no column(s) named in pft_anchor: " ,
147+ paste(missing_cols , collapse = " , " ))
148+ }
155149
150+ # a cycle with no matched phenology row has no anchor, and is dropped rather
151+ # than given a substitute date
156152plant <- crops | >
157- dplyr :: mutate(season_rank = dplyr :: dense_rank(.data $ season ),
158- .by = c(" parcel_id" , " year" )) | >
159- dplyr :: inner_join(phen_max , by = c(" parcel_id" , " year" )) | >
160- dplyr :: mutate(phen_rank = pmin(.data $ season_rank , .data $ max_rank )) | >
161- dplyr :: inner_join(phen , by = c(" parcel_id" , " year" , " phen_rank" ))
162- PEcAn.logger :: logger.info(sprintf(" Loaded %d cycles across %d parcels (phenology anchored)" ,
163- nrow(plant ), dplyr :: n_distinct(plant $ parcel_id )))
153+ dplyr :: inner_join(phen , by = c(" parcel_id" , " year" , " season" )) | >
154+ dplyr :: left_join(pft_by_code , by = c(" CLASS" , " SUBCLASS" )) | >
155+ dplyr :: left_join(pft_by_class , by = " CLASS" ) | >
156+ dplyr :: mutate(pft_group = dplyr :: coalesce(.data $ pft_group , .data $ pft_group_class ))
157+ PEcAn.logger :: logger.info(sprintf(
158+ " Anchored %d of %d crop cycles (%.1f%%) across %d parcels" ,
159+ nrow(plant ), nrow(crops ), 100 * nrow(plant ) / nrow(crops ),
160+ dplyr :: n_distinct(plant $ parcel_id )))
161+
162+ # non-crop pfts have no anchor rule. report them so a crop type missing a rule
163+ # is visible rather than silently absent
164+ dropped <- plant | >
165+ dplyr :: filter(! .data $ pft_group %in% names(pft_anchor )) | >
166+ dplyr :: count(.data $ pft_group , sort = TRUE )
167+ if (nrow(dropped ) > 0 ) {
168+ PEcAn.logger :: logger.info(sprintf(
169+ " Dropping %d cycles whose pft has no anchor rule:" , sum(dropped $ n )))
170+ for (i in seq_len(nrow(dropped ))) {
171+ PEcAn.logger :: logger.info(sprintf(" %s: %d cycles" ,
172+ dropped $ pft_group [i ], dropped $ n [i ]))
173+ }
174+ }
175+ plant <- plant | > dplyr :: filter(.data $ pft_group %in% names(pft_anchor ))
176+
177+ # index a numeric matrix so the anchor stays config driven, not a branch per pft
178+ anchor_idx <- cbind(seq_len(nrow(plant )),
179+ match(pft_anchor [plant $ pft_group ], anchor_cols ))
180+ anchor_num <- do.call(cbind , lapply(plant [anchor_cols ], as.numeric ))
181+ plant $ date <- as.Date(anchor_num [anchor_idx ], origin = " 1970-01-01" )
182+
183+ # a matched row is expected to carry every transition, so a NULL anchor means the
184+ # product changed rather than a cycle being legitimately undated
185+ no_anchor <- sum(is.na(plant $ date ))
186+ if (no_anchor > 0 ) {
187+ PEcAn.logger :: logger.severe(sprintf(
188+ " %d cycles have a NULL anchor in the gap-filled product" , no_anchor ))
189+ }
164190
165191# # subsample
166192# parcel set is sampled once and applied to all years so the same parcels
@@ -226,16 +252,16 @@ if (nrow(zero_env) > 0) {
226252}
227253
228254kept <- design | > dplyr :: filter(.data $ rate_source == " crosswalk" )
229- src <- kept | > dplyr :: count(.data $ phenology_source , sort = TRUE )
230- PEcAn.logger :: logger.info(" Anchor provenance (phenology_source ):" )
255+ src <- kept | > dplyr :: count(.data $ gapfill_date_source , sort = TRUE )
256+ PEcAn.logger :: logger.info(" Anchor provenance (gapfill_date_source ):" )
231257for (i in seq_len(nrow(src ))) {
232258 PEcAn.logger :: logger.info(sprintf(" %s: %d cycles (%.1f%%)" ,
233- src $ phenology_source [i ], src $ n [i ],
259+ src $ gapfill_date_source [i ], src $ n [i ],
234260 100 * src $ n [i ] / nrow(kept )))
235261}
236262
237263design <- kept | >
238- dplyr :: select(" parcel_id" , " year" , " season" , " date" , " code" ,
264+ dplyr :: select(" parcel_id" , " year" , " season" , " date" , " code" , " pft_group " ,
239265 " min_n_lbs_acre" , " max_n_lbs_acre" ) | >
240266 # fixed row order so the per row draws in 02 are reproducible under the seed
241267 dplyr :: arrange(.data $ parcel_id , .data $ year , .data $ season )
0 commit comments