Skip to content

Commit 90cbe54

Browse files
authored
Merge pull request #28 from SocSci-for-Sustainability/observer-dispatch
v0.2.5 generalized observer dispatch prep for opinion observing
2 parents 239f4dd + edd03cb commit 90cbe54

75 files changed

Lines changed: 547 additions & 290 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: socmod
22
Title: Social behavior models in R
3-
Version: 0.2.4
3+
Version: 0.2.5
44
Authors@R:
55
person("Matt", "Turner", , "maturner01@gmail.com", role = c("aut", "cre"),
66
comment = c(ORCID = "0000-0001-7421-4552"))

NAMESPACE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ export(make_preferential_attachment)
3333
export(make_regular_lattice)
3434
export(make_small_world)
3535
export(not_adjacent)
36+
export(observe_behavior)
37+
export(observe_dispatch)
38+
export(observe_latent)
3639
export(plot_homophilynet)
3740
export(plot_network_adoption)
3841
export(plot_prevalence)

NEWS.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
# socmod 0.2.4 (2025-10-24)
1+
# socmod 0.2.5 (2025-09-27)
2+
3+
- Added observation dispatch to `run_trial()` and `run_trials()`.
4+
- Default remains `"behavior"`, ensuring backward compatibility.
5+
6+
# socmod 0.2.4 (2025-09-24)
27

38
- If only `agents` are provided to `make_abm` an empty graph is created by
49
default.

R/model-dynamics.R

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ success_bias_interact <- function(learner, teacher, model) {
229229
}
230230
}
231231

232+
232233
### -------- CONTAGION -----------
233234

234235
#' Contagion-based partner selection

R/observe.R

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#' Observation functions for agent-based models
2+
#'
3+
#' These functions provide a standardized way to record model state
4+
#' during trials. The dispatcher selects the appropriate function
5+
#' based on the `observe` argument.
6+
#'
7+
#' @param model An AgentBasedModel instance
8+
#' @param step Current simulation step (integer)
9+
#' @param label Optional label string for the trial
10+
#' @param ... Additional arguments for future observation types
11+
#'
12+
#' @export
13+
observe_behavior <- function(model, step, label = NULL, ...) {
14+
tibble::tibble(
15+
Step = step,
16+
agent = vapply(model$agents, \(a) a$name, character(1)),
17+
Behavior = vapply(model$agents, \(a) as.character(a$behavior_current), character(1)),
18+
Fitness = vapply(model$agents, \(a) a$fitness_current, numeric(1)),
19+
label = label
20+
)
21+
}
22+
23+
# Stub for latent opinions (future expansion in v0.3.0)
24+
#' @export
25+
observe_latent <- function(model, step, label = NULL, ...) {
26+
stop("Observation type 'latent' not implemented in v0.2.5")
27+
}
28+
29+
# Dispatcher
30+
#' @export
31+
observe_dispatch <- function(model, type = "behavior", step, label = NULL, ...) {
32+
switch(
33+
type,
34+
behavior = observe_behavior(model, step = step, label = label, ...),
35+
latent = observe_latent(model, step = step, label = label, ...),
36+
stop(sprintf("Unknown observation type: %s", type))
37+
)
38+
}
39+

R/trial.R

