| Title: | Topological Connectivity Analysis for Numeric Data |
|---|---|
| Description: | Builds topologies on the vertex set of a graph from the closed neighbourhoods of its vertices, a closed-neighbourhood variant of the construction of Nada, El Atik and Atef (2018) <doi:10.1002/mma.4726>, and computes their connected components exactly through the specialization preorder, without enumerating the topology. For time series it builds the horizontal and natural visibility graphs (Luque et al., 2009, <doi:10.1103/PhysRevE.80.046103>; Lacasa et al., 2008, <doi:10.1073/pnas.0709247105>) at equally or unequally spaced instants, deciding the natural criterion exactly under IEEE 754 double arithmetic, and from their directed versions the forward and backward topologies, a bitopological space in the sense of Kelly (1963) <doi:10.1112/plms/s3-13.1.71>, together with the Alexandrov topology of reachability. Reading a series backwards, with its instants reflected, exchanges the two topologies, so the difference of their numbers of components changes sign. |
| Authors: | José Mauricio Gómez Julián [aut, cre] |
| Maintainer: | José Mauricio Gómez Julián <[email protected]> |
| License: | GPL (>= 3) |
| Version: | 0.4.0 |
| Built: | 2026-10-09 13:11:02 UTC |
| Source: | https://github.com/IsadoreNabi/topologyR |
For each factor , builds the threshold neighbourhoods of the data
with radius and reports the size of the base they
generate and the sizes of its largest and smallest members. It shows how the
choice of the factor changes the granularity of a metric-threshold topology;
it does not select a factor, because no criterion of optimality is defined.
analyze_topology_factors(data, factors = NULL, plot = TRUE)analyze_topology_factors(data, factors = NULL, plot = TRUE)
data |
Numeric vector with at least two elements and no missing values. No default. |
factors |
Positive numeric vector of divisors of the interquartile
range. Default |
plot |
Logical scalar; if |
The neighbourhood of observation is the set of indices with
. The family counted in base_size consists of
the empty set, the whole index set and every non-empty intersection of two
neighbourhoods, deduplicated. Each neighbourhood collects the observations
whose values lie in a closed interval, and an intersection of several such
sets is the intersection of the two whose intervals have the largest left
and the smallest right endpoint; the pairwise intersections are therefore
all the non-empty finite intersections, and together with the whole index
set they form the base generated by the neighbourhoods.
A data.frame with one row per factor and the columns
factor (numeric), threshold (numeric, ),
base_size (integer, number of sets in the family described in
Details, the empty set and the whole index set included),
max_set_size and min_set_size (integers, largest and
smallest cardinality among the non-empty intersections of two
neighbourhoods). If plot = TRUE, the data frame carries an
attribute "plot" with a ggplot object.
These are metric-threshold neighbourhoods on the values, not the
graph-induced neighbourhoods of the visibility-graph pipeline; the radius
is a tuning choice of the user. Up to version 0.3.0 the two size columns
were taken over the whole family, so min_set_size was always 0 (the
empty set) and max_set_size always (the whole index set);
they are now taken over the neighbourhood intersections, which is where the
factor acts.
stats::IQR() for the radius; 'ggplot2' (Imports) for the optional
plot.
calculate_topology() for a single threshold,
calculate_thresholds() for data-driven radii, generate_topology() for
graph-induced topologies.
set.seed(1) results <- analyze_topology_factors(rnorm(50), plot = FALSE) resultsset.seed(1) results <- analyze_topology_factors(rnorm(50), plot = FALSE) results
Given the forward topology and the backward topology
computed by generate_topology(), reports their numbers of
connected components and of base sets, two summaries of the difference
between the component counts, the pairwise connectedness of the
bitopological space, decided exactly, and, when supplied, comparisons with the
Alexandrov and the undirected topologies.
bitopology_invariants( forward, backward, n_elements, undirected = NULL, alexandrov = NULL )bitopology_invariants( forward, backward, n_elements, undirected = NULL, alexandrov = NULL )
forward |
Result of |
backward |
Result of |
n_elements |
Integer scalar: the number of points; must equal the number of points covered by the components. No default. |
undirected |
Optional result of |
alexandrov |
Optional result of |
The two base sizes are equal. Let be a binary relation on
a finite non-empty set , with rows and
columns , and let be the set
of non-empty intersections of non-empty families of rows, likewise
. Then .
Proof. For let
(with ) and for let
. Both maps
reverse inclusion and , , so
and ; hence and
are mutually inverse bijections between the intersections of rows
over all , the intents, and the intersections of columns over all
, the extents, in the terminology of formal concept analysis (Ganter &
Wille, 1999), and there are as many of each. is the set
of intents without , unless is a row, and without
, which is an intent exactly when no column is (the
intersection of all rows is the set of that are related to every
); symmetrically for with rows and columns
exchanged. The two corrections are the same two events, so the cardinals
agree. The closed forward and backward neighbourhoods are the rows and
columns of the reflexive closure of the arc relation, and the engine keeps
exactly the non-empty intersections, so forward_base_size equals
backward_base_size whenever both bases are complete. For the same
reason nada_forward_base_gain equals nada_backward_base_gain
whenever both are defined.
Comparison with the Alexandrov topology. for every digraph, and the Alexandrov topology of the reversed
reachability is contained in ; both have as many base sets as
the digraph has strongly connected components. Each gain is non-negative,
because every smallest open set of is an intersection of
members of the subbase and so belongs to , and , being
finer, separates at least as many points as . The gains count
sets of the construction; they do not measure how much finer
is than , since the size of an intersection-closed base depends
on the subbase that generates it. On a visibility graph the Alexandrov
topology is the chain of sets , so the gains
are .
Pairwise connectedness. is pairwise
disconnected when is the union of two disjoint non-empty sets, the
first open in and the second open in . The
function decides it exactly from the two subbases, which are always
complete, without enumerating either topology. Let and
be the smallest open sets of in and
, the intersections of the members of each subbase that contain
, and let be the digraph on with an arc
when or . A set is
open in with its complement open in if and only if
no arc of leaves : means
for every , and
means for
every , that is, and imply
. A proper non-empty set that no arc leaves exists if and only
if is not strongly connected, and then a strongly connected
component that no arc leaves is one; the function takes the first component
that Tarjan's (1972) algorithm completes. The smallest open sets cost the
intersections of the members that contain each point,
over the members of the two subbases (expected, since
intersect() hashes), and the search costs for the
arcs of ; with at most members of at most
points, the total is at worst, and much less when the
neighbourhoods are small. The accompanying article
decides the property by searching the enumerated topologies (its algorithm
5), with an undecided answer when an enumeration is cut; this criterion
gives the same answer whenever that search decides, and never leaves it
undecided.
On visibility graphs the answer is known in advance. When every arc
goes from a lower to a higher index, as in every directed visibility graph,
and , the space is pairwise disconnected. For each
, the final segment is the union of the
closed out-neighbourhoods of its points, each of which lies inside it, so it
is open in ; in the same way the initial segment
is open in ; and the two segments
partition . On the results of generate_bitopology() the check
therefore carries no information about the series: it answers
FALSE, with a witness found by the search (the segments
and are always one, but the search
can return another). It can answer
TRUE on digraphs with cycles, whose topologies are computed with
generate_topology().
A list with:
Integers and
: the numbers of connected components, exact even when a base
was truncated.
Logical.
Integers and
: the numbers of sets in the two bases, or NA when
the corresponding base was truncated.
Logical: the
base_complete flags of the two topologies.
Numeric in :
.
Integer .
List with pairwise_connected (logical, never
NA: the property is decided exactly, without enumerating either
topology) and, when it is FALSE, clopen_forward, a proper
non-empty -open set whose complement is -open,
and clopen_backward, that complement.
NULL unless alexandrov is given;
otherwise a list with alexandrov_base_size,
alexandrov_components, alexandrov_connected,
nada_forward_base_gain and nada_backward_base_gain
( and , NA with a
truncated base) and the same differences divided by
(nada_forward_relative_gain,
nada_backward_relative_gain).
NULL unless undirected is given;
otherwise a list with undirected_components,
undirected_connected and undirected_base_size
(NA with a truncated base).
irreversibility_components and asymmetry_direction are
descriptive statistics of the pair of topologies of one series. By the
time-reversal theorem T1 (see generate_bitopology()), reading the series
backwards with its instants reflected leaves unchanged and
changes the sign of , so both vanish on a palindrome observed on a
palindromic grid. Neither proves anything about the reversibility of the
process that generated the series, and a positive or negative is
not a prediction of any particular shape of the series. compares
counts: two different partitions can have the same number of components.
The connected components are exact whether or not a base was truncated;
the base sizes and the gains are not reported for a truncated base, because
the number of sets found before the cut is only a lower bound. Up to
version 0.2.0 the function also returned irreversibility_base, the
normalized difference of the two base sizes, which the theorem above makes
identically zero; it was removed in 0.3.0.
Base R only.
Ganter, B., & Wille, R. (1999). Formal concept analysis: Mathematical foundations. Springer. https://doi.org/10.1007/978-3-642-59830-2
Kelly, J. C. (1963). Bitopological spaces. Proceedings of the London Mathematical Society, s3-13(1), 71-89. https://doi.org/10.1112/plms/s3-13.1.71
Tarjan, R. (1972). Depth-first search and linear graph algorithms. SIAM Journal on Computing, 1(2), 146-160. https://doi.org/10.1137/0201010
generate_bitopology() for the complete pipeline and theorem T1.
g <- horizontal_visibility_graph(c(3, 1, 4, 1, 5, 9, 2, 6), directed = TRUE) tf <- generate_topology(g$out_adjacency, g$n, max_open_sets = 0L) tb <- generate_topology(g$in_adjacency, g$n, max_open_sets = 0L) inv <- bitopology_invariants(tf, tb, g$n) inv$asymmetry_direction inv$forward_base_size == inv$backward_base_size small <- generate_bitopology(c(3, 1, 4)) small$invariants$pairwiseg <- horizontal_visibility_graph(c(3, 1, 4, 1, 5, 9, 2, 6), directed = TRUE) tf <- generate_topology(g$out_adjacency, g$n, max_open_sets = 0L) tb <- generate_topology(g$in_adjacency, g$n, max_open_sets = 0L) inv <- bitopology_invariants(tf, tb, g$n) inv$asymmetry_direction inv$forward_base_size == inv$backward_base_size small <- generate_bitopology(c(3, 1, 4)) small$invariants$pairwise
Computes five scales of the data that can serve as radii for the
threshold neighbourhoods of calculate_topology() and
visualize_topology_thresholds(). They are descriptive scales, not
estimates of an optimal radius.
calculate_thresholds(data)calculate_thresholds(data)
data |
Numeric vector with at least two elements and no missing values. No default. |
A named list of five numeric scalars:
Mean of the gaps between consecutive sorted values.
Median of the gaps between consecutive sorted values.
Standard deviation of the data.
Interquartile range divided by 4.
The -th smallest of the
pairwise distances, with and the
position capped at the number of distances.
The dbscan entry keeps its historical name, but it is a single
order statistic of all pairwise distances; it is not the distance from any
observation to its -th nearest neighbour.
stats::dist(), stats::median(), stats::sd() and
stats::IQR().
calculate_topology(), visualize_topology_thresholds().
set.seed(1) calculate_thresholds(rnorm(100))set.seed(1) calculate_thresholds(rnorm(100))
Builds the threshold neighbourhoods of the data with a given radius and
returns the number of sets in the base they generate, counted as in
analyze_topology_factors().
calculate_topology(data, threshold)calculate_topology(data, threshold)
data |
Numeric vector with at least two elements and no missing values. No default. |
threshold |
Non-negative numeric scalar, the radius. No default. |
An integer scalar: the number of distinct sets among the
empty set, the whole index set and the non-empty intersections of two
neighbourhoods (see analyze_topology_factors() for why these are all the
finite intersections).
The count includes the empty set and the whole index set. Missing values are rejected; up to version 0.3.0 they were silently dropped from every neighbourhood.
Base R only.
analyze_topology_factors(), calculate_thresholds().
set.seed(1) calculate_topology(rnorm(30), threshold = 0.5)set.seed(1) calculate_topology(rnorm(30), threshold = 0.5)
Runs generate_topology() on the complete graph over the indices of the
data. Every closed neighbourhood of the complete graph is the whole index
set, so the result is the indiscrete topology, whose only open sets are the
empty set and the whole set, and the space is connected.
complete_topology(data, verify_axioms = FALSE)complete_topology(data, verify_axioms = FALSE)
data |
Numeric vector with at least two elements and no missing values; only its length is used. No default. |
verify_axioms |
Logical scalar passed to |
The list returned by generate_topology(): base {V},
two open sets when enumerated, one component.
The construction uses closed neighbourhoods ,
and in the complete graph for every vertex. The result is a
reference point: the coarsest topology on the set.
The compiled engine of generate_topology() (via 'Rcpp').
simplest_topology() for the opposite extreme, the discrete
topology.
result <- complete_topology(c(1, 2, 3, 4, 5)) result$n_open_sets result$connectedresult <- complete_topology(c(1, 2, 3, 4, 5)) result$n_open_sets result$connected
Computes the Alexandrov topology of the reachability preorder of a directed
graph: its open sets are the sets closed under following arcs, and the
smallest open set containing is the set of vertices reachable from
by directed paths, included. It accepts any digraph and
reports which of three cases it received; generate_bitopology() applies
it to directed visibility graphs, where it is always the same chain.
generate_alexandrov_topology( out_adjacency, n_elements, max_open_sets = 0L, check_connected = TRUE, verify_axioms = FALSE, expect = c("any", "index_ordered_dag") )generate_alexandrov_topology( out_adjacency, n_elements, max_open_sets = 0L, check_connected = TRUE, verify_axioms = FALSE, expect = c("any", "index_ordered_dag") )
out_adjacency |
List of length |
n_elements |
Integer scalar, at least 1: the number of vertices. Must
equal |
max_open_sets |
Non-negative integer scalar (default 0, no
enumeration): the enumeration of the open sets stops as soon as their
number exceeds it. There can be up to |
check_connected |
Logical scalar (default |
verify_axioms |
Logical scalar (default |
expect |
Character scalar: |
Three cases are handled. (1) Every arc goes from a lower to a higher index,
as in directed visibility graphs: the reachable sets are propagated in
reverse index order as bitsets, , in
word operations. (2) An acyclic digraph in another order:
the vertices are first re-indexed along a topological order. (3) A digraph
with directed cycles: mutually reachable vertices have the same reachable
set, so the topology is computed on the condensation, found with the
algorithm of Tarjan (1972), and expanded back; the expansion is a bijection
between the open sets of the condensation and those of the input, so every
reported field carries over exactly. A set is closed under following arcs
if and only if it is closed under following directed paths, which is why
propagating along single arcs gives the reachable sets.
A list with the elements of generate_topology() and three
more:
The same list of integer vectors: the distinct sets of reachable vertices, which form the minimal base of the topology. They are not closed under intersection in general (see Methodological notes).
Always TRUE: the minimal base is computed
exactly.
Connectedness and the connected components,
as in generate_topology().
The enumerated open sets
and their flags, as in generate_topology().
Logical: whether every arc went from a lower to a higher index.
Logical: whether the digraph had no directed cycle.
List of integer vectors: the strongly connected
components with more than one vertex, whose vertices the topology
cannot tell apart; list() when there are none.
The base is minimal, not intersection-closed. For the arcs
, , , , the reachable
sets are , , and ;
the first two meet in , which is open, as the union of
and , but is not one of them. The number of base sets
is the number of strongly connected components.
Relation with the forward topology. For every digraph, the
Alexandrov topology is contained in the forward topology generated by the
closed forward neighbourhoods (see generate_bitopology()):
each reachable set is the union of the of its points. It is not
contained in the undirected topology in general: on the path
, is open here and not in the
undirected topology, whose smallest open set containing 4 is
.
The connection between finite topologies and preorders goes back to Alexandroff (1937), after whom these spaces are named.
The compiled engine (C++ through 'Rcpp') for the propagation and the components; base R for the condensation.
Alexandroff, P. (1937). Diskrete Räume. Recueil Mathématique (Matematicheskii Sbornik), Nouvelle Série, 2(44)(3), 501-519. https://www.mathnet.ru/eng/sm5579
Tarjan, R. (1972). Depth-first search and linear graph algorithms. SIAM Journal on Computing, 1(2), 146-160. https://doi.org/10.1137/0201010
generate_topology() for the topologies of closed
neighbourhoods, generate_bitopology() for the time-series pipeline.
g <- horizontal_visibility_graph(c(3, 1, 4, 1, 5), directed = TRUE) alex <- generate_alexandrov_topology(g$out_adjacency, g$n) alex$base alex$connected cross <- generate_alexandrov_topology(list(c(3L, 4L), c(3L, 4L), integer(0), integer(0)), 4L) cross$base cyc <- generate_alexandrov_topology(list(2L, 3L, 1L), 3L) cyc$collapsed_classesg <- horizontal_visibility_graph(c(3, 1, 4, 1, 5), directed = TRUE) alex <- generate_alexandrov_topology(g$out_adjacency, g$n) alex$base alex$connected cross <- generate_alexandrov_topology(list(c(3L, 4L), c(3L, 4L), integer(0), integer(0)), 4L) cross$base cyc <- generate_alexandrov_topology(list(2L, 3L, 1L), 3L) cyc$collapsed_classes
Builds the directed visibility graph of a series (horizontal or natural, at
equally or unequally spaced instants) and from it four topologies on the
observations: the undirected topology of the closed
neighbourhoods, the forward topology of the closed forward
neighbourhoods , the backward
topology of the closed backward neighbourhoods
, and optionally the Alexandrov
topology of reachability; then computes their invariants with
bitopology_invariants(). The triple is a
bitopological space in the sense of Kelly (1963).
generate_bitopology( series, graph_type = c("hvg", "nvg"), max_open_sets = 0L, max_base_sets = 100000L, alexandrov = TRUE, times = NULL )generate_bitopology( series, graph_type = c("hvg", "nvg"), max_open_sets = 0L, max_base_sets = 100000L, alexandrov = TRUE, times = NULL )
series |
Numeric vector with at least two observations and no
|
graph_type |
Character scalar: |
max_open_sets |
Non-negative integer scalar (default 0, no
enumeration), passed to |
max_base_sets |
Positive integer scalar (default 100,000), passed to
|
alexandrov |
Logical scalar (default |
times |
|
Every arc of a directed visibility graph goes from the earlier to the later
observation, so the out-neighbour lists give and the
in-neighbour lists ; the same engine computes the three
topologies of closed neighbourhoods. Consecutive observations always see
each other, so the digraph contains the path ,
reachability is the total order, and the Alexandrov topology is the same
chain of sets for every series; it describes the
class of visibility graphs and not the series, and alexandrov = FALSE
skips it. Because every arc goes forward in time, two more comparisons are
constant on the class: is open in (the last
observation has no later neighbour) and not in (the only member
of the backward subbase that contains is its closed
in-neighbourhood, which also contains , since consecutive
observations see each other), so the two topologies differ for
every series with , a reversible one included; and the
bitopological space is pairwise disconnected for every such series (see
bitopology_invariants()). What T1 relates is of a series to
of its reversal, not to of the same
series.
Theorem T1 (time reversal). Let a series be observed at
strictly increasing instants , write ,
(what time_reverse() returns) and
. Under either visibility criterion, is
an edge for if and only if
is an edge for ; carries the closed
forward neighbourhood of for onto the
closed backward neighbourhood of for , and the
backward one onto the forward one; hence is a homeomorphism from
onto and from
onto . Consequently
and
, the two base sizes are
exchanged, asymmetry_direction, , changes sign,
and irreversibility_components,
, is unchanged.
Proof. The horizontal criterion for involves only the
values strictly between them, which carries onto those strictly
between and , with the same values. For the natural
criterion, the quantity of natural_visibility_graph() computed for
at equals for , term by term, so each condition transfers to the
reflected pair. Arcs run from the lower index to the higher one and
inverts that order, so forward closed neighbourhoods become backward ones;
a bijection between the subbases of two topologies that commutes with the
point map is a homeomorphism of the topologies they generate, and it carries
bases and connected components onto each other.
A list with graph (the directed visibility graph, as
returned by horizontal_visibility_graph() or natural_visibility_graph()
with directed = TRUE), undirected, forward and
backward (results of generate_topology()), alexandrov (the
result of generate_alexandrov_topology(), or NULL) and
invariants (the result of bitopology_invariants()).
What T1 says and what it does not. T1 fixes the meaning of the sign
of asymmetry_direction: reading the series backwards, with its
instants reflected, changes it. When the series and the gaps between its
instants both read the same in the two directions (a palindrome on a
palindromic grid; with times = NULL, a palindrome), the reversed
series is the same series up to a translation of time and the asymmetry is
exactly zero. T1 has no converse: a non-zero asymmetry does not show that
the process that generated the series is irreversible, and a zero asymmetry
does not show that it is reversible. The probabilistic statement it
supports is about laws: if the joint law of the observations together with
their instants is invariant under the joint reversal (values read
backwards, instants reflected), the law of asymmetry_direction is
symmetric about zero, the mass at zero included; with
times = NULL, or with fixed instants whose gaps read the same in
both directions, the condition is that the law of the values equals the law
of their reversal, and so it is for the horizontal criterion with any fixed
instants, which it does not use. For the natural criterion with fixed
instants whose gaps are not palindromic, reversing the values alone is
another operation, to which this statement does not apply.
None of this says anything about stationarity or about the generating
process. For a statistical treatment of time
irreversibility with visibility graphs, see Lacasa et al. (2012), who
estimate it by the Kullback-Leibler divergence between the in- and
out-degree distributions of the directed horizontal visibility graph.
Instants. Reversing the values over the same instants is a different
operation as soon as the gaps between consecutive instants are not
palindromic. The horizontal criterion does not use the instants, so for it
the two operations give the same graphs and T1 holds for both; for the
natural criterion T1 can fail for the reversal of the values alone: with
the values 0, 1, 2 at the instants 0, 1, 3, it creates the edge
, which neither the original series nor its reversal with
reflected instants has; see time_reverse(). With the natural criterion the theorem holds exactly in
the computation as well, under the hypotheses of
natural_visibility_exactness, because the criterion is then decided
exactly and negating the instants is exact.
horizontal_visibility_graph() or natural_visibility_graph(),
generate_topology(), generate_alexandrov_topology() and
bitopology_invariants(); the compiled engine through 'Rcpp'.
Kelly, J. C. (1963). Bitopological spaces. Proceedings of the London Mathematical Society, s3-13(1), 71-89. https://doi.org/10.1112/plms/s3-13.1.71
Lacasa, L., Nuñez, A., Roldán, É., Parrondo, J. M. R., & Luque, B. (2012). Time series irreversibility: A visibility graph approach. The European Physical Journal B, 85(6), Article 217. https://doi.org/10.1140/epjb/e2012-20809-8
bitopology_invariants(), time_reverse(),
generate_topology(), generate_alexandrov_topology().
series <- c(3, 1, 4, 1, 5, 9, 2, 6) bt <- generate_bitopology(series, graph_type = "hvg") bt$invariants$forward_components bt$invariants$backward_components bt$invariants$asymmetry_direction rv <- time_reverse(series) generate_bitopology(rv$series, graph_type = "hvg")$invariants$asymmetry_direction t <- c(0, 1, 3, 4, 7, 8, 9, 12) bn <- generate_bitopology(series, graph_type = "nvg", times = t) rn <- time_reverse(series, t) bn$invariants$asymmetry_direction generate_bitopology(rn$series, graph_type = "nvg", times = rn$times)$invariants$asymmetry_directionseries <- c(3, 1, 4, 1, 5, 9, 2, 6) bt <- generate_bitopology(series, graph_type = "hvg") bt$invariants$forward_components bt$invariants$backward_components bt$invariants$asymmetry_direction rv <- time_reverse(series) generate_bitopology(rv$series, graph_type = "hvg")$invariants$asymmetry_direction t <- c(0, 1, 3, 4, 7, 8, 9, 12) bn <- generate_bitopology(series, graph_type = "nvg", times = t) rn <- time_reverse(series, t) bn$invariants$asymmetry_direction generate_bitopology(rn$series, graph_type = "nvg", times = rn$times)$invariants$asymmetry_direction
Builds the topology on the vertices of a graph that is
generated by the closed neighbourhoods , computes
its connected components exactly, and optionally enumerates its open sets.
Given undirected adjacency lists it is the undirected construction; given the
out-neighbour or in-neighbour lists of a digraph it gives the forward or the
backward topology. Use it directly for any graph, or through
generate_bitopology() for time series.
generate_topology( adjacency, n_elements, max_open_sets = 1000000L, max_base_sets = 100000L, check_connected = TRUE, verify_axioms = FALSE )generate_topology( adjacency, n_elements, max_open_sets = 1000000L, max_base_sets = 100000L, check_connected = TRUE, verify_axioms = FALSE )
adjacency |
List of length |
n_elements |
Integer scalar, at least 1: the number of vertices. Must
equal |
max_open_sets |
Non-negative integer scalar (default 1,000,000). The
enumeration of the open sets stops as soon as their number exceeds it;
|
max_base_sets |
Positive integer scalar (default 100,000). The closure of the subbase under intersections stops at the first new intersection that makes the base exceed this number of sets. It is not a strict cap on the size of the returned base: the whole subbase is always kept, and so is the intersection that crosses the limit. |
check_connected |
Logical scalar (default |
verify_axioms |
Logical scalar (default |
The base is computed by wavefront propagation: each new intersection is
intersected, in the next round, with every set already in the base, until a
round produces nothing new. Every new set is a proper subset of a set
produced in the previous round, so the closure ends after at most
rounds; the base itself can have exponentially many sets in , which
is what max_base_sets guards against. The components are computed
from the signatures :
and are comparable when or
, and the components are those of the
comparability graph, found by breadth-first search in
word operations. The open sets
are enumerated by the same wavefront scheme with unions, which can produce
up to sets; when the space is already known to be disconnected,
the engine warns that the enumeration is not needed for connectivity.
A list with:
List of integer vectors: the distinct closed
neighbourhoods .
List of integer vectors: the distinct non-empty finite
intersections of members of the subbase. When base_complete is
FALSE it is a subfamily that contains the whole subbase.
Logical: whether the intersection closure finished
within max_base_sets.
Logical: whether the space is connected; NA if
check_connected = FALSE.
List of integer vectors: the connected components,
exact whether or not the base is complete (see Methodological notes);
an empty list if check_connected = FALSE and .
Integer: rounds of the intersection closure.
List of integer vectors with the enumerated open sets,
or NULL if max_open_sets = 0.
Integer: the number of enumerated open sets, or
NA if the enumeration was not requested.
Logical: TRUE if the enumerated family
is the whole topology; FALSE if the enumeration was requested
but cannot be certified complete, because max_open_sets cut it
or because the base was incomplete; NA if the enumeration was
not requested.
Integer: rounds of the union closure.
Logical, present only when verify_axioms = TRUE
and topology_complete is TRUE.
Character, present only if that check fails.
Closed neighbourhoods. Nada, El Atik and Atef (2018, Definition 3.1)
take as subbase the post classes of the adjacency relation, which for a
loopless graph are the open neighbourhoods . This function uses the
closed neighbourhoods , so that every vertex lies in its own
subbase element: the subbase covers the vertex set and the smallest open set
containing is contained in . The two constructions give
different topologies; on the complete graph, for instance, the closed
variant gives the indiscrete topology and the open one the discrete
topology.
Why the components are exact, even from a truncated base. In a finite
space every point has a smallest open set , the
intersection of the members of the subbase that contain . For any
family with subbase , every member of
that contains contains if and only if
: if , every open set containing
contains ; conversely, the members of the subbase that contain
are among those of , and their intersection is . The
relation is the specialization preorder.
Let be the equivalence generated by comparability. Each class
is open, because it contains for each of its points (every
satisfies ), and closed, because its complement
is a union of other classes; so every connected component lies inside one
class. Each class is connected: if a class split into two non-empty parts
and , both relatively open, some and
would be comparable, say ; the open set that cuts
out contains , hence , a contradiction (the case
is symmetric). The components are therefore the classes, and
since the engine places the whole subbase in the base before the closure
can be cut, the signatures it compares always give the same classes.
What the base size measures. The number of sets in the base is a property of the construction, not of the topology: two subbases that generate the same topology can have intersection closures of different sizes.
The compiled engine (C++ through 'Rcpp').
Nada, S., El Atik, A. E. F., & Atef, M. (2018). New types of topological structures via graphs. Mathematical Methods in the Applied Sciences, 41(15), 5801-5810. https://doi.org/10.1002/mma.4726
generate_bitopology() for the time-series pipeline,
generate_alexandrov_topology() for the Alexandrov topology of
reachability, is_topology_connected_exact() for an enumerated topology,
and the user manual in system.file("manuals", package = "topologyR").
g <- horizontal_visibility_graph(c(3, 1, 4, 1, 5)) topo <- generate_topology(g$adjacency, g$n, max_open_sets = 0L) topo$connected topo$components topo$topology_complete full <- generate_topology(g$adjacency, g$n, verify_axioms = TRUE) full$n_open_sets full$topology_complete full$axioms_okg <- horizontal_visibility_graph(c(3, 1, 4, 1, 5)) topo <- generate_topology(g$adjacency, g$n, max_open_sets = 0L) topo$connected topo$components topo$topology_complete full <- generate_topology(g$adjacency, g$n, verify_axioms = TRUE) full$n_open_sets full$topology_complete full$axioms_ok
Builds the horizontal visibility graph of a series (Luque et al., 2009): two
observations are adjacent when every observation strictly between them is
strictly lower than both. With directed = TRUE it also returns the
arcs from the earlier to the later observation of each edge, which
generate_bitopology() turns into the forward and backward topologies.
The instants of observation enter only through their order, so
times is validated and recorded but does not change the graph.
horizontal_visibility_graph(series, directed = FALSE, times = NULL)horizontal_visibility_graph(series, directed = FALSE, times = NULL)
series |
Numeric vector without |
directed |
Logical scalar (default |
times |
|
The series is scanned from left to right with a stack whose values decrease
strictly from bottom to top, in time. When observation
arrives, every stacked index with is popped and
joined to : nothing between and reaches ,
or it would have popped earlier. The index left on top, if any, is
higher than and is joined to unless a popped value equals
, which blocks at height . Every other earlier index
is blocked: one deeper in the stack by that top, which is higher than
; one popped before by the index that popped it, which lies in
between and is at least as high. Each edge is thus reported exactly once,
always with from < to.
A list with:
data.frame with integer columns from and
to, from < to, one row per edge.
Integer: the number of observations.
Integer: the number of edges.
List of integer vectors: element i holds the
neighbours of observation i, its open neighbourhood
; generate_topology() adds i itself.
Numeric vector: the instants used.
Only with directed = TRUE: list whose
element i holds the later neighbours of i.
Only with directed = TRUE: list whose
element i holds the earlier neighbours of i.
The criterion uses comparisons of values and nothing else, so the graph is
computed exactly on the numbers received, is invariant under any strictly
increasing transformation of the values, and does not depend on the
spacing of the instants. Decimal data are converted to binary doubles
before they reach the function, and the graph is that of the doubles. A
conversion that rounds each decimal to the nearest double keeps the order
of the decimals, and keeps distinct any two with at most fifteen
significant digits in the normal range (magnitudes from about
to ), because their
relative gap, at least , exceeds the relative spacing of
doubles, at most ; outside that range distinct decimals can
collapse:
, and all
become zero, and the edge between the first and the third disappears.
The function stops with an error when the floating-point environment does not provide IEEE 754 double arithmetic (see natural_visibility_exactness for the test). Under the denormals-are-zero mode, which some libraries switch on, the comparisons would treat every subnormal value as zero; the flush-to-zero mode, which changes results of arithmetic and not these comparisons, is refused as well, because the same test guards the natural criterion, which depends on it.
The compiled engine (C++ through 'Rcpp').
Luque, B., Lacasa, L., Ballesteros, F., & Luque, J. (2009). Horizontal visibility graphs: Exact results for random time series. Physical Review E, 80(4), Article 046103. https://doi.org/10.1103/PhysRevE.80.046103
natural_visibility_graph(), generate_bitopology(),
time_reverse().
series <- c(3, 1, 4, 1, 5, 9, 2, 6) g <- horizontal_visibility_graph(series) g$n_edges head(g$edges) gd <- horizontal_visibility_graph(series, directed = TRUE) gd$out_adjacency[[1]] gd$in_adjacency[[5]]series <- c(3, 1, 4, 1, 5, 9, 2, 6) g <- horizontal_visibility_graph(series) g$n_edges head(g$edges) gd <- horizontal_visibility_graph(series, directed = TRUE) gd$out_adjacency[[1]] gd$in_adjacency[[5]]
Builds the co-occurrence graph of a family of sets, in which two elements
are adjacent when some member of the family contains both, and reports
whether that graph is connected. It is retained from version 0.1.0 as a
quick necessary-condition screen; the exact test of topological
connectedness is generate_topology() for a topology given by a subbase and
is_topology_connected_exact() for a fully enumerated topology.
is_topology_connected(topology)is_topology_connected(topology)
topology |
A list of integer vectors (positive labels), read as a family of subsets of the labels that appear in it. A full topology, a base or a subbase are all admissible. No default. |
The ground set is taken to be the set of labels that appear in some member, and the graph is traversed from the first of them. Memory grows with the square of the largest label, because the graph is stored as a dense adjacency matrix.
A logical scalar: TRUE if the co-occurrence graph on
the labels that appear in the family is connected, FALSE if it is
not or if the family is empty.
For any family of open sets that covers the ground set, a topologically
connected space has a connected co-occurrence graph: if the graph split into
two non-empty parts, every member would lie inside one part, each part would
be a union of members and hence open, and the two parts would disconnect the
space. A disconnected graph therefore proves that the space is disconnected.
The converse fails: the discrete topology on {1, 2}, given by its open sets
{}, {1}, {2} and {1, 2}, is disconnected while its co-occurrence
graph is connected, and any list that contains the whole ground set makes
the graph complete. A TRUE answer is thus inconclusive.
Base R only.
is_topology_connected_exact() for the exact test on an enumerated
topology, generate_topology() for exact components from a subbase,
is_topology_connected2() and is_topology_connected_manual() for the
other legacy diagnostics.
is_topology_connected(list(c(1, 2, 3), c(3, 4, 5))) is_topology_connected(list(c(1, 2), c(3, 4))) is_topology_connected(list(integer(0), 1L, 2L, c(1L, 2L)))is_topology_connected(list(c(1, 2, 3), c(3, 4, 5))) is_topology_connected(list(c(1, 2), c(3, 4))) is_topology_connected(list(integer(0), 1L, 2L, c(1L, 2L)))
Decides whether a finite topological space, given by the complete list of
its open sets, is connected: it is connected if and only if no proper
non-empty open set has an open complement. Use it on the topology
element of a result whose topology_complete is TRUE; for a
topology given by a subbase, generate_topology() returns the components
without enumerating anything.
is_topology_connected_exact(topology, n_elements)is_topology_connected_exact(topology, n_elements)
topology |
List of integer vectors: the open sets of a topology on
|
n_elements |
Integer scalar, at least 1: the number of points. No default. |
Each set is encoded as a bitset and stored in a hash set; the function scans
the open sets once and looks up each complement, in
word operations when the hash spreads
the sets evenly over the table (the usual uniform-hashing assumption; a
fixed hash function gives no worst-case guarantee).
A list with connected (logical). When it is
FALSE, also component1, a proper non-empty open set whose
complement is open, and component2, that complement (integer
vectors). Each of the two is a union of connected components, not
necessarily a single component; the field names are kept from earlier
versions.
The function does not check that the list is a topology, that is, that it contains the empty set and the whole set and is closed under unions and intersections. On an arbitrary family it reports whether some proper non-empty member has its complement in the family, which is connectedness only when the family is the complete topology.
The compiled engine (C++ through 'Rcpp').
generate_topology() for exact components from a subbase.
chain <- list(integer(0), 1L, 1:2, 1:3) is_topology_connected_exact(chain, 3L) split <- list(integer(0), 1:2, 3:4, 1:4) is_topology_connected_exact(split, 4L)chain <- list(integer(0), 1L, 1:2, 1:3) is_topology_connected_exact(chain, 3L) split <- list(integer(0), 1:2, 3:4, 1:4) is_topology_connected_exact(split, 4L)
Checks whether every integer from 1 to the largest label of a family of sets appears in at least one member, that is, whether the family covers the ground set {1, ..., max}. Despite its name, retained from version 0.1.0, it does not test connectedness.
is_topology_connected_manual(topology)is_topology_connected_manual(topology)
topology |
A list of integer vectors (positive labels). No default. |
A logical scalar: TRUE if every integer from 1 to the
largest label appears in some member, FALSE otherwise or if the
family is empty.
Coverage is necessary for a family to be a base or a subbase of a topology on {1, ..., max}, but it says nothing about connectedness: the discrete topology covers its ground set and is maximally disconnected.
Base R only.
is_topology_connected_exact() and generate_topology() for
connectedness.
is_topology_connected_manual(list(c(1, 2, 3), c(3, 4, 5))) is_topology_connected_manual(list(c(1, 2), c(4, 5)))is_topology_connected_manual(list(c(1, 2, 3), c(3, 4, 5))) is_topology_connected_manual(list(c(1, 2), c(4, 5)))
Builds a directed graph with an arc between consecutive labels, in
increasing order, inside each member of a family of sets, and reports
whether every label that appears in the family is reachable from the
smallest one. It is retained from version 0.1.0 for backward compatibility
and is not a test of topological connectedness; use generate_topology() or
is_topology_connected_exact() for that.
is_topology_connected2(topology)is_topology_connected2(topology)
topology |
A list of integer vectors (positive labels), read as a family of subsets. No default. |
Memory grows with the square of the largest label, because the arcs are stored as a dense matrix.
A logical scalar: TRUE if every label that appears in
the family is reachable from the smallest label along the arcs described
above, FALSE otherwise or if the family is empty.
The answer depends on how the elements are labelled, so it is not a
topological invariant, and it is neither a necessary nor a sufficient
condition for connectedness. The base {1, 3}, {2, 3} generates the
connected topology whose open sets are {}, {3}, {1, 3}, {2, 3} and
{1, 2, 3} (no proper non-empty open set has an open complement), yet label
2 is not reachable from label 1 and the function returns FALSE. The
discrete topology on {1, 2}, listed as {}, {1}, {2}, {1, 2}, is
disconnected, yet the function returns TRUE.
Base R only.
generate_topology() and is_topology_connected_exact() for exact
connectedness, is_topology_connected() for a necessary condition.
is_topology_connected2(list(c(1, 2, 3), c(3, 4, 5))) is_topology_connected2(list(c(1L, 3L), c(2L, 3L)))is_topology_connected2(list(c(1, 2, 3), c(3, 4, 5))) is_topology_connected2(list(c(1L, 3L), c(2L, 3L)))
natural_visibility_graph() decides every pair by the sign of
for an observation between and : lies
strictly below the chord joining to if and only if
. That sign is computed exactly for every finite input, in a
floating-point environment that provides IEEE 754 double arithmetic,
under the hypotheses stated below; the graph is therefore the natural
visibility graph of the
doubles received, whatever the order in which floating-point operations
round. This page proves it. The computation has two stages, the adaptive
scheme of Shewchuk (1997): a floating-point evaluation whose sign is
accepted only when the result exceeds a threshold above which that sign is
proved correct, and an exact evaluation in integer arithmetic otherwise. The code is in the file
src/exact_predicate.h of the package sources.
equals , where is the orientation
determinant of the points , ,
, with and
. When the six inputs have magnitude at most
, the code computes the four differences, the products
and
likewise, and
, an evaluation of , where
denotes the double stored after one operation: its
exact result rounded to binary64, either directly or after a first rounding
to the evaluation format (the proof bounds both roundings together). When
(about ; the code writes it as
the hexadecimal literal 0x1p-930, which binary64 represents exactly,
and so does every wider format in which the comparison may be evaluated, so
its value does not depend on the compiler) and
, with , it accepts the sign of
as the sign of and returns its opposite as the
sign of ; otherwise it goes to the exact stage. Each of the ten values
above (four differences, two products, two absolute values, and
) is stored in a volatile variable, one operation per
statement, so it is the result of a single operation rounded to double: the
compiler can neither fuse a product into the subtraction nor reorder the
operations, and a value computed in a wider format is rounded to double
when it is stored.
Proof that the accepted sign is the sign of . Assume IEEE 754
binary64 doubles in any of the four rounding modes, each operation rounded
to binary64 directly, or first to a format with at least 64 significand
bits (such as the x87 extended format) or to the x87 extended format with
its precision control at 53 bits, and then to binary64 on the store. Under
any other evaluation the threshold is not established; the next section
says which part of these hypotheses is checked.
(o) No operation overflows: the differences have magnitude at most
, the products at most and at most
, and rounding is monotone, so every rounded value obeys the
same bounds.
(i) Hence every operation whose exact result has magnitude at least
returns it with relative error at most
, which bounds one directed rounding to double
() after one rounding to a 64-bit significand (); a
53-bit significand gives an error of at most that the store, exact
in that range, does not increase; an
operation whose exact result is smaller is exact if it is a subtraction of
two doubles (the difference is a multiple of below
, hence a double), and otherwise has absolute error below
, the sum of at most from the store and less
than from a previous rounding in a wider format.
(a) Each difference is exact or within relative error .
(b) Each product is with
and ,
the absolute term covering an underflow; likewise . Hence
, and since
and ,
.
(c) has the sign of : a
non-zero is at least in magnitude, a double that
every rounding mode keeps on the same side of zero, and
only if ; and
(the subtraction is exact when
its result is below ).
(d) implies that the exact sum is at
least (a smaller sum of two doubles would be exact, and
would equal it), hence at least , and
. If , then
, and with
the term
is below
; so , and
have the same sign, and so does .
Inputs above in magnitude go straight to the exact stage.
Without that bound, a directed rounding mode can turn an overflow into the
largest finite double instead of an infinity (under rounding toward zero,
every overflow; under rounding downward or upward, the overflows of one
sign), and the relative error, which approaches 1 as the exact result grows,
is no longer bounded by . With the
largest double, , the instants
and the values
, , give
, but under rounding toward zero a
filter without the bound computes the difference , of
magnitude , as , finds far
above the threshold, and returns .
Proof that it returns the sign of .
(1) Decomposition. For a finite non-zero double , the C function
frexp() returns, exactly, and with
, from to . has at
most 53 significant bits (a subnormal has fewer, and frexp()
normalizes it), so , computed exactly by ldexp(),
is an integer with , and
with , from to . Zero gives
.
(2) Products. is the sum of the six terms
, , , ,
, , each a product of two inputs with the
coefficient or ; the function that adds them receives,
for each term, only whether its coefficient is , so no other
coefficient can be passed. Each non-zero term is
with , and the
product is computed exactly in four words of 32 bits:
writing and with
, the four partial products are below ,
, and , the middle sum is below
and the high sum below , so no operation on
64-bit unsigned integers overflows.
(3) Alignment. Let and be the smallest and
the largest exponent of the non-zero terms and
. Then ,
where and are the sums of the terms of each sign, each
term , with , contributing
the integer ; has the
sign of . Each shifted term is below
, so and are below
.
(4) Capacity. Each accumulator has
words of 32 bits, at least
bits, so no carry leaves it; a term shifted by
bits is written into the words
to , and
. Since is at most
, is at most 137.
(5) Comparison. and are compared word by word from the
most significant one, which orders non-negative integers written in base
, and gives . After frexp() and
ldexp(), which are exact, the stage uses integer arithmetic only, so
no rounding, overflow or underflow can occur, and neither the rounding mode
nor the compiler options matter.
At compile time the build stops with an error unless doubles have the
binary64 format (radix 2, 53 significant digits, its exponent range) and
operations on doubles are evaluated in binary64 (FLT_EVAL_METHOD 0
or 1) or in a format with at least 64 significand bits
(FLT_EVAL_METHOD 2 with LDBL_MANT_DIG at least 64). It also
stops with -ffast-math or -ffinite-math-only, which let the
compiler assume that no infinity or NaN occurs, and, under GCC, with every
option that makes the compiler set __GCC_IEC_559 to 0 (among them
-funsafe-math-optimizations, -fassociative-math,
-freciprocal-math and -fno-signed-zeros). Among the options
that the compilers accept, contraction (with any compiler) and the
reassociation and reciprocal options of Clang, which does not reveal them
through its predefined macros, cannot change the floating-point stage.
Each of the ten intermediate values of that stage is a volatile object, and
the C++ standard requires every access to a volatile object to be performed
as the program writes it: each result is stored into its object, and every
operation that uses it reads it from that object, so the operand is the
double stored there and not a fused or wider intermediate; each statement
that computes one of them performs a single operation; the final test
reads and once and compares
with through an absolute value, a
scaling by a power of two (exact, since ) and
comparisons,
which are all exact; and
the stage contains no division (its operations are four subtractions, two
multiplications, one addition, absolute values, comparisons and a scaling
by ), so a reciprocal option has nothing to transform. An
accepted option can still act through the floating-point environment it
sets at start-up, which the run-time tests below cover. The
proof assumes, beyond these checks, a compilation that performs each
operation as written, as the C++ standard prescribes for operations on
volatile objects. What remains is the floating-point environment at run time,
which some libraries change: the flush-to-zero and denormals-are-zero modes (Clang
links such a start-up routine into executables built with
-funsafe-math-optimizations) make subnormal arithmetic inexact and
would also change the comparisons of both visibility criteria, and the
precision control of the x87 unit can reduce the significand to 24 bits.
Before building a graph, the constructors test, with results that are
exact in binary64 and therefore independent of the rounding mode, that
and
(53 significant bits), that
half of the smallest normal double is still positive and that the smallest
subnormal double compares greater than zero; they stop with an error when
any test fails. They also compute in the non-stop mode of IEEE 754: they
save the floating-point environment, disable every trap (an enabled trap on
inexact results or on underflow would otherwise interrupt the filter, whose
products can be inexact or underflow) and restore the environment when
they return. With the compile-time checks, these tests cover the
hypotheses of the proof except one case: a run-time precision strictly
between 53 and 64 bits would be neither covered by the proof nor detected,
and under it the exactness is not established.
In the source repository, dev/check_filter_constants.py checks the
numerical facts used in the floating-point stage (the constants 6.01,
6.02, 7.99 and 1.97, the bound , the bound on from
, the absolute terms, the overflow bounds and
) in exact rational arithmetic, and
dev/make_exact_predicate_fixture.py regenerates, deterministically,
the 1,800 sextuples of the test suite in nine regimes (among them extreme
exponents, subnormal numbers, points within two units of rounding of a
line, and inputs beyond the bound of the floating-point stage), with signs
computed in exact rational arithmetic outside the package. The test suite
compares the computed sign with each of them.
Shewchuk, J. R. (1997). Adaptive precision floating-point arithmetic and fast robust geometric predicates. Discrete & Computational Geometry, 18(3), 305-363. https://doi.org/10.1007/PL00009321
natural_visibility_graph(), horizontal_visibility_graph().
Builds the natural visibility graph of a series (Lacasa et al., 2008): two
observations are adjacent when every observation strictly between them lies
strictly below the straight segment that joins them, drawn at the instants
of observation, equally spaced unless times says otherwise. The
criterion is decided exactly on the numbers received, under the hypotheses
on the floating-point arithmetic stated in natural_visibility_exactness.
With
directed = TRUE it also returns the arcs from the earlier to the
later observation of each edge, which generate_bitopology() turns into the
forward and backward topologies.
natural_visibility_graph(series, directed = FALSE, times = NULL)natural_visibility_graph(series, directed = FALSE, times = NULL)
series |
Numeric vector without |
directed |
Logical scalar (default |
times |
|
Observations are adjacent when, for every between them,
The graph is built by divide and conquer on the maximum. Each level of the
recursion makes at most one decision per point of its intervals, so the
number of decisions is in the worst case (monotone data, whose
maximum is always at an end), when the maximum of every
interval splits it in parts of bounded proportion, and in
expectation when the values are exchangeable and without ties, because the
position of each maximum is then uniform, as the pivot of randomized
quicksort. Three facts make the construction correct for any strictly
increasing instants. (i) If maximizes on an interval and
, the chord from to takes at a
convex combination of and , at most , so
blocks every pair across it. (ii) For , every between
them is below the chord from to if and only if the slope
from to is smaller than the slope from each such to
; scanning leftwards from , the smallest of those slopes
belongs to the last index found visible, , so is visible if
and only if , and symmetrically on the right. (iii) The
edges inside each half depend only on the points of that half.
Each decision is therefore the sign of one quantity , and it is
computed exactly under those hypotheses. When the six inputs of a decision are at most
in magnitude, so that no intermediate value can overflow, the
engine evaluates in floating
point the orientation determinant , as the difference of two
rounded products and , and accepts the sign of the result
when its magnitude exceeds , where and
is the computed value of : above that threshold the sign is
proved correct. Otherwise, near a tie, or when does not exceed the
, about , is evaluated exactly in integer
arithmetic on the binary mantissas and exponents of the six products it
contains. This is the adaptive scheme of Shewchuk (1997). The proofs of
both stages, and the hypotheses on the arithmetic under which they hold
(every IEEE 754 rounding mode; evaluation in binary64, in a format with at
least 64 significand bits, or in the x87 format at 53 bits), with what is
checked of them, are written in natural_visibility_exactness; every
intermediate value of the floating-point evaluation is stored, so that the
compiler cannot fuse a product into the subtraction or reorder the
operations. A floating-point decision takes a fixed number of operations;
an exact one allocates and compares two accumulators of words of 32
bits, where grows with the spread of the binary exponents of the six
products and is at most 137, so its number of word operations is
proportional to .
The function stops with an error when the floating-point environment does
not provide IEEE 754 double arithmetic (subnormal numbers flushed to zero,
or operations with fewer than 53 significant bits), a condition that some
libraries create and under which the proofs do not apply.
A list with the same elements as
horizontal_visibility_graph().
What exact means here. Under the hypotheses of
natural_visibility_exactness, the graph is the natural visibility graph of
the doubles received, independent of the order of floating-point
operations. Consequences, under the same hypotheses: reversing the values
and reflecting the instants (see time_reverse()) exchanges the forward and
backward topologies exactly, for every input; and the graph does not change
when the values are replaced by and the instants by
, with , provided that the new
values and instants are finite doubles equal to these exact images (no
rounding), so that the instants stay strictly increasing. Proof: each
computed for the images equals , because the
coefficients of , and in add up to zero,
so cancels, and every difference of instants is multiplied by
, so cancels.
Versions up to 0.3.0 accepted only equally spaced instants and compared
the rounded slopes . When
no difference of values overflows and no slope is non-zero and below
in magnitude, each rounded slope is within relative error
of the exact one (, rounding to nearest), so
the two engines can disagree only on two slopes whose difference is at most
about times the sum of their magnitudes, that is, when a point lies
on a chord or very near it. Outside that range of magnitudes, with values
near the largest doubles or slopes in the subnormal range, the rounded
slopes of 0.3.0 could also err far from any tie.
Decimal data. A decimal such as 0.1 has no exact binary
representation, so points that are collinear in decimal can be slightly
below or above the line in binary, and the exact criterion then follows the
binary numbers. Suppose the values were recorded with decimal
places, with a non-negative integer
and integers , and that each was stored as the double
nearest to it. Then m <- round(10^d * y) returns the
integers , and the graph of the recorded decimals is the graph of
m, computed exactly; the same applies to the instants. The check
all(abs(m) <= 2^50) && all(m / 10^d == y) passes in that case, fails
whenever some is not the double nearest to a decimal with
places, and, when it passes, is the only decimal
with places whose nearest double is . Proof, in the
rounding to nearest in which R computes: is an exact double for
, because . Since is nearest
to , with ;
the computed product 10^d * y[i] equals
with , so it differs
from by less than , which is below
, and round() returns ; m[i] / 10^d, a single
correctly rounded division, returns the double nearest to , that
is . When the check passes, is the double nearest to
, and no other decimal with places shares it: two
such decimals differ by at least , while two numbers rounded to
the same differ by at most , which is below
. Finally, multiplying every value by
multiplies every by , which keeps its sign, and the
integers are exact doubles. Without the bound the recipe can
fail: the decimals
, and are stored as
, and , and rounding cannot recover
the middle one.
What the graph keeps of the series. Lacasa et al. (2008) describe the graph as inheriting properties of the series: in the words of their abstract, periodic series convert into regular graphs, random series into random graphs, and fractal series into scale-free networks. These are statements of the abstract, which does not say for which series they hold; they do not hold for every finite series: the periodic series 0, 1, 0, 1, 0 at the equally spaced instants 1, ..., 5 has, under the natural criterion, the edges 1-2, 2-3, 2-4, 3-4 and 4-5, and the degrees 1, 3, 2, 3, 1.
The compiled engine (C++ through 'Rcpp').
Lacasa, L., Luque, B., Ballesteros, F., Luque, J., & Nuño, J. C. (2008). From time series to complex networks: The visibility graph. Proceedings of the National Academy of Sciences, 105(13), 4972-4975. https://doi.org/10.1073/pnas.0709247105
Shewchuk, J. R. (1997). Adaptive precision floating-point arithmetic and fast robust geometric predicates. Discrete & Computational Geometry, 18(3), 305-363. https://doi.org/10.1007/PL00009321
horizontal_visibility_graph(), generate_bitopology(),
time_reverse().
series <- c(3, 1, 4, 1, 5, 9, 2, 6) g <- natural_visibility_graph(series) g$n_edges head(g$edges) natural_visibility_graph(c(0, 2, 4), times = c(0, 1, 4))$edges y <- round(0.1 * (1:40) + 0.3, 1) natural_visibility_graph(y)$n_edges m <- round(10 * y) all(abs(m) <= 2^50) && all(m / 10 == y) natural_visibility_graph(m)$n_edgesseries <- c(3, 1, 4, 1, 5, 9, 2, 6) g <- natural_visibility_graph(series) g$n_edges head(g$edges) natural_visibility_graph(c(0, 2, 4), times = c(0, 1, 4))$edges y <- round(0.1 * (1:40) + 0.3, 1) natural_visibility_graph(y)$n_edges m <- round(10 * y) all(abs(m) <= 2^50) && all(m / 10 == y) natural_visibility_graph(m)$n_edges
Returns the discrete topology on the indices of the data, the finest
topology on a set, in which every subset is open. Only the singleton base
is returned, because the full topology has open sets.
simplest_topology(data)simplest_topology(data)
data |
Vector with at least one element; only its length is used. No default. |
A list with:
List of the singletons.
The same list: the singletons form the minimal base.
Character: "discrete".
Integer, the number of points.
Logical: TRUE only when ; every
singleton is open and closed, so two or more points are
disconnected.
The discrete topology is a reference point: every topology produced by the package is coarser than or equal to it.
Base R only.
complete_topology() for the opposite extreme, the indiscrete
topology.
result <- simplest_topology(c(1, 2, 3, 4, 5)) result$connectedresult <- simplest_topology(c(1, 2, 3, 4, 5)) result$connected
Returns the series read backwards together with the reflection of its
instants, the operation under which the forward and backward topologies of
a visibility graph are exchanged (theorem T1, stated and proved in
generate_bitopology()). Use it instead of reversing the values alone,
which is a different operation as soon as the instants are not equally
spaced.
time_reverse(series, times = NULL)time_reverse(series, times = NULL)
series |
Numeric vector without |
times |
|
Reflecting the instants keeps every gap between consecutive observations
and reverses their order. Any reflection gives the same
visibility graphs in exact arithmetic, because both criteria are invariant
under translations of the time axis, and keeps the
observation window. The function takes because negation is exact
in floating point, whereas can round. With times = NULL
the reflection of is an exact translation of the same
equally spaced grid, so NULL is returned.
A list with series, the reversed values
, and times: NULL when
times is NULL, and otherwise the numeric vector
, again strictly increasing.
Reversing the values over the same instants coincides with this operation only when the gaps between consecutive instants read the same in both directions. With the instants 0, 1, 4 and the values 0, 2, 4, the reversed values 4, 2, 0 over the same instants make the first and the third observation visible to each other, while in the original series they are not; with the reflected instants -4, -1, 0 the exchange of the two topologies holds.
Base R only.
generate_bitopology() for the theorem and its consequences,
natural_visibility_graph() and horizontal_visibility_graph().
rv <- time_reverse(c(0, 2, 4), times = c(0, 1, 4)) rv natural_visibility_graph(rv$series, times = rv$times)$edges natural_visibility_graph(c(0, 2, 4), times = c(0, 1, 4))$edges natural_visibility_graph(c(4, 2, 0), times = c(0, 1, 4))$edgesrv <- time_reverse(c(0, 2, 4), times = c(0, 1, 4)) rv natural_visibility_graph(rv$series, times = rv$times)$edges natural_visibility_graph(c(0, 2, 4), times = c(0, 1, 4))$edges natural_visibility_graph(c(4, 2, 0), times = c(0, 1, 4))$edges
Computes the five candidate radii of calculate_thresholds() and the size
of the threshold-neighbourhood base that each one generates, and optionally
plots them side by side.
visualize_topology_thresholds(data, plot = TRUE)visualize_topology_thresholds(data, plot = TRUE)
data |
Numeric vector with at least two elements and no missing values. No default. |
plot |
Logical scalar; if |
A data.frame with the columns method (character),
threshold (numeric) and base_size (integer, counted as in
calculate_topology()). If plot = TRUE, it carries an attribute
"plots": a list with the elements threshold,
base_size and scatter.
The comparison is descriptive: it shows how the radius, and with it the granularity of the base, depends on the scale chosen.
'ggplot2' (Imports) for the plots; calculate_thresholds() and
calculate_topology() for the numbers.
calculate_thresholds(), calculate_topology(),
analyze_topology_factors().
set.seed(1) results <- visualize_topology_thresholds(rnorm(50)) resultsset.seed(1) results <- visualize_topology_thresholds(rnorm(50)) results