Line-of-sight table#

pianistpy.LosTable, pianistpy.losvd(), pianistpy.sigma_los(), pianistpy.sigma_los_aperture(), pianistpy.sigma_r(), pianistpy.sigma_t(), pianistpy.pdf_v()

LosTable(model) computes, once, everything the observables of a model read:

  • the intrinsic dispersion profile (Radial velocity dispersion);

  • the surface density \(\Sigma(R_{\rm 2D})\) and the line-of-sight dispersion \(\sigma_{\rm los}(R_{\rm 2D})\) over projected radius;

  • the line-of-sight velocity distributions \(D(R_{\rm 2D}, V_{\rm los})\) (Line-of-sight velocity distribution), one row per projected radius, from the center outward.

Every observable then takes the table in place of the model and reads its values from the table. Build a table when a model is asked for more than one observable, or for one observable many times (a fit, a grid of apertures).

As in Chiang+(2026d), the dispersion inside an aperture is the surface-density weighted average of \(\sigma_{\rm los}^2\). A centered circle of radius \(R_{\rm ape}\) gives

\[\overline{\sigma}_{\rm los}^2(R_{\rm ape}) = \frac{\int_0^{R_{\rm ape}} \Sigma(R_{\rm 2D})\,\sigma_{\rm los}^2(R_{\rm 2D})\,R_{\rm 2D}\,{\rm d}R_{\rm 2D}} {\int_0^{R_{\rm ape}} \Sigma(R_{\rm 2D})\,R_{\rm 2D}\,{\rm d}R_{\rm 2D}},\]

and an offset circle or a rectangle gives the same average over its own area. sigma_los_aperture() computes the dispersion inside an aperture in two ways:

  • Given the model, sigma_los_aperture computes the surface-density weighted average of \(\sigma_{\rm los}^2\) and reads no velocity distribution.

  • Given the table, sigma_los_aperture takes the second moment of the aperture’s velocity distribution, which is the table’s rows averaged over the aperture with the surface density as the weight.

The results on the model and on the table agree to a relative error of \(10^{-6}\). The call on the table takes longer than the call on the model.

Arguments and units#

LosTable(model) takes a Model of one component or a superposition (superpose()). For a superposition it holds one table per component in parts, and reads each with its component’s weight.

The table is computed in internal units, at the model’s own settings. table.to("physical", ...) recomputes nothing. table.to returns the same tables presented in the units of model.to("physical", ...), and the observables read from the returned table take and return quantities in those units.

The rows are computed in parallel processes. In a script, build the table under if __name__ == "__main__":, as Python’s multiprocessing requires. A call of losvd() or aperture_losvd() on a model, and a ProjectedSampler given a model, also build a table and need the same if __name__ == "__main__": guard. A notebook or an interactive session needs no if __name__ == "__main__": guard. If a parallel process ends during the computation, the call raises pianistpy.exceptions.WorkerError at once and states the cause. The usual cause is that the machine ran out of memory. Fewer cores (set_cores()) lower the memory that the call uses.

The table holds:

  • model, the model the table was built from;

  • profile (the intrinsic dispersion profile), projected (the surface density and the line-of-sight dispersion over projected radius), kept (the ranges of projected radius of the velocity quadrature) and table (the line-of-sight velocity distributions), each of the first component for a superposition;

  • validation, the table’s accuracy report;

  • parts, one table per component.

Where the rows run#

The rows stop at the first row beyond which less than losvd.outer_mass_share of the model’s total projected mass lies. The default is \(10^{-6}\). Settings(losvd={"outer_mass_share": 0}) keeps the rows to the model’s edge.

By default the rows start at the innermost radius of the dispersion profile. The setting losvd.innermost_read_radius sets the smallest projected radius the table will be read at. The rows then start at the last row inside the projected radius whose disk holds \(10^{-6}\) times the projected mass inside innermost_read_radius. An aperture’s integral then starts at the table’s first row. Each row kept is identical to the row at the same projected radius of a table built with innermost_read_radius and outer_mass_share set to 0.

A read outside the rows raises InputError. For a read beyond the last row or inside the first row, at a point or over an aperture, the error message gives the radius of the end row and the setting that moves the end row (losvd.outer_mass_share or losvd.innermost_read_radius). An aperture whose part inside the first row carries more than \(10^{-6}\) of the aperture’s weight also raises InputError. The error message gives the radius of the smallest centered circle that the table computes to a relative error of \(10^{-6}\).

Accuracy report#

The table adds rows until the interpolation between rows meets the tolerance of the model’s settings. Every row is checked against the surface density and the line-of-sight dispersion, both computed without the row. The report table.validation holds six checks, each with its relative error and the radius where it is worst:

  • norm: each row’s integral against the surface density;

  • second_moment: each row’s second moment against the projected dispersion;

  • unjudged: infinite if a row where the surface density is positive could not be checked, and 0 otherwise;

  • t_converged: infinite if the integral of a row along the line of sight did not converge, and 0 otherwise;

  • certificate: the table’s estimate of its error between rows;

  • layer_floor: the error floor of the integration toward zero angular momentum. The floor is nonzero only for a central anisotropy \(\beta_{\rm c}\) between 1/2 and 1, and a floor above \(10^{-6}\) raises ResultError.

The report’s tier is the band of the worst relative error among the six checks and the model’s own report (Accuracy). Building a table whose relative error is above \(10^{-4}\) issues an AccuracyWarning. A table whose relative error is above \(10^{-2}\) (tier "refused") raises AccuracyError, unless the model’s settings set write_anyway=True. An aperture call on a table (aperture_losvd, sigma_los_aperture) warns above \(10^{-4}\) and raises above \(10^{-2}\) in the same way for the aperture’s distribution. table.table.r_uncertified records the intervals of projected radius where the table could not meet the table’s tolerance. A read inside one of the intervals in table.table.r_uncertified raises ResultError, whose message gives the interval.

Example#

import numpy as np
import pianistpy as pp

model = pp.solve(
    pp.Component(pp.DoublePowerLaw.hernquist(), pp.Anisotropy(beta_c=0.25)),
    pp.Truncation(k_t=0.05),
    settings=pp.Settings(quick=True),
)
table = pp.LosTable(model)                     # built once
table.validation.tier                          # "silent", "recorded", "warned" or "refused"
{key: check.error for key, check in table.validation.checks.items()}

circle = pp.Aperture.circle(1.0)
pp.sigma_los_aperture(model, circle), pp.sigma_los_aperture(table, circle)   # on the model and on the table
pp.sigma_los(table, [0.1, 1.0, 10.0])
pp.losvd(table, 1.0, [0.0, 0.5, 1.0])

physical = table.to("physical", concentration=10.0, mass_0=1e9, r_0=500.0)
pp.sigma_los_aperture(physical, pp.Aperture.circle(500.0))                   # km/s, 500 pc circle

The Representative computing time page gives the time of a table’s first build and of the calls that read the table, measured on a workstation CPU, with a GPU, and on a laptop.