Skip to content

Refactor effects #113

Description

@auzaheta

Legacy Structure:

  • Effect files are currently organized by model (e.g., REM, DyNAM-choice).
  • Functions follow the naming convention: update_DyNAM_choice_effect()
  • Some models reuse effects defined for other models via wrappers.
    Example: indeg and outdeg are defined in REM and reused in DyNAM-rate, -choice, and -coordination models.

Describe the solution you'd like:

  • Restructure effect files by type, not by model:
    • effects_attribute.R: ego, alter, same, ...,
    • effects_dyadic_stats.R: tie, inertia, recip.
    • effects_nodal_stats.R: degree, indegree, outdegree.
    • effects_closure.R: trans, cycle, common_sender, common_receiver, four, node_trans.
    • effects_mixed_closure.R: mixed_trans, mixed_cycle, ...
    • effects_attr_struc.R: tertius, tertius_diff
    • effects_groups_nodal_stats.R
    • effects_groups_dyad_stats.R
    • effects_default.R: default method to initialize the effect when one is not provided.
  • Avoid duplication: centralize definitions where possible.
  • Add robust input validation:
    • Informative error messages when an effect is not suitable for a given input:
      • Undirected: only a few effects are valid (e.g., degree (new), trans (renamed?), tie, inertia).
      • Directed: most of the dyadic effects assume direction.
      • Two-mode: prohibit recip and closure effects use.
  • Restrict effect usage by model:
    Example: rate model should only allow nodal statistics.

Example using a central registry:

.effect_registry <- new.env(parent = parent.env(environment()))

register_effect <- function(name, init, update, meta = list()) {
  .effect_registry[[name]] <- list(
    init = init(),
    update = update(),
    meta = meta,
    args_validation = fn_validate()
  )
}

get_effect_def <- function(name) {
  if (!exists(name, envir = .effect_registry)) {
    stop("Unknown effect: ", name)
  }
  .effect_registry[[name]]
}
make_effect <- function(name, args = list()) {
  def <- get_effect_def(name)
    list(
      init = effect_factory(def$init, args),
      update = effect_factory(def$update, args)
    )
}

## example
register_effect(
  "indeg",
  init = function(network, weighted, window, transformer_fn, n2) {
    if (!is.infinite(windowed)) return(numeric(n2)) 
    .colSums(if (weighted) network else network > 0, n1, n2, na.rm = TRUE)
  },
  update = function(network, sender, receiver, replace, cache, weighted, transformer_fn) {
    cache[receiver] <- cache[receiver] + replace - oldValue
     ...
    list(changes, cache)
  },
  validate = c(not_two_mode, not_undirected), # either functions or a character vector or checks to perform
  meta = list(
    label = "Indegree",
    description = "Incoming degree of an actor in a network",
    family = "degree, nodal statistics"
  )
)

Wishlist:

  • Generate a single JSON/YAML table with all effect metadata:

    • Useful for documentation (pictorical representation of the effects)
    • Enables tooling like searchERGMterm()
    • Example fields: name, allowed_models, input_type, family, description, pictogram

    Possible drawbacks: file size, it makes complex to add news effects

  • Create help pages by type of effect that can be opened with ?effect_name

  • Enable tooling like searchERGMterm() given a type of input

Describe alternatives you've considered
Define effects as an R6:

EffectBase <- R6::R6Class("EffectBase",
  public = list(
    name = NULL,
    args = NULL,
    metadata = NULL,
    initialize = function(name, args = list(), metadata = list()) {
      self$name <- name
      self$args <- args
      self$metadata <- metadata
      self$validate_once()
    },
    validate_once = function() {
      # Shared checks done only once per effect object.
      stopifnot(is.character(self$name))
      # e.g. check args match allowed types, windows, weighted etc
      if (!self$metadata$suitable) {
        warning(sprintf("Effect %s not recommended: %s",
          self$name, self$metadata$reason))
      }
    },
    update = function(network, sender, ...) { ... }
  )
)

# example

IndegreeEffect <- R6::R6Class("IndegreeEffect",
  public = list(
    name = "indegree",
    initialize = function(args) { ... },
    update = function(network, sender, ...) { ... }
  )
)

It seems that using R6 classes can lead to an overhead in method distpatch. It's expected that S3 method dispatch is generally faster, especially when a calling method millions of times.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Difficulty: HighExpected workload is several months.Status: Revision NeededIssue needs more thinking or feedback before it can be addressed.Type: EnhancementIssue is about improving an existing feature.

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions