Utilities
PN terms and PN expansions
The basic building blocks of post-Newtonian theory are the terms and expansions. These are used to build up the various expressions that describe the dynamics of the system. The terms are the individual parts of the expansions, while the expansions are the full expressions that are expanded in powers of $1/c$.
PostNewtonian.PNTerm
— TypePNTerm{T,PNOrder,c⁻¹Exponent}
This object represents a single term in a PNExpansion. It has a single field: coeff
, which is the coefficient of the term. The type parameter T
is the type of the coefficient. The type parameter PNOrder
is a half-integer (just as in PNSystem
s) representing the PN order of the expansion. And the type parameter c⁻¹Exponent
is an integer representing the exponent of the PN expansion parameter $1/c$.
PNTerm
s can be multiplied and divided by scalars and exponentiated by integers, to produce another PNTerm
. They can also be added to other PNTerm
s to produce a PNExpansion
.
A simple way to define a PNTerm
or a PNExpansion
is to define the PN expansion parameter
c = PNExpansionParameter(pnsystem)
and use that naturally in formulas, as in
e = 1 + (v/c)^2 * (-ν/12 - 3//4) + (v/c)^4 * (-ν^2/24 + 19ν/8 - 27//8)
Any exponent higher than the desired PNOrder
will be automatically set to zero.
Useful facts:
v
has order1/c
x
has order1/c^2
γ
has order1/c^2
1/r
has order1/c^2
PostNewtonian.PNExpansionParameter
— FunctionPNExpansionParameter(pnsystem)
Create a PNTerm
object representing the post-Newtonian expansion parameter $c$. This can be used to automatically create more complicated PNTerm
s, which combine to form a PNExpansion
. This is a simple but effective way to write PN formulas while automatically tracking the PN order of each term.
PostNewtonian.PNExpansion
— TypePNExpansion{N,T,NMax}
This object can be multiplied by a scalar or another PNExpansion
object, and contains a tuple of coefficients. The coefficients are stored in the order of the expansion, with the zeroth-order coefficient first. Multiplication by a scalar just multiplies each of the elements. Multiplication by another PNExpansion
object is done by convolving the two tuples of coefficients.
Blanchet (2014) defines the post-Newtonian expansion parameter as follows:
This parameter represents essentially a slow motion estimate $ϵ ∼ 𝑣/𝑐$, where $𝑣$ denotes a typical internal velocity. By a slight abuse of notation, following Chandrasekhar et al. [...], we shall henceforth write formally $ϵ ≡ 1/𝑐$, even though $ϵ$ is dimensionless whereas $𝑐$ has the dimension of a velocity. Thus, $1/𝑐 ≪ 1$ in the case of post-Newtonian sources. The small post-Newtonian remainders will be denoted $𝒪(1/𝑐^𝑛)$. Furthermore, [...] we shall refer to a small post-Newtonian term with formal order $𝒪(1/𝑐^𝑛)$ relative to the Newtonian acceleration in the equations of motion, as $\frac{𝑛}{2}\text{PN}$.
Therefore, we consider the coefficients of the PNExpansion
to be a polynomial in $1/𝑐$. Here, the type parameter N
corresponds to the number of elements actually present in the tuple of coefficients, and T
is the type of the coefficients. The NMax
parameter is the maximum number of elements, related to the usual PN order by
\[\text{pn_order} = \frac{\texttt{NMax}-1} {2}.\]
The N
parameter is not related to the PN order; it is just used by Julia to know how many elements are currently in the coefficients, but is required to be 1 ≤ N ≤ NMax.
Macros
Some of the most useful features of this package are the macros allowing us to write PN expressions in fairly natural form, without worrying about calculating all the variables needed for each expression, or manually accounting for the various PN orders to which we may need to truncate PN expansions. To achieve this, we rely primarily on two macros.
PostNewtonian.@pn_expansion
— Macro@pn_expansion [pnsystem] expansion
Gather terms in expansion
by the powers of $1/c$ involved, zeroing out any terms with powers of $1/c$ higher than the pnsystem
's PNOrder
parameter, and combine the terms using the PNExpansionReducer
specified in argument of the function that includes this macro call.
This expansion is achieved by setting — inside a let
block created by this macro —
Note that the pnsystem
argument can be inserted automatically by @pn_expression
.
PostNewtonian.@pn_expression
— Macro@pn_expression [arg_index=1] func
This macro takes the function func
, looks for various symbols inside that function, and if present defines them appropriately inside that function.
The first argument to this macro is arg_index
, which just tells us which argument to the function func
is a PNSystem
. For example, the variables defined in PostNewtonian.FundamentalVariables
all take a single argument of pnsystem
, which is used to compute the values for those variables; this macro just needs to know where to find pnsystem
.
Once it has this information, there are five types of transformations it will make:
- Adds a keyword argument
pn_expansion_reducer::Val{PNExpansionReducer}=Val(sum)
to the function signature. This is used to determine how to reduce the PN expansion terms. The default isVal(sum)
, which will just return a single number, butVal(identity)
can be used to return the expansion. This should be used inside the function asPNExpansionReducer
, and will be automatically used inside any@pn_expansion
. - For every fundamental or derived variable, the name of that variable used in the body of
func
will be replaced by its value when called withpnsystem
. For example, you can simply use the symbolsM₁
orμ
in your code, rather than calling them asM₁(pnsystem)
orμ(pnsystem)
every time they appear. - Every
Irrational
defined inBase.MathConstants
orPostNewtonian.MathConstants
will be transformed to theeltype
ofpnsystem
. This lets you naturally use such constants in expressions like2π/3
without automatically converting toFloat64
. - Each of a short list of functions given by
unary_funcs
inutilities/macros.jl
will first convert their arguments to theeltype
ofpnsystem
. In particular, you can use expressions like√10
orln(2)
without the result being converted to aFloat64
. - Insert the
pnsystem
argument as the first argument to each occurrence of@pn_expansion
that needs it.
To be more explicit, the first three are achieved by defining the relevant quantities in a let
block placed around the body of func
, so that the values may be used efficiently without recomputation.
If you need to use one of the fundamental- or derived-variable functions as arguments of values other than those encapsulated in pnsystem
, you'll need to address them explicitly with the module name — as in PostNewtonian.v(;Ω, M)
.
PostNewtonian.type_converter
— Methodtype_converter(pnsystem, x)
Convert x
to a type appropriate for the float type of pnsystem
.
This is sort of an expansion of the convert
function, but with nicer syntax for types from this package, including the ability to do really weird things for SymbolicPNSystem
. It's needed to ensure that the types of variables and constants are correct when we use them in expressions, rather than just assuming everything is a Float64
.
Termination criteria
Hopefully, it should not be necessary to directly use these termination criteria. They are used by default in the orbital_evolution
function. But certain particularly extreme physical parameters may lead ODEs that are difficult to integrate — especially if new PN systems or terms are introduced. These, or similar functions may be helpful examples of "callbacks" that can be passed to the ODE integrator.
Note that several of these will issue warnings if the evolution has to be terminated for particularly bad or suspicious reasons, even if the quiet
flag is set to true
. See the documentation of the quiet
argument to the orbital_evolution
function for an example of how to use Logging
to quiet even the warnings.
PostNewtonian.decreasing_v_terminator
— Functiondecreasing_v_terminator([quiet])
Construct termination criterion to stop integration when v
is decreasing.
Note that some systems may truly have decreasing v
as physical solutions — including eccentric systems and possibly precessing systems. You may prefer to implement another solution, like detecting when v
decreases below some threshold, or detecting when v
is decreasing too quickly. See this function's source code for a simple
If this terminator is triggered while v
is less than 0.35, a warning will always be issued; otherwise an info
message will be issued only if the quiet
flag is set to false
.
PostNewtonian.dtmin_terminator
— Functiondtmin_terminator(T, [quiet])
Construct termination criterion to terminate when dt
drops below √eps(T)
.
Pass force_dtmin=true
to solve
when using this callback. Otherwise, the time-step size may decrease too much within a single time step, so that the integrator itself will quit before reaching this callback, leading to a less graceful exit.
If this terminator is triggered while v
is less than 0.35, a warning will always be issued; otherwise an info
message will be issued only if the quiet
flag is set to false
.
PostNewtonian.nonfinite_terminator
— Methodnonfinite_terminator()
Construct termination criterion to stop integration when any NaN or Inf is found in the data after an integration step.
If this terminator is triggered, a warning will always be issued.
PostNewtonian.termination_backwards
— Functiontermination_backwards(v₁, [quiet])
Construct termination criteria of solving PN evolution backwards in time
These criteria include checking that the masses are positive and the dimensionless spins are less than 1, as well as ensuring that the evolution will terminate at v₁
.
The optional quiet
argument will silence informational messages about reaching the target value of v₁
if set to true
, but warnings will still be issued when terminating for other reasons.
PostNewtonian.termination_forwards
— Functiontermination_forwards(vₑ, [quiet])
Construct termination criteria of solving PN evolution forwards in time
These criteria include checking that the masses are positive and the dimensionless spins are less than 1, as well as ensuring that the evolution will terminate at vₑ
.
The optional quiet
argument will silence informational messages about reaching the target value of vₑ
if set to true
, but warnings will still be issued when terminating for other reasons.
Manipulating ODE solutions
PostNewtonian.combine_solutions
— Methodcombine_solutions(sol₋, sol₊)
Combine ODESolutions
This function is internal to this package. It is not entirely general, but allows us to combine the backwards- and forwards-in-time solutions of the PN orbital-evolution ODE equations into a single ODESolution
object that should behave just as if it were the result of solve
. In particular, indexing, interpolation, and iterations should behave exactly as described in the DifferentialEquations
docs.
Irrational constants
These quantities are constants that appear in PN expressions, so they are not exported, but can be used by importing them explicitly or by using the fully qualified names. They are defined here as Irrational
s. This means that Julia can convert them to float types as necessary. Unfortunately, by default Julia converts to Float64
. For example, BigFloat(2ζ3)
will be a BigFloat
, but will only have the precision of a Float64
, because 2ζ3
is converted first. To get full precision, you'll need to do things like 2BigFloat(ζ3)
.
One approach to avoiding this is to explicitly redefine these constants as floats of the desired precision, using let
to essentially overwrite the name:
function foo(x)
let ζ3=oftype(x, ζ3)
2ζ3 + x
end
end
Inside the let
block, ζ3
is no longer an Irrational
; it has been converted to whatever number type x
is. Thus, when multiplying by 2, it is not converted to a Float64
; its precision matches that of x
.
This can be quite awkward, so the macro PostNewtonian.@pn_expression
is provided to (among other things) automatically search for all Irrational
s and replace them with the appropriate float values.
PostNewtonian.MathConstants.γₑ
— Constantγₑ
Euler's constant (also known as the Euler–Mascheroni constant) is defined as the limit as $n \to \infty$ of the difference between the $n$th partial sum of the harmonic series and $\log(n)$. This is OEIS sequence A001620.
julia> PostNewtonian.γₑ
γₑ = 0.5772156649015...
julia> n=10_000_000; sum(1 ./ (1:n))-log(n)
0.5772157149015307
PostNewtonian.MathConstants.ζ3
— Constantζ3
apery
Apéry's constant is defined as $ζ(3)$, where $ζ$ is the Riemann zeta function. This is OEIS sequence A002117.
julia> PostNewtonian.apery
ζ3 = 1.2020569031595...
julia> PostNewtonian.ζ3
ζ3 = 1.2020569031595...
julia> sum((1:10_000_000).^-3)
1.2020569031595896
PostNewtonian.MathConstants.ln2
— Constantln2
log2
The natural logarithm of 2. This is OEIS sequence A002162.
julia> PostNewtonian.ln2
ln2 = 0.6931471805599...
julia> exp(PostNewtonian.ln2)
2.0
julia> exp(big(PostNewtonian.ln2))
2.0
PostNewtonian.MathConstants.ln3
— Constantln3
log3
The natural logarithm of 3. This is OEIS sequence A002391.
julia> PostNewtonian.ln3
ln3 = 1.0986122886681...
julia> exp(PostNewtonian.ln3)
3.0000000000000004
julia> exp(big(PostNewtonian.ln3))
3.0
PostNewtonian.MathConstants.ln5
— Constantln5
log5
The natural logarithm of 5. This is OEIS sequence A016628.
julia> PostNewtonian.ln5
ln5 = 1.6094379124341...
julia> exp(PostNewtonian.ln5)
4.999999999999999
julia> exp(big(PostNewtonian.ln5))
5.0
PostNewtonian.MathConstants.ln³╱₂
— Constantln³╱₂
log³╱₂
log3halves
The natural logarithm of 3//2. This is OEIS sequence A016578.
julia> PostNewtonian.ln³╱₂
ln³╱₂ = 0.4054651081081...
julia> exp(PostNewtonian.ln³╱₂)
1.5
julia> exp(big(PostNewtonian.ln³╱₂))
1.5
PostNewtonian.MathConstants.ln⁵╱₂
— Constantln⁵╱₂
log⁵╱₂
log5halves
The natural logarithm of 5//2. This is OEIS sequence A016579.
julia> PostNewtonian.ln⁵╱₂
ln⁵╱₂ = 0.9162907318741...
julia> exp(PostNewtonian.ln⁵╱₂)
2.5
julia> exp(big(PostNewtonian.ln⁵╱₂))
2.5
Truncated series
We also have some utilities for dealing with series — or more precisely summations, since we only handle finitely many terms. In particular, we need truncated multiplication (and some times division) of truncated series. This multiplication is associative and there is a multiplicative identity element — though not always a multiplicative inverse — which means that this structure naturally forms a monoid. (In fact, it also forms more general structures, like a commutative algebra; all we need is the monoidal structure.)
Here, we are assuming that there is a fixed order at which the series are truncated, and that truncated multiplication preserves that order. That is, if $A$ and $B$ are summations in terms up to $v^N$ for some integer $N$, we want their product and ratio (if it exists) to also be a summation in terms up to $v^N$.
Note that we are not referring to these summations as "polynomials" in $v$, because the coefficients will sometimes involve $\ln(v)$ — which is not technically permitted for polynomials. In particular, the presence of logarithms is irrelevant to our meaning of the "order" of the truncated series. This is standard practice in post-Newtonian theory.[1]
The following functions implement this behavior:
PostNewtonian.truncated_series_inverse
— Functiontruncated_series_inverse(a)
truncated_series_inverse!(b, a)
Given the coefficients a
of a series, find the coefficients b
of the multiplicative inverse of that series, up to the order of the original series. That is, if
\[A \colonequals \sum_{i=0}^n a_{i-1} v^i,\]
then we return the coefficients b
of the series
\[B \colonequals \sum_{i=0}^n b_{i-1} v^i\]
such that
\[A\, B = 1 + \mathcal{O}(v^{n+1}).\]
See lagrange_inversion
for the compositional inverse (a.k.a. reversion), which returns the coefficients of $f^{-1}$ such that $f^{-1}(f(v)) = v + \mathcal{O}(v^{n+1})$.
Note that this function returns the coefficients of the inverse, rather than its value. This is relevant for use in truncated_series_product
and truncated_series_ratio
— the latter of which just combines the former with this function.
For example, suppose the input coefficients represent the series
\[A \colonequals \sum_{i=0}^n a_{i-1} v^i.\]
(Remember that Julia's indexing is 1-based, so we subtract 1 to get the index of the coefficient of $v^i$.) Then we return the coefficients b
of the series
\[B \colonequals \sum_{i=0}^n b_{i-1} v^i\]
such that
\[A\, B = 1 + \mathcal{O}(v^{n+1}).\]
This function requires that a[1]
be nonzero. If you have a series that starts at a higher term — say, $v^n$ for $n>0$ — you should factor out the $v^n$, and multiply the series resulting from this function by $v^{-n}$.
Explanation
The inverse coefficients can be computed fairly easily by induction. Start by defining
\[b_0 = 1/a_0.\]
Now, assuming that we've computed all coefficients up to and including $b_{i}$, we can compute $b_{i+1}$ from the condition that the term proportional to $v^{i+1}$ in the product of the series and its inverse must be zero. This gives
\[b_{i+1} = -b_0\sum_{j=1}^{i} a_j b_{i-j}.\]
PostNewtonian.truncated_series_product
— Functiontruncated_series_product(a, b, v)
Evaluate the truncated product of the series a
and b
, which are expanded in powers of v
.
Note that this function returns the value of the summation, rather than its coefficients.
Here we define the series in terms of the coefficients a
and b
as
\[A \colonequals \sum_{i=0}^n a_{i-1} v^i \qquad B \colonequals \sum_{i=0}^n b_{i-1} v^i,\]
and return the value of the product $A\, B$ truncated at $v^n$.
Internally, the sums are performed using evalpoly
.
See also truncated_series_ratio
.
PostNewtonian.truncated_series_ratio
— Functiontruncated_series_ratio(a, b, v)
Evaluate the truncated ratio of the series a
and b
, which are expanded in powers of v
.
Note that this function returns the value of the summation, rather than its coefficients.
Here we define the series in terms of the coefficients a
and b
as
\[A \colonequals \sum_{i=0}^n a_{i-1} v^i \qquad B \colonequals \sum_{i=0}^n b_{i-1} v^i,\]
and return the value of the ratio $A / B$ truncated at $v^n$.
This function simply combines truncated_series_product
and truncated_series_inverse
.
truncated_series_ratio(a, b)
Evaluate the truncated ratio of the series a
and b
, evaluated at expansion value 1. This is relevant when the expansion is not in the dynamic variable v
, for example, but in powers of $1/c$ as in post-Newtonian expansions. (That is, when the v
dependence is already include in the input coefficients.)
PostNewtonian.lagrange_inversion
— Functionlagrange_inversion(a)
Compute the compositional inverse (a.k.a. reversion) of the power series expansion
\[f(x) = a[1]*x + a[2]*x^2 + \ldots + a[n-1]*x^{n-1}\]
about 0, where a
is an NTuple. Note, in particular, that there is no constant term. The result is a similar NTuple b
allowing us to write
\[f^{-1}(y) = b[1]*y + b[2]*y^2 + \ldots + b[n-1]*y^{n-1},\]
such that $f^{-1}(f(x)) = f(f^{-1}(y)) = x$ mod $x^n$.
See truncated_series_inverse
for the multiplicative inverse.
When the constant coefficient $a_0$ is nonzero, the result must be expanded about a different point, which is done by evaluating the output as $f^{-1}(y-a_0)$. Similarly, if the original expansion is about a point $x_0 ≠ 0$, the result must be shifted by adding $x_0$ to the output.
Johansson (2015) summarizes this basic form of the algorithm nicely:
Our setting is the ring of truncated power series $R[[x]]/\langle x^n \rangle$ over a commutative coefficient ring $R$ in which the integers $1,...,n−1$ are cancellable (i.e., nonzero and not zero divisors). [...] If $f(x) = x/h(x)$ where $h(0)$ is a unit, then the compositional inverse or reversion $f^{-1}(x)$ satisfying $f(f^{-1}(x)) = f^{-1}(f(x)) = x$ exists and its coefficients are given by
\[[x^k] f^{-1}(x) = \frac{1}{k} [x^{k-1}] h(x)^k.\]
Note that Johansson also presents a pair of asymptotically faster algorithms for computing the compositional inverse. Because of the low orders of the power series expansions we typically work with, it is not clear if the improved scaling of those algorithms would actually be beneficial in practice, so we stick with the basic algorithm here — though they would not be too difficult to implement if needed.
PostNewtonian.x╱f_mod_xⁿ⁻¹
— Functionx╱f_mod_xⁿ⁻¹(a)
Compute the truncated power series expansion of $x/f$ mod $x^{n-1}$, where $f$ is the power series expansion of a function $f(x)$
\[f(x) = a[1] x + a[2] x^2 + a[3] x^3 + \ldots + a[n-1] x^{n-1}.\]
Note, in particular that there is no constant term. The result is a power series expansion
\[h(x) = h[1] + h[2] x + h[3] x^2 + \ldots + h[n-1] x^{n-2},\]
which notably does have a constant term.
This function is essentially a helper function for the lagrange_inversion
function.
PostNewtonian.hⁱ✖h_mod_xⁿ⁻¹
— Functionhⁱ✖h_mod_xⁿ⁻¹(h, i)
Compute the truncated power series expansion of $h^i$ mod $x^{n-1}$, where $h$ is the power series expansion of a function $h(x)$
\[h(x) = h[1] + h[2] x + h[3] x^2 + \ldots + h[n-1] x^{n-2}.\]
The result is a power series expansion
\[h^i(x) = h^i[1] + h^i[2] x + h^i[3] x^2 + \ldots + h^i[n-1] x^{n-2}.\]
This function is essentially a helper function for the lagrange_inversion
function.
Miscellaneous
PostNewtonian.apply_to_first_add!
— Methodapply_to_first_add!(expr, func)
Apply func
to the first sub-expression found in a "prewalk"-traversal of expr
that satisfies isadd
. If func
acts in place, so does this function. In either case, the expression should be returned.
PostNewtonian.find_symbols_of_type
— Methodfind_symbols_of_type(mod, T)
Given a module mod
(not just its name, but the actual imported module), find all objects inside that module that are instances of the given type T
. The returned quantity is a vector of Symbol
s.
PostNewtonian.flatten_binary!
— Methodflatten_binary!(expr, symbols)
Flatten nested binary operations — that is, apply associativity repeatedly.
PostNewtonian.isadd
— Methodisadd(x)
Return true
if the Expr
x
is a call to (+)
or :+
.
PostNewtonian.iscall
— Methodiscall(x, symbols)
Return true
if the Expr
x
is a call to any element of symbols
.
PostNewtonian.ismul
— Methodismul(x)
Return true
if the Expr
x
is a call to (*)
or :*
.
PostNewtonian.value
— Methodvalue(x)
Return x
or the value wrapped by the Dual
number x
- 1Different texts in post-Newtonian theory treat these logarithmic terms with varying levels of rigor. The preferred method is Hadamard regularization (often referred to in the literature as the partie finie). A good summary is found in Section 6 of Blanchet's Living Review. Another potential approach could be taken following this paper. But for our purposes, it will suffice to take the simplistic approach of treating logarithmic terms as if they were any other constant.