Units and quantities

Units

wave_tracer uses a strongly-typed unit system for all physical quantities, and quantities that are read from scene files are expected to specify units as well. This is very useful when writing physical simulation software, and helps catch errors early.

wave_tracer defines wt::f_t as the floating-point representation type for real numbers, and most quantities also use this floating-point type for their underlying representation. wt::f_t defaults to single-precision 32-bit float, but can be compile-time configured.

Internally, wave_tracer uses the aurora au library for strongly-typed units and dimensions. Units are defined in namespace wt::u. In addition, common types for quantities, like length, wavenumber and power, are defined (see below). For example:

length_t len = 3 * u::mm;
area_t area = m::sqr(len);
auto p = irradiance_t(1.1f * u::W / area);
std::cout << std::format("{}", p) << std::endl;

In the above, we define a variable with a quantity dimension of ISQ length and units of millimetre, square it to produce area, and then construct a quantity of radiometric irradiance. wt::length_t stores values in units of metres represented using wt::f_t floating-points.

Conversions

Dimensionless quantities can be cast to a unitless number wt::f_t implicitly or explicitly. Implicit casts are guaranteed to be lossless (i.e., only work with unitless quantities), and fail to compile otherwise:

auto area_ratio1 = area / m::sqr(2 * u::m);
f_t ratio1 = area_ratio1;
auto area_ratio2 = area / m::sqr(2 * u::mm);
f_t try1 = area_ratio2;        // fails to compile! Lossy conversions cannot be done implicitly.
f_t try2 = (f_t)area_ratio2;   // compiles: forces lossy conversion

The implicit cast of area_ratio2 is not a lossless conversion because the type area_t has units of metre squared; therefore, even though the type of area_ratio is a dimensionless quantity, its units include a static scaling constant of \(\tfrac{\text{m}^2}{\text{mm}^2}\), and a conversion to a unitless number cannot be done losslessly. The explicit cast forces a lossy conversion.

Neither form will compile when attempting to convert from a non-dimensionless quantity. To extract the underlying representation without the units, different functions of the form wt::u::in_X are provided. For example,

length_t len = 3 * u::mm;
std::cout << std::format("len = {} mm", u::in_mm(1 * u::m)) << std::endl;
std::cout << std::format("len = {} m", u::in_m(len)) << std::endl;
std::cout << std::format("{}", u::in_Hz(1 * u::m)) << std::endl;   // fails to compile: cannot cast `mm` to `Hz`

The first cast is lossless, and simply discards the units to produce a number (of type wt::f_t); the second cast is lossy, and first converts the representation of len to metres; the third cast does not compile as a conversion from millimetres to Hertz is nonsensical.

Operators and math

Quantities of same dimension can be compared to each other and ordered:

static_assert(1 * u::mm < 1 * u::m);

Comparisons between quantities of different dimensions will fail to compile. Common math operations, like wt::m::sqrt() and wt::m::abs() are defined for quantities and behave properly. Likewise, trigonometric functions are defined for angle quantities. c++ concepts for quantity dimensions are also provided. For example, the following template function accepts only angular quantities:

auto cone_solid_angle(const Angle auto& half_opening_angle) {
   return m::four_pi * m::sqr(m::sin(half_opening_angle / 2.f)) * u::ang::sr;
}

std::cout << std::format("{}", cone_solid_angle(1 * u::ang::rad)) << std::endl;
std::cout << std::format("{}", cone_solid_angle(m::pi/180 * u::ang::deg)) << std::endl;

Point quantities

Some physical quantities require specifying an origin shift. For example, temperatures can be measured in units of Kelvin or degrees Celsius: both are of identical dimension and magnitude, but have shifted origins with respect to each other. To construct a point quantity, the wt::point() function is used:

const auto T = point<u::Kelvin>(5000);
std::cout << "the XYZ colourspace Planckian locus of a 5000K blackbody emitter is "
          << std::format("{}", colourspace::planckian_locus(T)) << std::endl;

// these are quantity points
auto p1 = point<u::Kelvin>(100.);
auto p2 = point<u::Celsius>(10.);
// differences between quantity points yields quantities
auto q1 = point<u::Kelvin>(140.) - p1;
auto q2 = point<u::Celsius>(50.) - p2;