Lines changed: 43 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ Trial <- R6::R6Class(
1313
observations = NULL,
1414
outcomes = NULL,
1515
metadata = list(),
16+
label = NULL,
1617

1718
#' @description Initialize a Trial with a model and functions
1819
#' @param model An AgentBasedModel instance
1920
#' @param metadata Label-value metadata store for trial information
20-
initialize = function(model, metadata = list()) {
21+
initialize = function(model, metadata = list(), label = NULL) {
2122

2223
# Sync internal variables with user-provided.
2324
self$model <- model
@@ -35,38 +36,25 @@ Trial <- R6::R6Class(
3536
#' @param legacy_behavior The maladaptive behavior treated as "adaptation failure"
3637
#' @param adaptive_behavior The behavior treated as "adaptation success"
3738
run = function(
38-
stop = 50, legacy_behavior = "Legacy", adaptive_behavior = "Adaptive") {
39+
stop = 50, legacy_behavior = "Legacy",
40+
adaptive_behavior = "Adaptive", observe = "behavior") {
3941

4042
step <- 0
4143

4244
self$model$set_parameter("legacy_behavior", legacy_behavior)
4345
self$model$set_parameter("adaptive_behavior", adaptive_behavior)
4446
step <- 0
47+
4548
obs_list <- list()
46-
n_agents <- length(self$model$agents)
47-
48-
obs_list[[1]] <- tibble::tibble(
49-
Step = 0,
50-
agent = vapply(self$model$agents,
51-
\(a) a$name, character(1)),
52-
Behavior = vapply(self$model$agents,
53-
\(a) as.character(a$behavior_current), character(1)),
54-
Fitness = vapply(self$model$agents,
55-
\(a) a$fitness_current, numeric(1)),
56-
label = self$label
57-
)
5849

5950
# Get learning and iteration functions from the model's learning strategy.
6051
lstrat <- self$model$get_parameter("model_dynamics")
6152
partner_selection <- lstrat$get_partner_selection()
6253
interaction <- lstrat$get_interaction()
6354
model_step <- lstrat$get_model_step()
64-
65-
# Main iteration loop.
66-
while (TRUE) {
67-
68-
step <- step + 1
69-
55+
56+
repeat {
57+
# --- run one model step ---
7058
# Partner selection and interaction with selected partner.
7159
for (agent in self$model$agents) {
7260
partner <- NULL
@@ -80,41 +68,40 @@ Trial <- R6::R6Class(
8068
if (!is.null(model_step)) {
8169
model_step(self$model)
8270
}
83-
84-
# Update observations.
85-
obs_list[[step + 1]] <- tibble::tibble(
86-
Step = step,
87-
agent = vapply(self$model$agents, \(a) a$name, character(1)),
88-
Behavior = vapply(self$model$agents, \(a) as.character(a$behavior_current), character(1)),
89-
Fitness = vapply(self$model$agents, \(a) a$fitness_current, numeric(1)),
71+
72+
# --- collect observations via dispatcher ---
73+
obs_list[[step + 1]] <- observe_dispatch(
74+
self$model,
75+
type = observe,
76+
step = step,
9077
label = self$label
9178
)
92-
93-
# Stop when stop function returns TRUE or max steps reached.
94-
if (is.function(stop)) {
95-
if (stop(self$model)) {
96-
break
97-
}
98-
} else if (step >= stop) {
99-
break
100-
}
101-
} # End main iteration loop
102-
103-
behaviors <- unlist(
104-
purrr::map(
105-
self$model$agents, \(a) as.character(a$get_behavior())
106-
),
107-
use.names = FALSE
108-
)
79+
80+
# --- stopping condition ---
81+
step <- step + 1
82+
if (is.numeric(stop) && step >= stop) break
83+
if (is.function(stop) && stop(self$model)) break
84+
}
85+
86+
# bind all observations
10987
self$observations <- dplyr::bind_rows(obs_list)
110-
111-
self$outcomes$adaptation_success <-
112-
length(unique(behaviors)) == 1 &&
88+
89+
# if we observe behavior, identify whether this is adaptive success or not
90+
if (observe == "behavior") {
91+
behaviors <- unlist(
92+
purrr::map(
93+
self$model$agents, \(a) as.character(a$get_behavior())
94+
),
95+
use.names = FALSE
96+
)
97+
self$outcomes$adaptation_success <-
98+
length(unique(behaviors)) == 1 &&
11399
unique(behaviors) == adaptive_behavior
100+
101+
self$outcomes$fixation_steps <- step
102+
}
114103

115-
self$outcomes$fixation_steps <- step
116-
117-
invisible (self)
104+
return(invisible(self))
118105
},
119106

120107
#' Add or update metadata in a Trial object
@@ -187,14 +174,15 @@ fixated <- function(model) {
187174
#' trial <- run_trial(model, stop = 10)
188175
#' @export
189176
run_trial <- function(model,
177+
observe = "behavior",
190178
stop = socmod::fixated,
191179
legacy_behavior = "Legacy",
192180
adaptive_behavior = "Adaptive",
193181
metadata = list()) {
194-
182+
195183
# Initialize, run, and return a new Trial object.
196184
return (
197-
Trial$new(model = model, metadata = metadata)$run(
185+
Trial$new(model = model, metadata = metadata)$run(
198186
stop = stop, legacy_behavior = legacy_behavior,
199187
adaptive_behavior = adaptive_behavior
200188
)
@@ -234,7 +222,7 @@ run_trial <- function(model,
234222
#' )
235223
#' @export
236224
run_trials <- function(model_generator, n_trials_per_param = 10,
237-
stop = 10, .progress = TRUE,
225+
stop = 10, .progress = TRUE, observe = "behavior",
238226
syncfile = NULL, overwrite = FALSE, ...) {
239227

240228
# Check if syncfile is given...
@@ -270,7 +258,8 @@ run_trials <- function(model_generator, n_trials_per_param = 10,
270258

271259
run_trial(
272260
model, stop, legacy_behavior, adaptive_behavior,
273-
metadata = list(replication_id = param_list$replication_id)
261+
metadata = list(replication_id = param_list$replication_id),
262+
observe = observe
274263
)
275264
},
276265
.progress = .progress

_pkgdown.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ reference:
99
- starts_with("run_trial")
1010
- Trial
1111
- fixated
12+
- observe_behavior
1213
- title:
1314
desc: |
1415
Summarise a collection of trial outcomes or prevalence dynamics over model parameters

docs/404.html

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/LICENSE-text.html

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/LICENSE.html

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)