Changes in version 0.7.3 - dynet() gains format = "broadcast": a log of actions in groups, read as every action addressed to the whole group, becomes ties from the actor to every other member at the action's time. - event_graph() gains events = "messages", which merges the ties sharing a source, start, end and session into one event reaching several targets (an action to a whole group, an email to several recipients). A message is followed by any later event that leaves one of its targets. - The events view of plot() gives every source its own row by default (rows = "actor", honouring top); rows = "chain" keeps one row per relay chain. A message graph is drawn this way by default. - Every adjacency of an event graph now says what its two events were: first and second give each as from->to (from--to when undirected), and each tie attribute of the two events comes as first_ and second_. print() shows the events by who reached whom rather than by number alone; from_event and to_event remain as the keys of the event table. - dynet() gains format = "turns" for logs of who acted when, with no recipient, such as coded chat or collaboration actions. Within each group, every action takes up the one before it: a directed contact from the previous actor to the current one, at the current action's time, with the row's other columns as its tie attributes. The group is the session unless session is named. - summary() of an event graph built with sessions = "separate" gives each event's session. - plot() of an event graph gains type = "events", which draws the events themselves as the vertices: one point per event at its start time, one arc per adjacency, and one row per relay chain (a weakly connected component of the event graph). color_by colours and shapes the points by a column of the event table, such as a coded action, and labels = TRUE writes from->to under each point. The storyline remains the default. - The events of event_graph() carry the tie attributes of the spells they come from, and summary() of an event graph gives each event's from, to and tie attributes beside its degrees, so an event can be read by what it was (for example, which coded action was relayed) with no merge. - The spell column of the event table is documented correctly: it is the event's row in the log given to dynet(), counted after dropped rows, not its row in as.data.frame(dn). Changes in version 0.7.2 - event_graph() now joins two events only when the later one starts strictly after the earlier one ends. An event that starts at the very instant an interval event ends is no longer its successor, so every wait is positive and delta = 0 admits no adjacency. This is the rule of Reticula (Badie-Modiri and Kivela, 2023), the reference implementation of the event graph, against which event_graph() has been checked on 600 random networks with identical results. Changes in version 0.7.1 - The paths() example shows criterion = "latest_departure" on the small example network rather than the school data, so every example again runs in under five seconds. - A test of reach under every path criterion calls reachability(), where reach has lived since 0.4.13; it failed on every platform after the 0.6.0 merge. Changes in version 0.7.0 - event_graph() builds the event graph of a temporal network: every spell is a vertex, and an arc joins two events that share a vertex when the later one starts after the earlier one ends, within delta (Kivela et al., 2018). A path in it is a chain of events taken one after another, so on contact data its reachability is that of paths() with a short traversal time. adjacency = "next" keeps only the earliest successors (Kovanen et al., 2011); direction and sessions decide which shared vertices and which session blocks may join two events. The result has print(), summary(), plot() and as.data.frame(what = "events" | "adjacencies") methods. - The reference index lists every exported verb again, including the measurement, null-model and community verbs that arrived in 0.6.0. Changes in version 0.6.0 The measurement work developed on the next branch joins the released line. Every addition is available under the 0.5 names: snapshot centrality is centrality_series(), centrality on time-respecting paths is path_centrality(), and temporal reach is reachability(). Null models and significance - randomise() draws surrogate temporal networks from a null model, and random_dynet() simulates a temporal network from a binomial, Poisson, block or activation model. - significance() compares any verb that returns measure and value against a null model, with a percentile interval and a permutation p-value for every row, corrected for multiplicity. Paths and centrality - paths() takes a criterion: "foremost_then_shortest" (the default and the earlier behaviour), "min_hops", "foremost", "fastest", "latest_departure" and "shortest". With "shortest", cost = "weight" finds the journeys of least summed tie weight and reports path_cost. - path_centrality() takes the same criterion and cost, and gains "efficiency" and the stream measures "katz" (Beres et al., 2018), "pagerank" (Rozenshtein and Gionis, 2016) and "walk" (Oettershagen, Mutzel and Kriege, 2022). top = k returns the k most central vertices by closeness under "min_hops" or "shortest", computing only the searches that ranking needs. - reachability() takes criterion = "min_hops". - edge_centrality() scores the contacts themselves; the scores of the contacts arriving at a vertex reconcile exactly with its temporal betweenness. - centrality_series() gains the participation coefficient, measure = "participation" with groups. - metrics() gains "temporal_efficiency" and "temporal_diameter", with basis and traversal_time. Global measures, inter-event times and motifs - gaps() returns inter-event gaps per vertex or per pair. - persistence() measures neighbourhood persistence between consecutive bins, and turnover() reports volatility and fluctuability. - segregation() is the segregation-integration difference over time. - motifs() is the delta-temporal three-node motif census (Paranjape, Benson and Leskovec, 2017). Temporal communities and phases - temporal_communities() detects communities by generalised Louvain on ordinally coupled slices, and multislice_modularity() scores a partition. - match_communities() gives community labels a meaning that carries across time; community_change() measures how much the structure moved between bins, and community_trajectory() what each vertex did across it. - phases() detects temporal phases by clustering the between-bin similarity. Internal - The path search keeps the 0.5.0 speed-up for the default criterion and falls back to exhaustive search only where a criterion needs it: the pure foremost family, and searches from an unattained origin. - aricode, clue, cluster and multinet are not suggested: they served only as test oracles, and equivalence tests do not ship. Changes in version 0.5.1 (2026-10-07) - CRAN resubmission. The pathways() example pools three named sources instead of all fourteen, and the animate() examples draw fewer frames, so every example runs in well under a second. - thought_chains is documented as based on the Trees of Thought study (Saqr, López-Pernas and Törmänen, 2026) with about 20 percent of the records removed, dates and rates changed and anonymised, and the data augmented by simulation. - mooc_people cites its source chapter directly; a stale reference to a removed article is gone from the mooc_posts example. - The package help page carries the current Description, and a local-only reproduction script no longer ships. Changes in version 0.5.0 - First CRAN release. CRAN preparation: the maintainer is recorded as copyright holder, the README gives the CRAN installation line, and the animation article's GIFs carry alternative text. - durations() is up to 37 times faster on large logs (about 20,000 spells: 26 s to under 1 s). A spell between two vertices without declared activity is now built as its own fragment in one vectorised step, and each pair's fragments are indexed once rather than matched against every pair. Results are identical to 0.4.13. - The time-respecting path search behind paths(), reachability(), path_centrality() and pathways() is much faster. A search state's candidate entries are computed for all its ties at once, the ties leaving each vertex are indexed once, and state keys are built in one call. Forward searches also drop dominated states: a vertex reached no earlier than an existing state with fewer hops. Such a state can never be, or lead to, a shortest-foremost path, so arrival times, hop counts, path counts and betweenness are identical to 0.4.13. On the thought_chains data (23,017 contacts), path_centrality() drops from 314 s to 5 s and one-source paths() from 35 s to under 1 s. Backward searches are vectorised but not pruned. Changes in version 0.4.13 Breaking changes - dyn_reachability() is renamed reachability(). The old name still works, forwards every argument unchanged and warns with class dynet_deprecated; it will be removed in a future release. Note that sna also exports a reachability(): with both attached, the one attached last wins, so call Dynet::reachability() when in doubt. - dyn_centrality() is split in two, because its two scopes returned different things. centrality_series() is the old default (scope = "snapshot"): centrality in every window, a series per vertex. path_centrality() is the old scope = "temporal" for "closeness" and "betweenness": one value per vertex from time-respecting paths across the period, with no time column. Temporal "reach" and "reach_count" are reachability(). Neither new function has a scope argument, so each returns one shape. path_centrality(plot = TRUE) now draws, which the old temporal scope silently ignored. dyn_centrality() still works, returns exactly what it returned before and warns with class dynet_deprecated. The new names avoid cograph::centrality() and tna::centralities(). - centrality_series(measure = "strength") now weights each spell by the share of its duration inside the window (weight * overlap / duration) instead of counting its full weight in every window it touches. Tiled windows therefore add back up to the network's total weight rather than counting a long spell once per window. Point contacts keep their full weight in the window that holds them, window = 0 still uses full weights at the instant, and the part of a spell outside the observation period is not reassigned to observed windows. Degree and every binary measure are unchanged. snapshots(), animate() and the network plots still count each spell's full weight in every bin it touches, as networkDynamic::network.collapse() does; their documentation now says so. - plot(type = "events") marks where each link starts the way cograph's TNA styling does: the first 20% of each link, from its source, is dotted. The new edge_start_style and edge_start_length arguments, named as in cograph::splot(), change or turn off the mark. Row labels are now drawn in their actor's colour, so they work as the colour key, and nodes are larger. Bug fixes - A nodes = or ties = condition that cannot be evaluated, such as one naming a column the table does not have, now raises dynet_bad_selection (also dynet_bad_input) instead of base R's raw error, whose class changed in R-devel and failed the devel CI check. - metrics()' "temporal_density", "observed_pair_density", "onset_intensity" and "observed_pair_onset_intensity" no longer count time after the data end as exposure. Without explicit observation bounds, a last window reaching past the final spell divided by its full width, so tiled windows did not pool to the whole-period value. Integration now stops at the observation period, which defaults to the data's span, as tsna::tEdgeDensity() does. On school_contacts with weekly windows the final partial week's temporal density goes from 0.0013 to 0.0181; explicit observation_end values are honoured as before. - metrics()' "concurrent_nodes" and "concurrent_share" now require simultaneity. With a positive window they were read from the window's union snapshot, so a vertex tied to one partner early in the window and to another later was counted as concurrent although the two ties never overlapped. A vertex now counts when relations to two distinct neighbours are active at the same instant somewhere in the window; spells that only meet at a boundary do not overlap, and a point contact is concurrent with whatever is active at its timestamp. window = 0 results are unchanged. The result records the rule in the concurrency_window_rule attribute. mixing() is unaffected: it counts group pairs connected anywhere in the window and never implied simultaneity. Changes in version 0.4.12 Breaking changes - In a threaded log, a self-reply that loops = FALSE drops is now removed before the thread's lifetime is computed, so a dropped post no longer keeps its thread alive. Threaded networks built from logs with self-replies can have shorter spells than before; loops = TRUE is unchanged. New features - dynet() gains min_thread_posts: for a threaded log, threads with fewer surviving posts are dropped whole and reported with a message, so the "threads that never became an exchange" rule of the chapter-17 analysis is one argument rather than a hand-written filter. Requires thread; a value below 1 or a non-threaded log raises dynet_bad_input / dynet_needs_thread. - plot() on a mixing() result accepts a group name in highlight, which colours every flow into or out of that group. A highlight that matches no series raises dynet_unknown_highlight instead of drawing everything grey. - mooc_people carries expert_level, the chapter's label for the experience code, so the mixing attribute needs no recode. - The website article that rebuilt the MOOC network from the published CSV files was removed; vignette("ch17-temporal-networks") builds the same network from the bundled data in one call. The site deploy now clears files that are no longer built. - set_tie_sessions() gains breaks and labels: sessions can be cut on the time axis (breaks = c(7, 14) gives three weeks) instead of being derived by hand as a column and matched positionally against the spell table. Changes in version 0.4.7 Breaking changes - add_vertex_spells() and update_vertex_spells() now refuse input they cannot honour instead of accepting it silently. Supplying session to a network with no session scheme raises dynet_incompatible_vertex_spells rather than dropping the label; add_vertex_spells() used to discard it and return normally while update_vertex_spells() already errored on the same input. Supplying a column outside the vertex-spell schema to update_vertex_spells() raises dynet_unknown_column rather than returning the object unchanged, so a misspelled field is no longer a silent no-op. Performance - summary() gains temporal_density, FALSE by default. That one row integrates exact occupancy over every eligible ordered pair, so its cost is quadratic in the vertex count: on a 442-vertex forum network it alone took about 32 seconds, while the other fourteen rows were immediate. It now reads "not computed" unless asked for, and summary() on that network takes 0.3 seconds. Pass temporal_density = TRUE for the number. Bug fixes - as_dynet() placed per-edge attributes on the wrong spell when importing an undirected networkDynamic. dynet() canonicalises an undirected pair before sorting its spells, and the importer derived its ordering from the raw tail and head, so the two permutations disagreed whenever the endpoints were stored in the other order. Attributes now follow the canonical endpoints. - remove_ties() matched the start and end selectors with exact equality on doubles, so a spell that accumulated as 0.1 + 0.1 + 0.1 could not be removed by naming 0.3. Times are now compared with the same magnitude-relative tolerance the rest of the package uses. - dyn_centrality(measure = "closeness", scope = "temporal") returned Inf without a word when every reachable vertex was joined within one instant. Inf is still returned, since it is the honest limit, but a dynet_zero_latency warning now accompanies it. Documentation - Five statements that contradicted the code are corrected: kept self-loops are counted by degree and contribute two, snapshots(at = ) can return zero rows when the nearest bin holds no active tie, animate(seed = NULL) leaves the caller's random state advanced rather than restored, similarity(sessions = "separate") adds no session column, and only one of the four Krackhardt indices is an index of hierarchy. - set_tie_sessions() now documents that a full-length vector is matched positionally against the sorted spell table, not against the data frame the network was built from. Derive labels from as.data.frame(dn). - dynet() now documents that canonical spell column names -- duration, weight, session, thread, onset_censored, terminus_censored -- are dropped from tie attributes even when never named as arguments. - Internal specification identifiers that had leaked into the manual pages with no definition anywhere are replaced by the measure names they referred to. Changes in version 0.4.6 - New verb animate(): the measurement grid as a film, written to a GIF (gifski) or an mp4 or webm video (av), chosen by the extension of file. It takes the same four grid arguments as every measuring verb, so an animation shows exactly what snapshots() tabulates and what plot(dn, type = "snapshots") draws as a filmstrip, and a test pins that the three agree bin for bin. Each bin is drawn tween times, six by default. Between bins the vertices glide along the smoothstep curve, a tie about to appear fades in dotted and green, one about to vanish fades out dashed and vermilion, and with measure = node size follows a snapshot measure from dyn_centrality() on the same grid. Tie width follows weight on one scale fixed across the whole animation, so the same weight has the same width in every frame. A vertex not present in a bin is drawn as absent says: faded in place, parked out of sight at the edge of the layout and gliding in when it arrives and out when it leaves, or hidden; a present vertex with no tie is drawn as isolates says. A timeline strip under the network shows the grid, a marker at the current time, and the key; both the tie states and the strip can be turned off. Five layouts and a coordinate table. "spring", the default, lays out the union of every bin once; "circle", "oval" and "groups" are rings; "relaxed" re-runs cograph::layout_spring() per bin, seeded from the bin before it and held within max_displacement, then smooths every vertex's path with a centred triangular kernel, which halved the direction reversals between consecutive moves on school_contacts at a two per cent cost in structure. Under every layout but "relaxed" a vertex never moves, and every layout covers the whole vertex set, so a vertex never changes place because its neighbours came and went. The file goes to tempfile() unless file says otherwise, so nothing reaches the working directory by accident. gifski and av are Suggests; without the one the extension needs the verb raises dynet_needs_gifski or dynet_needs_av, and an extension it cannot write raises dynet_unknown_format. A bin holding nothing to draw is skipped with a message that counts the skipped bins. The tidy bin table comes back invisibly, one row per bin with time, nodes, ties, forming, dissolving and the bin's first rendered frame, as class dynet_animation with print(), summary() and as.data.frame(); as.data.frame(x, what = "frames") maps every rendered frame to its time. - New website article Animating a temporal network, a tutorial on animate() over the classroom and the MOOC forum data. - set_vertex_spells(dn, "ties") declares each vertex present from the start of its first tie spell to the end of its last, so a network built from a tie log alone can say when each vertex arrived and left. - New vignette ch17-temporal-networks: chapter 17 of Learning Analytics Methods and Tutorials (Saqr, 2024), "Temporal network analysis: Introduction, methods and analysis with R", re-run with Dynet's verbs in the chapter's own order -- build, active subnetwork, visualisation, graph-level and node-level measures, reachability, mixing. Its data is bundled as mooc_posts (2529 posts across 338 discussion threads of a MOOC forum, April to June 2013) and mooc_people (445 participants with their experience level), so the vignette reaches no network at render time. The two places where the chapter's own code changes its numbers -- the thread spell rule, and ties admitted before single-post discussions are dropped -- are named and measured rather than reproduced. - Backward routes on interval spells keep the route family of an unattained supremum. An interval spell is half-open, so the latest departure into a target is a supremum no journey reaches exactly. paths(direction = "backward") used to report NA hops, zero paths and no steps for such a vertex, which left every backward trajectory tree on interval data drawing only its source. The family that approaches the supremum is now reported in full -- hops, exact path count and reconstructed steps -- and attained is what records that the instant itself is not realised. Route reconstruction under sessions = "bounded" and sessions = "separate" is unchanged. - Reachability is anchored at each vertex's own presence. With vertex spells declared, dyn_reachability(), dyn_centrality(scope = "temporal") and the default origin of paths() start a vertex's forward search at its first appearance inside the window and its backward search at its last, instead of at the window bound. A vertex that entered the network late no longer scores zero; a backward search may anchor at the instant a vertex leaves. An explicit at is still used exactly. - dynet(nodes = ) names the vertices from a node table whose key is not name but which has a name column, as network(vertex.attrnames = ) does: edge endpoints and vertex spells given by the key are translated, and the key stays on the node table as an attribute. vertex.id, node, vertex and node.id are recognised as vertex keys. - dynet(vertex_spells = ) resolves its node, start and end columns through the alias table and ignores other columns, so a node table with onset and terminus columns is accepted as it is. - rename_nodes() takes the name of a vertex attribute whose values become the node names. Changes in version 0.4.4 - Every example, the README, both vignettes and the reproductions are written one call per line: a verb's result is named, and the next verb takes that object. No call is nested inside another. Nothing computed changed. Changes in version 0.4.3 - remove_ties(), remove_arcs() and update_ties() take ties as a condition on the spell table (ties = duration > 2), as induce_subgraph() already did; positions and masks still work. - On an undirected network a tie keeps its row position through an edit: dynet() now canonicalises endpoints before sorting, the order every rebuild uses, so update_ties(ties = 1:2) edits the rows the caller saw. - Bonacich power and information centrality on a singular system return NA under a warning of class dynet_kernel_singular instead of silently; the spectral warning and this one share the parent class dynet_measure_undefined. - The sample and indegree/outdegree deprecation warnings carry class dynet_deprecated; the duplicate-nodes warning carries dynet_duplicate_nodes. - Documentation states what was previously only implemented: assortativity correlates total degree at both ends of every arc; closeness centralisation uses this package's reachable-set closeness with maxima n - 1 (directed) and n - 2 (undirected); pshifts() orders simultaneous turns by speaker, group turn, then target in vertex order; temporal closeness is Inf when every reachable vertex is reached at zero latency; co-presence connects every member pair for the whole group span. Changes in version 0.4.2 - eigenvector, hub and authority are certified like eigenvector prestige: a snapshot whose spectral radius is zero or whose Perron root is repeated returns NA for that block under a warning of class dynet_eigen_undefined, instead of one arbitrary basis vector. - dynet() picks up a column named weight, weights or strength as the tie weight and says so; before, an unnamed weight column was silently replaced by ones. - window = 0 on the default grid now samples through the last observed instant, as tsna does; a positive window is unchanged. Changes in version 0.4.1 - Plot defaults: node-level bar charts and the frequency trajectory tree colour by vertex, in the network's own order, so a vertex keeps one colour across every view; plot(dn, type = "timeline") and "events" take step, a bin width (1/24 on a network in days is hourly), and the timeline is clipped to the declared observation window; collapse_network() and path_network() results have plot() methods with Dynet's rendering defaults, so plot(collapsed, layout = "oval") is the whole call. - dynet() keeps every column of the log it did not consume as a tie attribute, for interval, contact and threaded logs, so induce_subgraph(ties = group == "A_01") works on a freshly built network as it already did after as_dynet() and add_ties(). - dynet(thread_clock = "relative") puts each thread of a threaded log on its own clock, measured from the thread's first post: the Trees of Thought construction in one call. - Reproduction inst/reproduction/thought-chains/thought_chains.Rmd: the Trees of Thought analysis on the bundled thought_chains, with a gallery of every temporal view and result plot. - New bundled dataset thought_chains: the Trees of Thought reply table (code of a message to the code of the message it answers) with identities stripped, the bottom 20% of authors trimmed, the two sparse weekdays removed, the calendar shifted by whole weeks, and Evaluation merged with Acceptance into Approving. 23,017 links, 240 participants, nine codes. - Window edges are snapped onto the data's own boundaries. A grid built as start + k * step with a step such as 1/24 fell one ulp short of the exact spell boundaries a date-converted network carries, so a spell ending exactly at a window's start was counted inside it. Integer-hour and POSIXct-hour encodings of one network now give identical series (tests/testthat/test-bin-edge-snap.R). Changes in version 0.4.0 - New verb pathways(): whole time-respecting routes ranked by how often they are used, with print, summary, plot and as.data.frame(what = "steps"). - plot = TRUE on the thirteen measurement verbs draws as a side effect and still returns the tidy table, in the manner of hist(). - induce_subgraph(ties = ) takes a condition on the spell table, as nodes = already did. - as.data.frame(x, what = "diagnostics") exposes the prestige diagnostics; dynet_pshifts and dynet_collapsed_list gained the full method set and as.data.frame(x, session = ) reaches one session by argument. - New bundled dataset synthdata, a resampled synthetic twin of the Trees of Thought interaction data, with the reproduction under inst/reproduction/. - The dependency floor is R >= 4.1; the package defines its own %||%. Continuous checking on macOS, Windows and Linux (devel, release, oldrel-1, and a pinned 4.1). - Events landing just past a bin boundary are no longer discarded; the final default window is closed on its right edge. - Two wrong-output defects found by adversarial review were fixed, and the internal helper pages were removed from the manual. Changes in version 0.3 - The similarity() verb, centrality mode ("all", "out", "in"), node selection by condition, temporal reach, closeness and betweenness, bounded and session-aware path searches, per-hop traversal time, and the first two vignettes. - Public verbs dropped the dyn_ prefix except dyn_centrality() and dyn_reachability(). Changes in version 0.2.0 - First tidy, name-addressed release: one constructor for interval, contact, threaded and co-presence logs; every verb returns a one-row-per-observation data frame; values checked against the statnet ecosystem.