// point quantities represent absolute values.
// shifted origins are correctly respected: 100K is less than 10°C
std::cout << std::format("{} < {} : {}",p1,p2, p1<p2) << std::endl;
// quantities represent relative values: 40K is equivalent to 40°C
std::cout << std::format("{} == {} : {}",q1,q2, q1==q2) << std::endl;
// indeed, adding either 40K or 40°C to 100K yields an identical result:
std::cout << std::format("{} + {} == {}",p1,q1, p1+q1) << std::endl;
std::cout << std::format("{} + {} == {}",p1,q2, p1+q2) << std::endl;

Notice that the difference between a pair of point quantities yields a quantity (not a point quantity). Therefore, quantities of Kelvin and degrees Celsius are equivalent, as they have identical magnitude, but point quantities of Kelvin and degrees Celsius are not equivalent, as they have shifted origin.

Adding a quantity to a point quantity of same dimension yields a point quantity. Adding a pair of point quantities is nonsensical and is ill-formed.

Constants

Several common SI constants are provided, see wt::siconstants.

c_t complex_IOR_for_material(frequency_t freq,
                              f_t rel_permittivity,
                              quantity<decltype(u::Siemens{}/u::metres{})> conductivity) noexcept {
   constexpr auto mu0 = siconstants::vacuum_permeability;
   constexpr auto c   = siconstants::speed_of_light_in_vacuum;

   auto k = freq_to_wavenum(freq);
   auto temporal_freq = k * c;
   auto epsilon0 = f_t(1) / (mu0 * c*c);
   auto rel_sigma = -conductivity / (epsilon0 * temporal_freq);

   return std::sqrt(c_t{ rel_permittivity, (f_t)rel_sigma });
}

// computes and the prints complex IOR for material with 80 relative permittivity and
// 0.055 microsiemens/centimetre conductivity with 1GHz radiation.
auto ior = complex_IOR_for_material(1 * u::GHz, 80, 0.055 * u::µS/u::cm);
std::cout << std::format("{}", ior) << std::endl;

In order for std::format to work with complex numbers, <wt/math/format.hpp> needs to be included. Similarly, the formatting of quantities is provided by <wt/math/quantity/format.hpp>.

General quantities

The following general quantities are defined. The suffix “[unit]” indicates the units used for internal storage.

Dimension

Concept

length \([\small\text{m}]\)

wt::length_t

wt::Length

area \([\small\text{m}^2]\)

wt::area_t

wt::Area

volume \([\small\text{m}^3]\)

wt::volume_t

wt::Volume

length density \([\small\text{m}^{-1}]\)

wt::length_density_t

area density \([\small\text{m}^{-2}]\)

wt::area_density_t

volume density \([\small\text{m}^{-3}]\)

wt::volume_density_t

angle \([\small\text{radians}]\)

wt::angle_t

wt::Angle

solid angle \([\small\text{sr}]\)

wt::solid_angle_t

wt::SolidAngle

angle density \([\small\text{radians}^{-1}]\)

wt::angle_density_t

solid angle density \([\small\text{sr}^{-1}]\)

wt::solid_angle_density_t

wavenumber \([\small\text{mm}^{-1}]\)

wt::wavenumber_t

wt::Wavenumber

wavelength \([\small\text{mm}]\)

wt::wavelength_t

wt::Wavelength

frequency \([\small\text{GHz}]\)

wt::frequency_t

wt::Frequency

wavenumber density \([\small\text{mm}]\)

wt::wavenumber_density_t

wavelength density \([\small\text{mm}^{-1}]\)

wt::wavelength_density_t

power \([\text{Watt}]\)

wt::power_t

wt::Power

temperature \([\text{K}]\) (point)

wt::temperature_t

wt::Temperature

time point \([\text{nanosec}]\) (point)

wt::timepoint_t

wt::TimePoint

time duration \([\text{sec}]\)

wt::duration_t

wt::Duration

As long as the quantity dimensions match, a quantity can be implicitly converted to another. For example: f_t(1) / length_t(2 * u::m) can be implicitly captured by a wt::length_density_t, and vice versa.

