Note the pipe operator in this
vignette (|>
) assumes you have R 4.1.0+, allowing
natural chaining of commands. The package does not require this version
of R, only this vignette. We also set the seed to ensure reproducibility
of this vignette.
This tutorial will begin by considering one dimensional standard Brownian motion. In Multiple Dimensions we consider multi-dimensional Brownian motion with user-specified covariance matrix. Unless otherwise stated, all syntax introduced in the one-dimensional case, and the output types, matches that of multi-dimensional Brownian motion.
The first task is to create an object which will contain the Brownian
motion path and layer information. This is accomplished using the
create.bm()
function. It is possible to call with all
default options, but here for demonstration purposes we will force the
path to start at W−1 = 100 (the default is
W0 = 0).
Of course, we may be interested in subsequently “forcing” a path
constrained to hit particular point(s) at particular time(s) (i.e.,
bridge simulation). For instance, we may wish the path to “hit” W0 = 101 and W1 = 99. This can be
achieved using the add.points
which has two required
arguments, one for specifying a vector of forced times, and the second a
vector of forced points:
At this point we can see a plot of our nascent path, by simply using
the plot()
function:
Here, black dots represent path observations and these are connected by grey lines. Note grey lines do not represent the actual path, although see later section on Publication Graphics for details on plotting an illustrative sample path (TODO).
Alternatively, if at the outset we wish to simulate a Brownian
bridge, we can supply a vector of times and position to the
create.bm
function. For instance, replicating the
above:
The simplest operation is unconditional simulation of the path at
future times (beyond the end time of the existing path). To do this we
use the function sim()
, and supply a vector of times. Any
of the times supplied which are in excess of the current end point will
be simulated in order unconditionally (and all other times will be dealt
with appropriately automatically, as discussed later). For instance,
noting for our object bm
the current end point is t = 1, suppose we wish to simulate
the path at the times t = (2, 3, 4), we have:
Note that the bm
object is updated in place: there is no
need to assign the result back to bm (ie you do not need to do
bm <- bm |> sim(2:4)
, although there will be no
negative side effects if you do so).
We can again visualise the path by using the plot()
function:
The simplest step beyond unconditional simulation would be to
simulate times which are intermediate to already observed time points.
This can be achieved using Brownian bridge simulation. As before, we can
simply use the function sim()
: all time points in a vector
supplied to the function which fall between existing times in the
existing bm
path will be appropriately simulated. The
handling of the function sim()
intuitively deals with a
mixed vector of times which require unconditioned and conditioned
simulation. For example, suppling the vector t = (0.5, 1.5, 2.5, 3.5, 4.5) (and
noting that the current end point of bm
is t = 4), we have:
Further note that the first argument to sim()
can be a
vector of times produced in any manner, say via seq()
(or
even by random simulation of times):
The above will simulate at 5 times randomly chosen between the start
and end times of the existing path. Note this highlights that the vector
of times passed to sim()
need not be sorted: (non-standard)
sorting of required times is handled by the package to ensure robustness
of functionals.
At this point it is instructive to look at the stored path, which can be achieved by using the standard R element indexing syntax, as though the object was a data frame. Therefore,
bm[,"t"]
contains the times of observations, t;bm[,"W_t"]
contains the value of the path at those
times, Wt.bm[,"t"]
#> [1] -1.0000000 -0.2347582 0.0000000 0.5000000 1.0000000 1.5000000
#> [7] 2.0000000 2.0786540 2.3973197 2.5000000 3.0000000 3.0303288
#> [13] 3.5000000 4.0000000 4.0194883 4.5000000
bm[,"W_t"]
#> [1] 100.00000 100.20550 101.00000 99.61020 99.00000 99.39885 98.76083
#> [8] 99.10028 99.51900 99.17654 99.43776 98.82608 98.05434 96.99743
#> [15] 97.05768 97.87486
In this simple setting t
and Wt
comprise the “path skeleton”: they are a finite dimensional
representation of the Brownian motion path, which can be used to
reconstruct the path at any other time point of interest. The indices of
the vectors t
and W_t
match one another, and
so (for instance) the value taken by the path at time t = 1 is Wt = 99. To
avoid the user having to do complicated matching of the indices in order
to extract the value of the path at a given time, one can instead index
the bm
object as follows, specifying in the t
argument what time (or vector of times) they require, together with the
part of the object they require (here, "W_t"
, which must be
quoted):
Note that by default calls to the bm
environment without
specifying the t
argument will instead assume the user
wants the associated index. For instance, the following retrieves the
path location at the first time, whatever that may be (here,
the first time is t = −1):
As an illustration of using this “under the hood” information, we can simulate an Exponential(λ = W4.5/10) time past the end of the path, without having had to track in our code that t = 4.5 is the end of the path what this current end time is:
Of course, the specification of the above time to simulate is rather
clunky. To aid the user the package provides default labels
to the time points which constitute the path skeleton (with each point
possibly having multiple labels). We further give the user the option to
add in their own labels. For instance, we can see the currently defined
labels as a named list of times:
bm[,"labels"]
#> $start
#> [1] -1
#>
#> $end
#> [1] 4.563118
#>
#> $seg.start
#> [1] -1
#>
#> $seg.end
#> [1] 4.563118
#>
#> $forced
#> [1] -1 0 1
#>
#> $user
#> [1] 2.0000000 3.0000000 4.0000000 4.5000000 3.5000000 2.5000000
#> [7] 1.5000000 0.5000000 4.0194883 3.0303288 2.0786540 2.3973197
#> [13] -0.2347582 4.5631185
Note that each list item name corresponds to the times that have been
assigned a particular label (and note that some times have multiple
labels, for instance t = 1).
For our existing path bm
all existing labels are those
automatically assigned by the package. All automatically assigned labels
are dynamically updated as the path changes. In particular:
start
and end
are the first and last times
in the existing skeleton.seg.start
and seg.end
are the first and
last times for respective “segments” (see Segments).forced
is applied to any point introduced by the user
by specifying not just the time, but also the position of the path.
Recall, when we initialised the path (at t = −1), we introduced “forced”
points at t = 0 and t = 1.user
is applied to any time for which the path has been
simulated by the user using the sim()
function.To access the vector of times associated with a label then this can
be achieved as follows (for instance, supposing we wanted the
user
times)
bm[,"labels"]$user
#> [1] 2.0000000 3.0000000 4.0000000 4.5000000 3.5000000 2.5000000
#> [7] 1.5000000 0.5000000 4.0194883 3.0303288 2.0786540 2.3973197
#> [13] -0.2347582 4.5631185
Alternatively this can be achievied by simply passing a label (for
instance, user
) as the index in a call to bm
and requesting the associated times t
:
bm["user","t"]
#> [1] 2.0000000 3.0000000 4.0000000 4.5000000 3.5000000 2.5000000
#> [7] 1.5000000 0.5000000 4.0194883 3.0303288 2.0786540 2.3973197
#> [13] -0.2347582 4.5631185
There is considerable flexibility for the user to introduce their own
labelling. Existing times associated with a user specified label are
fixed (and do not get dynamically updated in the same manner as the
automatic labels above). This can be done as the path is generated by
using the optionally available argument label
in either the
add.points()
or sim()
function we have
introduced thus far. label
if specified should be a vector
of length 1 (in which case the same
label is applied to all elements of the vector suppled), or of length
equal to that of the supplied vector. For instance, suppose we wanted to
simulate the path at times t = (6, 7) and give them the label
special.time.1
and special.time.2
respectively
then:
bm |> sim(c(6, 7), label = c("special.time.1", "special.time.2"))
bm[,"labels"]
#> $start
#> [1] -1
#>
#> $end
#> [1] 7
#>
#> $seg.start
#> [1] -1
#>
#> $seg.end
#> [1] 7
#>
#> $forced
#> [1] -1 0 1
#>
#> $user
#> [1] 2.0000000 3.0000000 4.0000000 4.5000000 3.5000000 2.5000000
#> [7] 1.5000000 0.5000000 4.0194883 3.0303288 2.0786540 2.3973197
#> [13] -0.2347582 4.5631185 6.0000000 7.0000000
#>
#> $special.time.1
#> [1] 6
#>
#> $special.time.2
#> [1] 7
Note now the addition of the new labels and associated times. We may
wish to specify an additional user label to existing times with existing
labels. This can be done by simply supplying a vector of the existing
times to the function add.labels()
. For instance, we may
wish the times with the label forced
to be be given the
label special.time.1
, and those with the label
user
to be given the additional label
special.time.2
:
bm |>
add.labels(t = bm["forced","t"], label = c("special.time.1")) |>
add.labels(t = bm["user","t"], label = c("special.time.2"))
bm[,"labels"]
#> $start
#> [1] -1
#>
#> $end
#> [1] 7
#>
#> $seg.start
#> [1] -1
#>
#> $seg.end
#> [1] 7
#>
#> $forced
#> [1] -1 0 1
#>
#> $user
#> [1] 2.0000000 3.0000000 4.0000000 4.5000000 3.5000000 2.5000000
#> [7] 1.5000000 0.5000000 4.0194883 3.0303288 2.0786540 2.3973197
#> [13] -0.2347582 4.5631185 6.0000000 7.0000000
#>
#> $special.time.1
#> [1] 6 -1 0 1
#>
#> $special.time.2
#> [1] 7.0000000 2.0000000 3.0000000 4.0000000 4.5000000 3.5000000
#> [7] 2.5000000 1.5000000 0.5000000 4.0194883 3.0303288 2.0786540
#> [13] 2.3973197 -0.2347582 4.5631185 6.0000000
Note that the above could be achieved by using the sim()
function (noting that applying sim()
to an existing path
does not alter the existing path).
Of course, we may wish to delete labels: this can be done using the
delete.labels()
function. For instance, now removing the
special.time.1
label, and removing the
special.time.2
label from the times t = (7, 2, 3):
bm |>
delete.labels(label = c("special.time.1")) |>
delete.labels(t = c(7, 2, 3), label = c("special.time.2"))
bm[,"labels"]
#> $start
#> [1] -1
#>
#> $end
#> [1] 7
#>
#> $seg.start
#> [1] -1
#>
#> $seg.end
#> [1] 7
#>
#> $forced
#> [1] -1 0 1
#>
#> $user
#> [1] 2.0000000 3.0000000 4.0000000 4.5000000 3.5000000 2.5000000
#> [7] 1.5000000 0.5000000 4.0194883 3.0303288 2.0786540 2.3973197
#> [13] -0.2347582 4.5631185 6.0000000 7.0000000
#>
#> $special.time.2
#> [1] 4.0000000 4.5000000 3.5000000 2.5000000 1.5000000 0.5000000
#> [7] 4.0194883 3.0303288 2.0786540 2.3973197 -0.2347582 4.5631185
#> [13] 6.0000000
Note that it is only possible to add.labels
and
delete.labels
for user labels, and not any automatic
labels. For instance,
bm |> add.labels(t = 6, label = "end")
#> Error in assert.bmlabel(label, t): one of the specified labels clashes with an internal label naming scheme: label not added, please change.
Of course, we may also wish to know what value(s) the path took at
time(s) given by a specific label. Again, this can be achievied by
simply passing a label (for instance, special.time.2
) as
the index in a call to bm
:
bm["special.time.2", "W_t"]
#> [1] 96.99743 97.87486 98.05434 99.17654 99.39885 99.61020 97.05768
#> [8] 98.82608 99.10028 99.51900 100.20550 97.52921 96.30520
Note the flexibility labelling offers, as we can use labels themselves as arguments in function calls. For instance, returning to our earlier example which motivated our labelling, we could simulate the path at an Exponential(λ = W4.5/10) time past the end of the path, without having had to track in our code that t = 7:
Having initialised the path at W−1 = 100 we may
naturally wish to simulate a time prior to the time the path was
intisialised. Simulating prior to the initialised time is ambiguous once
the path also includes layers
(see Layers, layers, everywhere!). As a result, directly
using the sim()
function (for instance, simulating the path
at time t = −2) will result in
an error:
bm |> sim(-2)
#> Error in sim.BrownianMotion(bm, -2): cannot simulate path at times before the path was initialised (at time -1) -- to add in points prior to the beginning of path use add.points()
As indicated in the error message, one apporach to handling this is to manually add in a point. For instance, if we were unconditionally simulating the path to the left of the initial time:
Of course, we may wish to now artificially “adjust” the path by
shifting the path to the right so it is initialised at t = 0. This is easily achieved by
creating a new path (say, bm.adjust
) using the
concat.bm()
function and specifying the time the adjusted
path should be initialised (although for full details of the
concat.bm()
function see Path
Concatenation)
bm.adjust[,"labels"]
#> $start
#> [1] 0
#>
#> $end
#> [1] 9.087926
#>
#> $seg.start
#>
#> 0
#>
#> $seg.end
#>
#> 9.087926
#>
#> $forced
#> [1] 1 2 3 0
#>
#> $user
#> [1] 4.000000 5.000000 6.000000 6.500000 5.500000 4.500000 3.500000 2.500000
#> [9] 6.019488 5.030329 4.078654 4.397320 1.765242 6.563118 8.000000 9.000000
#> [17] 9.087926 0.000000
#>
#> $special.time.2
#> [1] 6.000000 6.500000 5.500000 4.500000 3.500000 2.500000 6.019488 5.030329
#> [9] 4.078654 4.397320 1.765242 6.563118 8.000000
Note that for the bm.adjust
path all associated labels
have also been shifted.
bm
objectsIt is important for package users to understand that all Brownian
motion path objects, such as bm
and bm.adjust
in the above, are internally stored as R environments. This means that
attempting to assign from one variable name to another does
not result in a copy of the object as one might expect with
idomatic R: instead, you end up with two variable names referencing the
same environment so that calls on one variable appear to update both.
For example,
# Current end time of path in bm
bm["end","t"]
#> [1] 7.087926
# Assign bm to a new variable called bm.x
bm.x <- bm
# Check end time of bm.x
bm.x["end","t"]
#> [1] 7.087926
# Extend bm.x path
bm.x |> sim(bm.x["end","t"] + 100)
# Observe both have updated since these are just handles to the same environment
bm["end","t"]
#> [1] 107.0879
bm.x["end","t"]
#> [1] 107.0879
If you do in fact wish to create a copy rather than an additional
handle to the same path, then a simple call to concat.bm()
without providing any other path to concatenate with will trigger a
copy:
# Current end time of path in bm
bm["end","t"]
#> [1] 107.0879
# Use concat to trigger a copy
bm.x <- concat.bm(bm)
# Check end time of bm.x
bm.x["end","t"]
#> [1] 107.0879
# Extend bm.x path
bm.x |> sim(bm.x["end","t"] + 100)
# Observe this time that both have not updated since we obtained a copy
bm["end","t"]
#> [1] 107.0879
bm.x["end","t"]
#> [1] 207.0879
In many settings we want to simulate Brownian motion together with “layer” information. Layer information determines a compact (spatial) interval which constrains the path over a given time interval. This can be achieved using this package in three broad ways:
There are merits to using each of these approaches, and so the user has the flexibility (when possible) of specifying these.
If a layer is simulated then this forms part of the path skeleton. Once a layer is simulated, then simulating further points of the path is more involved: again, these fall into the category of unconditional and conditional simulation.
We begin by specifying that we want to simulate a Bessel layer over
the interval t = [10, 12] for
our path bm.adjust
. This can be achieved by using the
(simply named!) function layers()
. Note that t = 10 is beyond the existing
terminal time of the path, and so the path will first be
(unconditionally) simulated to this time:
Purple is used to illustrate a Bessel layer (which will be useful later when we consider simultaneously different layer types). The solid lines are the layer indicating the spatial interval which constrains the path (the path falls entirely between the upper and lower solid lines). The dashed lines indicate a spatial interval which does not constrain the path (the path at some point will fall out-with the interval). As the end-points of the interval co-incide with the dashed lines we further know that the path will fall at some point both above the upper dashed line and below the lower dashed line.
Note we can now simulate at a time falling within this Bessel layer
specification and the above constraints will be honoured and result in
an updated layer specification. This is achieved using the same
sim()
function used to date: it will automatically detect
the surrounding layer information and conduct conditional Bessel layer
simulation. For example, simulating at t = 11 gives:
In particular, note that the Bessel layer specification has now updated, resulting in the single layer becoming two Bessel layers either side of the newly simulated time. In this case (though not always) this has resulted in two further variants of the Bessel layer. This is shown in the left hand interval by two dotted lines replacing the interior dashed lines of the earlier figure, and in the right hand interval by one dotted, one dashed lines replacing the interior dashed lines.
The dotted line indicates a more nuanced layer information, where the layer does not coincide with the end-points for the interval, but again indicates a spatial interval which does not constrain the path (the path at some point fall out-with the interval).
More precisely, in the left hand interval with two dotted lines there are three possible cases for the (unrealised) path. In particular, either:
The right hand interval has both a dashed (lower) and dotted (upper) line. These follow the same convention as the two earlier examples. As one of the end-points co-incides with the dashed line (lower) we know the path at some point falls below this level. This eliminates the possibility of case 3 in the list above, leaving case 1 and 2. It is possible that the dashed and dotted lines were in the reverse order (dashed being the upper, and dotted being the lower), in which case we would either have case 1 or 3.
When using layers()
there is no need for the start and
end times to co-incide with the current skeleton. It can be some future
interval (as above), an interval of time pre-ceding the current
skeleton, or an interval which partially includes the current skeleton.
If there are pre-existing layers then a call to layers()
only introduces layers for intervals which do not already have layers.
For instance, consider the introduction of layers over the interval
t ∈ [5, 14]:
Note that as the bm.adjust
object becomes larger,
calling the plot
command can be less informative. You may
instead wish to only plot
a specific time interval for
clarity. This is possible using the t.lim
argument within
the plot
command. For instance, suppose you were interested
only in visualising the interval [5, 10]:
The second layer type we will consider is based around first passage times of Brownian motion.
First passage times are the basis of the “localised layers” we introduce fully in the next subsection. Here, we consider the path initialiased at its current terminal value, and determine the first time it leaves a prescribed interval. The interval can be prescribed by the user in a number of different ways, by specifying among a choice of arguments:
delta
: specifying a symmetric interval around the
current terminal value;delta.l
and delta.u
: specifying a
asymmetric interval which is delta.l
below and
delta.u
above the current terminal value;l
and u
: specifying the lower and upper
boundary values directly.To better illustrate this we begin by simulating the existing path
unconditionally at time t = 15
by again calling sim()
before then simulating a first
passage to a symmetric interval ±1 from
W10. Notice that
commands can be chained together using the pipe |>
operator:
The green lines indicate the bounds specified by You, the user. The
green line does not form part of the realised Brownian motion, and is
used for visualisation. As such, it can be turned off using the
hide.user
argument in the plot command:
Note that the green lines were over-plotting solid red lines. Again, solid lines form part of the Brownian motion object and indicate the interval which constrains the path over the specified interval. We term this a “localised layer” as the end-point coincides with layer. Red is used as the colour to plot such layer types. The dashed lines follow the earlier convention, although note there will only be one. In the case above we in effect have the upper dashed line coinciding with the left hand end-point, and both the dashed and solid lines coinciding with the right hand end-point.
If we instead we want asymmetric layers and use the arguments
delta.l
and delta.u
then these are the lower
and upper offsets (respectively) of the current terminal end-point:
The output in this case is slightly different as this is achieved algorithmically by decomposing the requested layers into iterative calls of the symmetric variant of the function.
We may not be interested in the first passage time of a level offset
by the current terminal time, but instead some specified level. This can
be achieved by using the arguments l
and u
where these are the lower and upper layers requested. For instance, we
may want the first passage time of the (spatial) interval [90, 100]:
Note that if using the arguments l
and u
then the current terminal point must be contained within the interval,
as otherwise it is not possible and results in an error:
bm.adjust |> first.passage(l = 80, u = 85)
#> Error in first.passage(bm.adjust, l = 80, u = 85): u must be greater than the current endpoint state of the Brownian motion.
Again, time points within a localised layer can be simulated using
sim()
in the usual manner (and the package will
automatically detect the surrounding layer information to perform the
correct conditioning).
Note conditional simulations within a localised layer can give rise to a change in layer type, in this instance resulting in the first part of the localised layer becoming of Bessel type (recalling that for localised layers the layer coincides with the the end-point).
Further note that first passage times can only be requested for one-dimensional Brownian motion. For multi-dimensional Brownian motion it is more natural to consider the approach outlined in the next section:
Simulating localised layers by means of first passage times can be advantageous in some settings, although the time the first passage occurs is random, and this randomness can introduce its own complications.
Instead, we may wish to simulate localised layers over a specified
interval (in the same manner as we did for Bessel layers). For instance,
suppose we want a localised layer in the interval t ∈ [23, 29] this can again be
achieved using the layers()
function, but with the
additional argument type = "localised"
:
Note that the resulting skeleton for the interval will comprise zero
or more (red) localised layers, followed by a single (purple) Bessel
layer: this is because the end-point will no longer coincide with the
layer. By default delta
is chosen to be $\sqrt{t-s}/2$, although this can be altered
by the user using the argument mult
which multiplies the
default layer size. For instance,
We may wish to add a bridge analogue of a localised layer: ie a
localised layer over one or more existing intervals in the skeleton.
This can be achieved again by using the layers()
function
with the argument type = "localised"
. For instance, we may
wish to introduce localised intervals over every interval between times
[140, 230] (where t = 230 exceeds the current end-time
of t = 210):
Note that the path has been extended (unconditionally) beyond t = 140 as per above. For each interval within the current skeleton, which did not already have a layer, a localised layer has been introduced. The “bridge” localised layers are simulated by means of an appropriate scaling of unconditional Brownian motions, and so have a gradient of (Wt − Ws)/(t − s). The interior dashed, dotted and solid lines, together with the associated Bessel layer all follow the earlier conventions (albeit now with a gradient). The outer horizontal solid lines are for the use of You (and don’t form part of the skeleton) to easily indicate a spatial interval which constrains the path over a given (time) interval. We can again more clearly see the localised bridge layers by specifying a shorter interval to plot the path (for instance [140, 180]):
As the horizontal layers do not strictly form part of the path
skeleton, these can be toggled off when plotting by again using the
hide.user
argument:
In addition to the earlier discussion on the slots $t
and $W_t
, there are slots which contain the layering
information, both for the main path and those in the transformed space
just discussed above.
bm[,"layers"]
is a data frame (tibble) containing
the layering information of the main path, including variables:
type
to indicate the layer type, one of:
bessel
localised
intersection
(see forthcoming section);bessel-bb
localised-bb
intersection-bb
(see forthcoming section);The suffix -bb
indicates that this layer type results
from simulating the localised layer bridge of the last section.
t.l
and t.u
giving the temporal
interval (left and right time points) to which the layer
applies;
Ld
corresponds to the lower solid line;
Uu
corresponds to the upper solid line;
Lu
corresponds to the lower interior dashed/dotted
line;
Ud
corresponds to the upper interior dashed/dotted
line;
Lu.hard
indicating whether the lower interior line
coincides with the end point;
Ud.hard
indicating whether the upper interior line
coincides with the end point;
Note: The output in multiple dimensions will vary, as discussed in Multiple Dimensions.
$user.layers
is a data frame containing the
information on which first passage layers were requested by You
(resulting in the green lines on the plot). This contains
t.l
and t.u
giving the temporal interval
(left and right time points) to which the layer applies;L
and U
which are the boundaries specified
by You (perhaps indirectly via delta
or
delta.l
/delta.u
)$bb.local
is a nested Brownian motion object used in
the construction of the localised layer bridges. Note this will only
contain intervals pertaining to the localised layer bridge intervals of
$layers
(those ending -bb
), but otherwise
follows the same form.
Note: $user.layers
and
$bb.local
are only specified for one-dimensional Browian
motion
For example, the path produced to date in this vignette has the following internal layering representation:
t.l | t.u | L | U |
---|---|---|---|
15.00000 | 16.29056 | 92.54972 | 94.54972 |
16.29056 | 23.91984 | 91.54972 | 95.54972 |
23.91984 | 80.43416 | 90.00000 | 100.00000 |
type | t.l | t.u | Ld | Uu | Lu | Ud | Lu.hard | Ud.hard |
---|---|---|---|---|---|---|---|---|
localised | 140.0000 | 144.6401 | -3.162278 | 3.162278 | 0.000000 | 3.162278 | TRUE | TRUE |
bessel | 144.6401 | 180.0000 | 0.000000 | 6.324555 | 3.162278 | 5.341517 | TRUE | TRUE |
Again, we might naturally be more interested in the layers associated with a specific time. A given time may not lie within the current skeleton of the path, and if it does (for reasons discussed later), then of the two possible layers (to the left and right of that point) we are naturally more interested in the right hand layer. Note that there may be no layer associated with a given time (in which case it is supressed). For instance, suppose we were interested with path layers at the times t = {4.5, 4.6…, 5.5}:
bm.adjust[t = seq(4.5, 5.5, by = 0.1), "layers"]
#> # A tibble: 3 × 9
#> type t.l t.u Ld Uu Lu Ud Lu.hard Ud.hard
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <lgl> <lgl>
#> 1 bessel 5 5.03 98.7 99.6 98.8 99.4 TRUE TRUE
#> 2 bessel 5.03 5.5 97.4 99.5 98.1 98.8 TRUE TRUE
#> 3 bessel 5.5 6 96.3 98.8 97.0 98.1 TRUE TRUE
Now suppose we want an Intersection layer over the interval t = [50, 60], this can be done by
calling the function layers()
with the argument
type = "intersection"
. The structure of introducing
Intersection layers follows that of Bessel layers. Note that t = 280 is beyond the current
terminal point of the path:
Blue is used to illustrate an Intersection layer. Again, solid lines are the layer indicating the spatial interval which constrains the path. The interior lines for Intersection layers are always dashed. Dashed lines have the same interpretation as for Bessel layers, but need not coincide with the end-points for an interval. More precisely, we know the path has a maximum which falls between the upper dashed and solid lines, and a minimum which falls between the lower dashed and solid lines.
The sim()
function can be used once again to simulate
intermediary points of intersection layers. For instance, consider
simulating the path at t = 325:
Note that on simulating an intermediary point(s) a number of Bessel
(purple) or Intersection layers may arise. Intersection layers have the
same interpretation as the parent Intersection layer, and Bessel layers
are the same as in the earlier section. By default, the package will if
possible attempt to “convert” Intersection layers to Bessel layers: we
will address this default in the next subsection. Any further simulation
will be handled appropriately (for the given layer type) by using the
sim()
function.
As demonstrated in the previous section, by default the package has a preference for using Bessel layers over Intersection layers wherever possible. It is typical that the user will want to only consider one layer type, and the Bessel layer type has been selected as default as this is most likely to be the one practitioners will use. We do however want the flexibility to use multiple layer types, or change the default to Intersection.
To do this we can change the default argument from
prefer = "bessel"
to prefer = "intersection"
within the functions we have considered. For instance, we may consider a
new path (bm2
) initialised at W0 = 0:
This has the consequence that any call to sim()
(with no
further argument as the preference is inherited from the creation of the
path) will where possible convert Bessel layers to Intersection layers.
Note that although it is only occasionally possible to convert
Intersection layers to Bessel layers, it is always possible to change
Bessel layers to Intersection layers.
Now suppose we want to simulate Bessel layers over t ∈ [1, 3]. This can be achieved by
again using the layers()
function, but noting that as the
default type is “intersection” (as inherited on the creation of the path
with the argument prefer = "interesection"
), the additional
argument type = "bessel"
has to be passed to the
layers()
function to simulate a Bessel layer, In
particular,
then if we want to simulate the path at t = 2 and have resultant Bessel
layers then this needs to be specified by the user as it differs from
the default on the paths creation
(prefer = "intersection"
):
If however we don’t include the argument
prefer = "bessel"
, then further simulation of the path (say
at t = 1.25 and t = 2.5) results in both Bessel
layers being converted to Intersection layers:
Note that we do not need to specify we have a preference for
Intersection layers, as this preference is an attribute assigned to the
path on its creation using create.bm()
.
It is not possible to convert from, or to, Localised layers from
other layer types. However, once the path has been created with the
attribute prefer = "intersection"
then conditional
simulation of localised layers will result in the appearance of
Intersection layers (as opposed to Bessel layers). For instance, note
that when simulating a Localised layer over the interval t ∈ [4, 5] that the final interval
is an Intersection layer (and not a Bessel layer as before):
The preference for Intersection layers inherited from the creation of the path can be changed when calling any of the other functions we have introduce. For instance, consider simulating localised layers (of the Bessel sort) in the interval t ∈ [6, 8]:
and then the simulation of intermediary points in this interval:
We can change the layer type (if possible) for every interval in the
path by simply calling layers()
together with arguments for
the end-points for the path. For instance, suppose we now wanted
Intersection layers (we choose to add type = "intersection"
for clarity, but it is strictly not needed since this is the
prefer
default set upon the creation of the path):
This results in any Bessel layers being converted to Intersection layers. Similarly we could attempt to convert back to Bessel layers (although this may not always be possible for every interval):
Note that converting the layers from Intersection to Bessel or Bessel
to Intersection does not change the preference when calling the
sim()
function. For instance:
When simulating layers the package will by default control the size
of the layer so it scales appropriately with the length of the interval
being considered. In particular, by default, the package will attempt to
ensure the difference between the upper solid and upper dashed/dotted
lines AND the difference between the lower solid and upper
dashed/dotted lines is no greater than $\sqrt{t-s}$ (where s and t are the left and right hand
end-points of the interval respectively). This control on the layer size
can be removed by changing the refine = TRUE
attribute to
refine = FALSE
on creation of the path. Note that this is
not recommended as it can introduce numerical instability, but to show
its affect consider the following Bessel layer over the interval [0, 10]:
Now, if we simulate a number of intermediary points (say a grid of 20), then note that although we again have Bessel layers that the size of the Bessel layers is “stretched” when contrasted with earlier simulations:
Contrast this with a similar path where we haven’t set
refine = FALSE
:
Whether the package does refinement by default is inherited from the
attribute when the path is created. We can change this at some future
point by appropriately specifying within the other functionals in the
same manner as the prefer
attribute discussed earlier. For
instance, consider the introduction of mid-points to the existing path
in the interval [0, 5]:
Note that the intervals in the first half of the path have been refined, but the second half of the path remains the same.
Now, in the case where we are refining by default
(refine = TRUE
), then we may want to alter the default
layer size. This is done using the mult
attribute, which
was described earlier with Localised layers. Recall that by default when
refining, the difference between the upper solid and upper dashed/dotted
lines AND the difference between the lower solid and upper
dashed/dotted lines is no greater than $\sqrt{t-s}$. The mult
attribute
multiplies this tolerance by a user-specified amount. By default,
mult = 1
and is specified at the creation of the path. For
instance, consider a new path (bm5
) together with Bessel
layers in the interval [0, 10], but
with the default layer size multiplied by 3 :
The simulation of the path at additional points will inherit
mult = 3
. For instance, contrast the following with
bm4
above:
If we introduce an additional points (say mid-points to the existing
path in the interval [0, 5]), but we
want increased increased refinement (say of mult = 1
) this
can be done by changing the default argument in any of the earlier
functions as before. For instance:
Note that the intervals in the first half of the path are far more
resolved. It is possible to set the layer size to some constant level
(not dependent on the length of the interval) by specifying
mult
to be at a level of constant multiplied by the
reciprocal of $\sqrt{t-s}$. This
however is not recommended, as it can introduce numerical
instability.
Finally, we may wish to refine the existing intervals without the
introduction of additional points. This can be achieved using the
refine
function, which can increase the resolution
on any (or all) intervals. For instance:
The mult = 1
argument has increased the resolution of
existing layers in the interval [5, 10]
by 3×, and will be comparable to the
first half of the path.
It is possible that the user wants to delete both the path
observations and layers, or only the layers, of the skeleton over some
specified interval. This can be achieved within the package by using the
delete.skeleton()
function, and specifying an open interval
which you wish to delete from the skeleton. The left and right hand ends
of the open interval are specified using the arguments l
and r
respectively. For instance, considering
bm5
, we may wish to delete the entire skeleton in the
interval (2.5, 5):
By default the package will delete both path observations and layers
within the open interval (by default the argument
type = "all"
). However, deletion of only the layers
(retaining the path observations) is possible by setting
type = "layer"
. For instance, considering deleting only the
skeleton layers in the interval (5, 7.5):
It is also possible to delete all user labels (mirroring the
functionality of delete.labels
discussed earlier, automatic
labels are not deleted). For instance, when running the following code,
note that the automatically generated labels between l
and
r
are retained:
bm5 |> delete.skeleton(l = 5, r = 7.5, type = "label")
bm5[,"labels"]
#> $start
#> [1] 0
#>
#> $end
#> [1] 10
#>
#> $seg.start
#>
#> 0
#>
#> $seg.end
#>
#> 10
#>
#> $forced
#> [1] 0
#>
#> $user
#> [1] 10.00 5.00 7.50 2.50 9.00 9.50 6.50 7.00 1.50 2.00 8.50 8.00
#> [13] 6.00 5.50 1.00 0.50 2.25 1.75 1.25 0.75 0.25
Note that as a safety precaution, l
and r
must co-incide with time points within the skeleton. As a
consequence, if You want to delete an interval which does not co-incide
with time points within the skeleton then You must first introduce those
points using the sim
function. For instance, consider
deleting the skeleton entirely in the interval (2.0001, 7.9999) (noting these times do not
currently appear in the skeleton) then this can be achieved as
follows:
In some scenarios we may wish to delete the path entirely to the left or right of some point. This is possible by not specifying the right or left point (respectively). For instance, suppose we wish to delete the skeleton entirely to the right of t = 2 (fixing the path to the left of t = 2) then:
Alternatively, deletion entirely to the left of t = 1 is possible in a similar
manner by not specifying the l
argument:
Note that (as a fail-safe) it is not possible to delete the entire
path by leaving both arguments un-specified. Intead You should
do this by removing the bm5
object from your workspace
using the base R rm
command. If you attempt to do this You
will get an (informative) error:
For many practical uses of the package, the user may wish to add in
path discontinuities. In such settings we will have a number of path
“segments” (the term we give for the continuous paths separated by the
path discontinuities). To aid this, in addition to "t"
and
"W_t"
, the stored path actually contains a
"W_tm"
object:
bm[,"W_tm"]
contains the value of the path at those
times, Wt−.Note that when considering the simulation of any intermediary time
for an existing path (using the sim()
function), we treat
the process as cadlag: for instance, consider simulation at time t′ for an existing path (using the
sim()
function), where t1 < t′ < t2
and t1 and t2 are the closest
realisations of the path on either side, then the simulation will be
done conditional on (t1, Wt1)
and (t2, Wt2−).
To illustrate "W_tm"
, we begin by considering a new
Brownian motion object, bm6
, realised at a collection of
times:
Note that if we look at the associated $t
,
$W_t
and $W_tm
for the bm6
Brownian motion object, that (as suggested by our plot above) we have a
single continuous path (Wt = Wtm
at all times the path has been realised):
data.frame(t = bm6[,"t"],
W_t = bm6[,"W_t"],
W_tm = bm6[,"W_tm"],
equality = bm6[,"W_t"] == bm6[,"W_tm"])
#> t W_t W_tm equality
#> 1 0 0.00000000 0.00000000 TRUE
#> 2 1 -0.60461072 -0.60461072 TRUE
#> 3 2 0.15414496 0.15414496 TRUE
#> 4 3 -0.02528936 -0.02528936 TRUE
#> 5 4 -0.41498855 -0.41498855 TRUE
#> 6 5 -1.38261959 -1.38261959 TRUE
#> 7 6 -0.78879135 -0.78879135 TRUE
#> 8 7 -0.81582735 -0.81582735 TRUE
#> 9 8 -1.55678461 -1.55678461 TRUE
#> 10 9 -1.32747495 -1.32747495 TRUE
#> 11 10 -0.12975571 -0.12975571 TRUE
Now, to add in a path discontinuity we can use the
add.segment()
function, which moves by some user
specified amount the path between two times which appear in the path
skeleton , or the entire path to the right of a given time,
or the entire path to the left of a given time. There are a
number of ways the user can specifiy the movement.
For instance, suppose we wish to move the path between times in the
interval t ∈ [1, 3) by a
constant 1, then this can be achieved
using the optional delta
argument. We may wish to
label
this and so we can additionally pass in our own
labelling argument:
data.frame(t = bm6[,"t"],
W_t = bm6[,"W_t"],
W_tm = bm6[,"W_tm"],
equality = bm6[,"W_t"] == bm6[,"W_tm"])
#> t W_t W_tm equality
#> 1 0 0.00000000 0.0000000 TRUE
#> 2 1 0.39538928 -0.6046107 FALSE
#> 3 2 1.15414496 1.1541450 TRUE
#> 4 3 -0.02528936 0.9747106 FALSE
#> 5 4 -0.41498855 -0.4149885 TRUE
#> 6 5 -1.38261959 -1.3826196 TRUE
#> 7 6 -0.78879135 -0.7887913 TRUE
#> 8 7 -0.81582735 -0.8158274 TRUE
#> 9 8 -1.55678461 -1.5567846 TRUE
#> 10 9 -1.32747495 -1.3274749 TRUE
#> 11 10 -0.12975571 -0.1297557 TRUE
Note that the plot of the path has changed. Vertical, dotted, black lines at t = 1 and t = 3 indicate there are path discontinues at those times. As per convention with cadlag processes, at t = 1 (for instance) there is an open circle indicating the value the path take at the time (W1−), and (as per the package convention) a block dot at t = 1 represented the value the path at W1. This can be verified in our data frame. As discussed above, conditional simulation of the path at intermediary times is appropriately handled by the package. For instance, consider a sequence of times between t = 0 and t = 4:
Further note that the automatic labels
we introduced for
user convenience are now additionally identifying the segment start and
end times (seg.start
and seg.end
respectively), and those appear in addition to the labels we’ve passed
into the add.segment()
function:
bm6[,"labels"]
#> $start
#> [1] 0
#>
#> $end
#> [1] 10
#>
#> $seg.start
#> [1] 0 1 3
#>
#> $seg.end
#> [1] 1 3 10
#>
#> $forced
#> [1] 0
#>
#> $user
#> [1] 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0 3.2 3.4 3.6 3.8 2.2
#> [16] 2.4 2.6 2.8 1.2 1.4 1.6 1.8 0.2 0.4 0.6 0.8
#>
#> $path.move
#> [1] 1 3
Of course, shifting the path by some fixed amount may not be natural
in all settings. Suppose instead we want to at t = 10 “jump” to some given point
(for instance, W10 = 0), then this can
again be done by “moving” the path fixed at the left by the current end
point (i,e. [10, ∞)), to 0, by means of the W_t
optional
argument in add.segment()
. To better illustrate this,
following our “jump” to 0 we will
simulate the path to t = 11:
Using only the argument r
we can shift the path fixed at
some right point in the same fashion. For instance, everything in the
interval t ∈ [0, 5) being
shifted such that W0 = 4:
A cautionary note is that if we shift the entire path by specifying
l
and r
to be the start and end of the path
respectively, then that will introduce path discontinuities at those
time since the specification of l
and r
is of
a half-open interval (ie t = 0− and t = 11 are both times that appear in
the existing path skeleton, and we would be moving the path in the
interval t ∈ [0, 11)). For
instance,
Instead, we should omit both the l
and r
arguments (we will first undo the previous path move):
bm6 |>
add.segment(l = bm6["start", "t"], r = bm6["end", "t"], delta = 1) |>
add.segment(delta = -1)
plot(bm6)
Further note that the syntax of the delete.skeleton()
function matches that of add.segment()
. When dealing with
path segments aligned with the specified l
and
r
in then delete.skeleton()
function then
everything in the interval [l, r) will be deleted:
Wl will be
deleted, and within the bm
object Wl− will be
copied so Wl− = Wl;
Wr− will
be deleted and replaced with Wr within the
bm
object. For instance, lets consider removing the path
segment we introduced between t = 1 and t = 3:
data.frame(t = bm6[,"t"],
W_t = bm6[,"W_t"],
W_tm = bm6[,"W_tm"],
equality = bm6[,"W_t"] == bm6[,"W_tm"])
#> t W_t W_tm equality
#> 1 0.0 3.0000000 3.0000000 TRUE
#> 2 0.2 3.0927744 3.0927744 TRUE
#> 3 0.4 3.2950951 3.2950951 TRUE
#> 4 0.6 2.4342801 2.4342801 TRUE
#> 5 0.8 2.4721542 2.4721542 TRUE
#> 6 1.0 2.3953893 2.3953893 TRUE
#> 7 3.0 2.9747106 2.9747106 TRUE
#> 8 3.2 2.9679436 2.9679436 TRUE
#> 9 3.4 2.6541898 2.6541898 TRUE
#> 10 3.6 2.7234927 2.7234927 TRUE
#> 11 3.8 3.2457690 3.2457690 TRUE
#> 12 4.0 2.5850115 2.5850115 TRUE
#> 13 5.0 -2.3826196 1.6173804 FALSE
#> 14 6.0 -1.7887913 -1.7887913 TRUE
#> 15 7.0 -1.8158274 -1.8158274 TRUE
#> 16 8.0 -2.5567846 -2.5567846 TRUE
#> 17 9.0 -2.3274749 -2.3274749 TRUE
#> 18 10.0 -1.0000000 -1.1297557 FALSE
#> 19 11.0 0.8149376 0.8149376 TRUE
Note that (as in the above example) the deletion of part of a path may result in a change to the number of segments: this will result in an updating of the associated automatic labelling (but not the user specified labelling).
bm6[,"labels"]
#> $start
#> [1] 0
#>
#> $end
#> [1] 11
#>
#> $seg.start
#> [1] 0 5 10
#>
#> $seg.end
#> [1] 5 10 11
#>
#> $forced
#> [1] 0
#>
#> $user
#> [1] 1.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0 3.2 3.4 3.6 3.8 0.2 0.4
#> [16] 0.6 0.8 11.0
#>
#> $path.move
#> [1] 1 3
We can access the vector associated with W_tm
by calling
the environment in the same way as previously. Alternatively, by not
specifying any argument then calling the environment returns
t
, W_t
, W_tm
and
layers
:
bm6[t = 1]
#> $t
#> [1] 1
#>
#> $W_t
#> [1] 2.395389
#>
#> $W_tm
#> [1] 2.395389
#>
#> $layers
#> # A tibble: 0 × 9
#> # ℹ 9 variables: type <fct>, t.l <dbl>, t.u <dbl>, Ld <dbl>, Uu <dbl>,
#> # Lu <dbl>, Ud <dbl>, Lu.hard <lgl>, Ud.hard <lgl>
#>
#> $labels
#> $labels$user
#> [1] 1
#>
#> $labels$path.move
#> [1] 1
In some settings it may be useful to concatenate path skeletons.
Seperate path skeletons can be considered as segments of a newly created
Brownian motion object, and if the paths are align then be considered as
single segments. This can be done using the bm.concat()
function that we introduced earlier: but in addition to passing in a
single bm
together with the required t0
argument specifying the start of the newly created path, any
number of paths in the order they are to be concatenated. For instance,
consider the concatenation of bm6
together with
bm5
, and then again bm6
, into a newly created
bm.concat
object initialised at t = 10
Creating a multi-dimensional Brownian motion object is again
accomplished using the create.bm()
function, by specifying
a number of different (optional) arguments. In addition to time
t
(defaulting to t = 0) and position W_t
(defaulting to vector 0), we have
dim
which specifies the number of dimensions (default dim
= 1) and covariance matrix
cov
(defaulting to the identity matrix). For instance, a
standard three-dimensional Brownian motion may be created as
follows:
Some care has to be made in specifying W_t
in multiple
dimensions. If the specified vector is of length one, then that position
is used to specify all coordinates of the path at the initial time. For
instance,
Whereas to initialize the path at the position (0, 1, 2) we simply provide a vector to the
W_t
argument:
As before, we can specify on the path creation multiple points in the
path. Here we supply a vector to t
and a matrix of
dimension the length of t
times dim
. For
instance, consider the path at t = 1 at W1 = (0, 1, 2) and at
t = 2 at W2 = (2, 0, 1)
bm.d <- create.bm(t = c(1,2), W_t = matrix(c(0,1,2,2,0,1),nrow = 2, ncol = 3, byrow = TRUE), dim=3)
bm.d[t=c(1,2), "W_t"]
#> [,1] [,2] [,3]
#> [1,] 0 1 2
#> [2,] 2 0 1
The function add.points
can be used directly in multiple
dimensions. For instance, if we want to add the point W0 = (−1, −1, −1).:
bm.d |> add.points(t = 0, W_t = c(-1, -1, -1))
bm.d[t=c(0,1,2), "W_t"]
#> [,1] [,2] [,3]
#> [1,] -1 -1 -1
#> [2,] 0 1 2
#> [3,] 2 0 1
Similar care has to be taken in specifiying the covariance matrix
cov
. cov
if specified as a scalar is
interpreted as a multiplier of the identity matrix, if it is specified
as a vector of length dim
then it is interpreted as
diagonal with the vector as the diagonal elements, or it can be
specified as a matrix. For instance,
bm.d <- create.bm(dim = 3, cov = (diag(x= 0.5, nrow=3) + 0.5))
bm.d$cov
#> [,1] [,2] [,3]
#> [1,] 1.0 0.5 0.5
#> [2,] 0.5 1.0 0.5
#> [3,] 0.5 0.5 1.0
The final arguments in create.bm
– namely
refine
, mult
and prefer
– have
precisely the same interpretation as in the one dimensional setting
(with the same defaults). The one difference is they can alternatively
be specified as vectors of length dim
if You wish different
specification in each dimension.
Having created a BrownianMotionNd
object, simulation of
the path and layers is possible using exactly the same syntax as for the
sim
and layers
functions before. For instance,
suppose we simulate the path at times t = (0.5, 5) and add a layers over
the interval t = [0, 10] then
this is achieved simply by:
Note that additional arguments for layers
operate in
exactly the same fashion in multiple dimensions. Note however
first.passage
is not possible in multiple dimensions as it
does not have the same clear interpretation as in one dimension
(although "localised"
can be specified for the argument
type
in layers
).
At this point we can see a plot of our nascent path, by simply using
the plot()
function:
In multiple dimensions the plotting is somewhat different. By default a ‘pairwise’ plot is produced: each component is plotted along the diagonal and labelled appropriately. Note in the diagonal plots there only exists “black” layers in addition to the plotted path: this indicates intervals in which the entire path is constrained, but due to the covariance structure of the path, this does not have the more nuanced interpretation possible in the one-dimensional case (this is discussed more in the later plotting section). The off-diagonal plot on row i column j simply shows coordinate i plotted against coordinate j (with no axis for time). The red lines demarcate a region of the two dimensional space that the entire path is constrained in for the entire interval. Whereas the black demarcates a region parallel to the axis which constrains the red region. Again, more discussion on plotting in multiple dimensions is given in the later plotting section.
Refinement and Deletion also use the same functions
(refine
and delete.skeleton
respectively) and
syntax as in one dimension. For instance, suppose we wish to refine the
entire path using the mult
argument with mult = 0.1, and delete the entire path to the
right of t = 5, then we can do
this as follows:
A little more care has to be taken in adding in segments using the
add.segment
function. Although the arguments are the same
as in one dimension, specifying the argument W_t
instead
follows the same conventions as in in the create.bm
function discussed earlier. In particular, if W_t
is a
scalar then then point is repeated in every component, else a vector has
to be specified of length dim
. For instance, consider
adding a new segment at t = 5
by jumping to the point W5 = (10, 11, 12) can be
achieved as follows (extended to t = 10 with layers to more clearly
show the discontinuity):
Note that the discontinuity in the diagonal pairwise plots is indicated using a dashed line (as in the one-dimensional setting). Similarly, in the off-diagonal plots the point of discontinuity (from Wt− to Wt) is joined using a dashed line.
Path concatenation is also possible, but care has to be taken to
concatenate paths of both the same dimension and covariance structure
(there are a number of checks carried out in this scenario to ensure
this is the case). For instance, consider concatenating
bm.d
with itself into a new object named
concat.bm.d
, initialised at t = 5:
Of course, the off-diagonal plots don’t appear to have changed due to overplotting.
There were some examples above of indexing into the Brownian Motion
object which show that the interface in multiple dimensions is in
harmony with that of univariate paths. Therefore, all the standard
indexing behavior described in the earlier section
applies, with the obvious changes to multi-dimensional outputs for
W_t
and W_tm
.
Repeating the earlier examples on a multi-dimensional path:
bm.d[,"t"]
#> [1] 0.0 0.5 5.0 10.0
bm.d[,"W_t"]
#> [,1] [,2] [,3]
#> [1,] 0.000000 0.0000000 0.00000
#> [2,] 0.775882 -0.7164934 0.16457
#> [3,] 12.000000 10.0000000 10.00000
#> [4,] 12.740161 13.0331808 9.02344
bm.d[t = 0.5, "W_t"]
#> [,1] [,2] [,3]
#> [1,] 0.775882 -0.7164934 0.16457
bm.d[4, "W_t"]
#> [,1] [,2] [,3]
#> [1,] 12.74016 13.03318 9.02344
The simulation of a path with non-identity covariance matrices relies
upon transforming the path to one with identity covariance matrix,
simulating in this transformed space, and then doing the reverse
transformation as required. In particular, we consider the
transformation Zt = Wt(Λ−1/2)′,
where Wt
is our original path and Λ is
our specified covariance matrix, cov
.
It is possible to access the transformed (standard) Brownian motion
path by accessing the $Z
member and using this in the usual
way (ie to access times, positions etc), noting that the position is now
denoted Z_t
rather than W_t
in this space:
bm.d$Z[,"t"]
#> [1] 0.0 0.5 5.0 10.0
bm.d$Z[,"Z_t"]
#> [,1] [,2] [,3]
#> [1,] 0.000000 0.0000000 0.0000000
#> [2,] 1.122364 -0.8945207 0.2015562
#> [3,] 2.144014 7.4645225 12.2474487
#> [4,] 1.531647 11.3656170 11.0514117
bm.d$Z[t = 0.5, "Z_t"]
#> [,1] [,2] [,3]
#> [1,] 1.122364 -0.8945207 0.2015562
bm.d$Z[4, "Z_t"]
#> [,1] [,2] [,3]
#> [1,] 1.531647 11.36562 11.05141
Layer information can be obtained in the standard way:
bm.d$Z[,"layers"]
#> # A tibble: 3 × 5
#> t.l t.u outer.L[,1] [,2] [,3] outer.U[,1] [,2] [,3] inner.cube
#> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <list>
#> 1 0 0.5 -0.0884 -1.16 -0.530 1.21 0.265 0.732 <dbl [8 × 3]>
#> 2 0.5 5 -2.48 -6.68 -2.43 1.92 0.166 0.732 <dbl [8 × 3]>
#> 3 5 10 -0.704 5.23 8.82 4.38 13.6 14.5 <dbl [8 × 3]>
Again, each row of the tibble specifies layers information for an
interval of time (between t.l
and t.u
). Note
however that the formatting of the layer information in each row, in
multiple dimensions, is slightly different. Here, for ease of
representation, we only supply the outer most layer for each component
(Ld
and Uu
). This is then composed in vectors
of length dim
in outer.L
(the lower bounds)
and outer.U
(the upper bounds) respectively. For instance,
element 2 of outer.L
is Ld
in component 2 of
Z. inner.cube
is
a matrix of size 2dim × dim
specifying the vertices of the hypercube obtained by composing for each
component of Z the vectors
outer.L
and outer.U
.
To access the full detail (including full layer information) for each component of Z (for instance, component 2), this can be achieved as follows:
bm.d$Z.bm[[2]][]
#> $t
#> [1] 0.0 0.5 5.0 10.0
#>
#> $W_t
#> [1] 0.0000000 -0.8945207 7.4645225 11.3656170
#>
#> $W_tm
#> [1] 0.0000000 -0.8945207 -5.6146243 11.3656170
#>
#> $layers
#> # A tibble: 3 × 9
#> type t.l t.u Ld Uu Lu Ud Lu.hard Ud.hard
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <lgl> <lgl>
#> 1 bessel 0 0.5 -1.16 0.265 -1.12 0.221 FALSE FALSE
#> 2 bessel 0.5 5 -6.68 0.166 -6.54 0.0336 FALSE FALSE
#> 3 bessel 5 10 5.23 13.6 7.46 11.4 TRUE TRUE
#>
#> $labels
#> $labels$start
#> [1] 0
#>
#> $labels$seg.start
#> [1] 0 5
#>
#> $labels$forced
#> [1] 0
#>
#> $labels$user
#> [1] 0.5 5.0 10.0
#>
#> $labels$end
#> [1] 10
#>
#> $labels$seg.end
#> [1] 5 10
Note that this is in exactly the same format as the discussion in the one-dimensional setting (indeed, it is a one-dimensional Brownian motion).
The full Z object can be plotted as follows:
Notice that the diagonal plots are now demarked Z (as opposed to W), and again are coloured (following the same convention as the one dimensional plotting). The off-diagonal plots have the same interpretation as previously discussed in the W setting.
Now, returning to the original (untransformed) path, W. In addition to using indexing to
obtain t
, W_t
, and labels
, we can
also obtain layer information in the usual way:
bm.d[,"layers"]
#> # A tibble: 3 × 5
#> t.l t.u outer.L[,1] [,2] [,3] outer.U[,1] [,2] [,3] inner.cube
#> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <list>
#> 1 0 0.5 -0.933 -1.16 -0.433 1.71 0.441 0.598 <dbl [8 × 3]>
#> 2 0.5 5 -7.04 -6.48 -1.99 2.37 0.355 0.598 <dbl [8 × 3]>
#> 3 5 10 6.32 7.07 7.20 18.4 16.0 11.8 <dbl [8 × 3]>
Here, the layer convention follows that discussed for Z above. The inner.cube
is obtained by doing the reverse transformation of each of the 2dim × dim vertices of the
inner.cube
in the transformed Z object. As such, it specifies a
region of W space which
contains the entire path over an interval of time. outer.L
(resp. outer.U
) is then composed by taking the minimum
(resp. maximum) over each coordinate of the vertices. As such,
outer.L
and outer.U
specify an outer bounding
hyper-cube, parallel to the axis in the W space.
We return to plotting now we more fully understand the layers of the Brownian motion object. Consider the W plot:
Here, in the off-diagonal plots to construct the region demarcated by
the red lines, we have simply taken the convex hull of all vertices
given by the inner cube
over the specified time window, and
projected the hyper cube onto the relevant two dimensional components.
The interpretation is that the entire path is constrained in the inner
red region for the entire time requested. The region given by the black
lines is simply an outer hypercube parallel to all axis which bounds the
red region (again, composed by taking the minimums and maximums over
every component), which in practice will be somewhat easier to use.
Of course, we may wish to only plot over a shorter period of time,
for instance t = [0, 0.5],
which can be achieved using the t.lim
argument as
before:
Notice, if we plot for Z the same time interval, then as the time interval only comprises a single layer, then both red and black demarked regions overlap, and they are parallel to the axis:
Of course, we may wish a more refined look at the layer information.
This can be achieved using the argument layers.2d
and
specifying "convex"
(the default, described above),
"detailed"
, or "union"
.
"detailed"
simply plots each layer seperately. For instance
(returning to the entire interval t = [0, 10]):
Note here red and black demarked regions are drawn for every layer in the requested time interval. Visually it is clear in this instance that there are two regions in which the path is constrained – corresponding to the interval prior to the addition of a path segment, and after.
In cases of paths with lots of layer information,
"detailed"
can become cluttered. Instead, here we can use
"union"
which takes the union of all red demarked regions.
Note that (i) this depends on an external package, sf
,
which is very large; and (ii) is more computationally expensive to
compute. Hence, although it is a more accurate portrayal of the layer
information, it is not the default option (and sf
is only a
“Suggested” not required package).
Here, we have identified the two red demarked regions corresponding to the union of two dimensional projections of the hypercubes in every interval prior to the discontinuity, and every interval after. As before, the black demarked region identifies a bounding hypercube parallel to the axis.
The remaining plotting options are:
pairs
(TRUE
/FALSE
) to
indicate whether to produce the full pairwise plots (TRUE
),
or only off-diagonal plots (FALSE
).dims
which enables listing only a subset of dimensions
to plot.Together, pairs
and dims
allow You to plot
any part of the pairwise plots. For instance, if we wanted to plot only
the one-dimensional component 2, then this achieved by:
Whereas, if we wanted a "union"
plot of component 3
against component 2, then this is possible as follows: