Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: taupatch
Title: Spatial Habitat Suitability Models for Zooplankton High-Abundance Patches
Version: 0.1.0
Version: 0.2.0
Authors@R:
person("Camille", "Ross", email = "camille.ross@maine.edu", role = c("aut", "cre"),
comment = c(ORCID = "0000-0002-1428-2294"))
Expand All @@ -23,6 +23,7 @@ Imports:
datamatch,
dplyr,
ggplot2,
parallel,
parsnip,
recipes,
rsample,
Expand All @@ -49,7 +50,8 @@ Suggests:
shiny,
shinyFiles,
rnaturalearth,
testthat (>= 3.0.0)
testthat (>= 3.0.0),
withr
Remotes:
chross22/datamatch,
chross22/derivoce,
Expand Down
7 changes: 7 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Generated by roxygen2: do not edit by hand

S3method(print,taupatch_ensemble)
export(abundance_units)
export(add_derivoce_covariates)
export(apply_prejoin_steps)
Expand All @@ -21,7 +22,10 @@ export(derivoce_choices)
export(derivoce_covariates)
export(derivoce_required_inputs)
export(derivoce_steps_for)
export(ensemble_rules)
export(ensemble_settings)
export(fetch_covariates)
export(fit_patch_ensemble)
export(fit_patch_model)
export(format_zoop_data)
export(gam_bases)
Expand All @@ -32,6 +36,9 @@ export(generate_mock_covariates)
export(generate_mock_zoop_data)
export(glm_coefficients)
export(is_raw_export)
export(jackknife_covariates)
export(jackknife_dropped)
export(jackknife_settings)
export(label_patch)
export(load_config)
export(load_zoop_data)
Expand Down
112 changes: 111 additions & 1 deletion R/config.R
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ load_config <- function(path) {
if (!file.exists(path)) {
stop("Config file not found: ", path, call. = FALSE)
}
config <- yaml::read_yaml(path)
config <- read_config_yaml(path)

config <- resolve_config_paths(config, path)
config <- apply_config_defaults(config)
Expand All @@ -25,6 +25,7 @@ load_config <- function(path) {
validate_study_area(config)
validate_covariates(config)
validate_uncertainty(config)
validate_jackknife(config)

config$species$resolved <- resolve_species(config)

Expand All @@ -37,6 +38,103 @@ load_config <- function(path) {
config
}

#' Read a config YAML without losing keys that spell a boolean
#'
#' `yaml::read_yaml()` parses YAML 1.1, where a bare `n` is the boolean `false`.
#' That is correct for a *value* and wrong for a *key*, and the difference is
#' silent: a derivoce step written
#'
#' ```yaml
#' - type: lag_covariate
#' vars: [CHL]
#' n: 2
#' ```
#'
#' parses to a list whose key is named `FALSE`, so `spec$n` is `NULL` and the
#' step falls back to a one-month lag. The config asked for two, the run used
#' one, and nothing said so. The same goes for `y`, `yes`, `no`, `on`, `off`,
#' `true` and `false` in any capitalisation.
#'
#' The fix is to keep the source text. `yaml`'s handlers are given the original
#' scalar as it was written — `"n"`, not `FALSE` — so this marks each one and
#' then, once the structure exists, restores it: text in a name position is the
#' key the file actually wrote, and text in a value position becomes the logical
#' it meant. The two cannot be told apart while parsing, which is exactly why
#' this is two passes rather than a cleverer handler.
#'
#' @param path path to a config YAML file
#' @return the parsed config list
#' @seealso [write_config_yaml()], the write side — `yaml::as.yaml()` already
#' quotes these keys on the way out, so a config this package writes is read
#' correctly by anything
#' @keywords internal
read_config_yaml <- function(path) {
mark <- function(x) paste0(yaml_bool_marker, x)
restore_yaml_bools(
yaml::read_yaml(path, handlers = list("bool#yes" = mark, "bool#no" = mark))
)
}

#' The marker that carries a boolean's source text through parsing
#'
#' A prefix rather than an attribute or a class, because it has to survive
#' `yaml` collapsing a sequence of scalars into an atomic vector, which drops
#' attributes. `[true, false]` would otherwise come back as two strings.
#'
#' Deliberately plain ASCII. The first version used control characters, on the
#' reasoning that nothing could collide with them. That was true, and it made
#' `file(1)` report the whole of `config.R` as binary rather than as source, so
#' editors and diff viewers presented the file as corrupt.
#'
#' Spelling it out costs nothing. Only the boolean handlers ever prepend this,
#' and they only ever see scalars YAML itself resolved as booleans — a quoted
#' `"true"` carries no boolean tag and is never marked. The one way left to
#' collide is a config value that genuinely begins with this text.
#'
#' @keywords internal
yaml_bool_marker <- "<taupatch:yaml-bool>"

#' Turn marked scalars back into keys and logicals
#'
#' Names get the source text they were written with; values get the logical
#' that text meant. A sequence that mixes a marked scalar with an unmarked one
#' cannot be a logical vector, so it keeps the text — `[true, maybe]` is a list
#' of two strings, which is the only reading available.
#'
#' @param x a parsed YAML value
#' @return `x` with markers resolved
#' @keywords internal
restore_yaml_bools <- function(x) {
if (is.list(x)) {
names(x) <- unmark_yaml_bool(names(x))
return(lapply(x, restore_yaml_bools))
}
if (!is.character(x) || length(x) == 0) return(x)

marked <- startsWith(x, yaml_bool_marker)
if (!any(marked)) return(x)

source <- unmark_yaml_bool(x)
if (!all(marked)) return(source)
# The YAML 1.1 spellings of true. Everything else the handlers see is a false.
tolower(source) %in% c("y", "yes", "true", "on")
}

#' Strip the boolean marker, leaving what the file wrote
#'
#' Anchored at the start rather than replaced wherever it appears, so a quoted
#' value that happens to contain the marker's text further along is left alone.
#'
#' @param x a character vector, or `NULL` for an unnamed list
#' @return `x` with any marker prefix removed
#' @keywords internal
unmark_yaml_bool <- function(x) {
if (is.null(x)) return(NULL)
marked <- startsWith(x, yaml_bool_marker)
x[marked] <- substring(x[marked], nchar(yaml_bool_marker) + 1L)
x
}

#' Resolve config paths relative to the project directory
#'
#' `paths.project_dir` itself is resolved relative to the config file's own
Expand Down Expand Up @@ -191,6 +289,18 @@ validate_threshold <- function(threshold, species_name) {
#' @return `TRUE` invisibly; errors otherwise
#' @keywords internal
validate_model <- function(config) {
# An ensemble has no single type of its own. Its members are validated
# instead, each under the config it will actually be fitted with - so a
# per-member override that cannot work is an error now rather than partway
# through fitting the ensemble.
ensemble <- ensemble_settings(config)
if (!is.null(ensemble)) {
for (type in ensemble$types) {
validate_model(member_config(config, type, ensemble))
}
return(invisible(TRUE))
}

type <- resolve_model_type(config)
entry <- model_types()[[type]]

Expand Down
Loading
Loading