Some helper converters for quantities that quantify electrodynamic (EM) radiation frequency are defined:

wavelength to wavenumber

wt::wavelen_to_wavenum()

wavenumber to wavelength

wt::wavenum_to_wavelen()

EM frequency to wavelength (vacuum)

wt::freq_to_wavelen()

EM frequency to wavenumber (vacuum)

wt::freq_to_wavenum()

wavenumber (vacuum) to EM frequency

wt::wavenum_to_freq()

Time

<wt/util/chrono.hpp> provides simple facilities for time tracking.

The current point in time (quantity point of type wt::timepoint_t) given by a wall clock can be retrieved using wt::chrono::wall_clock::now(). A wall clock provides the system time, which can be changed by the user at any point, and is not guaranteed to be monotonic. These time points are point quantities that use 64-bit unsigned integers to count the nanoseconds since epoch: 00:00:00 UTC, 1 January 1970, ignoring leap seconds. Wall clock time points are useful for various timestamps, like last modification date of a file or log messages, but should not be used for time duration tracking.

To compute the elapsed time duration, for example to measure runtime, a monotonic clock is used. wt::chrono::monotonic_clock::now() returns the current time point using an opaque, implementation-specific type, and is guaranteed to be monotonically increasing. Taking the difference between a pair of these types yields a time duration (quantity of type wt::duration_t), which counts seconds using a floating-point representation.

const auto start_time = chrono::monotonic_clock::now();

// ...

const auto duration = chrono::monotonic_clock::now() - start_time;
assert(duration > 0*u::s);
std::cout << "elapsed: " << format::chrono::format("{:%H:%M:%S}", duration) << std::endl;

Formatting facilities for time points and durations are given by the wt::format::chrono::format function (defined in <wt/util/format/chrono.hpp>).

Radiometric quantities

wave_tracer defines common power-derived radiometric quantities, including spectral variants. Note that wave_tracer defines spectral quantities per wavenumber, and not per wavelength.

Dimension

radiated flux \([\small\text{Watt}]\)

wt::radiant_flux_t

radiant intensity \([\tfrac{\text{Watt}}{\text{sr}}]\)

wt::radiant_intensity_t

radiated irradiance \([\tfrac{\text{Watt}}{\text{m}^2}]\)

wt::irradiance_t

radiance \([\tfrac{\text{Watt}}{\text{m}^2 \cdot \text{sr}}]\)

wt::radiance_t

spectral radiated flux \([\small\text{mm}\cdot\text{Watt}]\)

wt::spectral_radiant_flux_t

spectral radiant intensity \([\tfrac{\text{mm}\cdot\text{Watt}}{\text{sr}}]\)

wt::spectral_radiant_intensity_t

spectral irradiance \([\tfrac{\text{mm}\cdot\text{Watt}}{\text{m}^2}]\)

wt::spectral_irradiance_t

spectral radiance \([\tfrac{\text{mm}\cdot\text{Watt}}{\text{m}^2 \cdot \text{sr}}]\)

wt::spectral_radiance_t

Importance-derived quantities are also defined:

Dimension

importance flux \([\small\text{m}^2 \cdot \text{sr}]\)

wt::QE_flux_t

importance flux per solid angle \([\small\text{m}^2]\)

wt::QE_area_t

importance flux per area \([\small\text{sr}]\)

wt::QE_solid_angle_t

importance (Quantum efficiency) \([\small\text{dimensionless}]\)

wt::QE_t

The importance-derived quantities have a similar geometric meaning to the power-derived quantities: for example, spectral radiated flux and importance flux are both quantities that integrate emitted power or importance, respectively, over solid angle and area; spectral irradiance and importance flux per area are both differential quantities that quantify flux (power or importance, respectively) per unit area.

Non-spectral variants of importance-derived quantities above are not defined, as these are not used in practice.

Facilities that are used for light transport, like Stokes parameters vectors and beams (the primary light transport primitive), have specializations for relevant radiometric quantities. For example, wt::spectral_radiance_stokes_t and wt::spectral_radiance_beam_t.