limTOD.cstbeam — using CST Studio far-field exports

The sibling of limTOD.uvbeam for the other format a measured or simulated horn arrives in: CST Microwave Studio far-field ASCII exports, one file per frequency. Needs only healpy and scipy, both base dependencies — there is no extra to install.

from limTOD.cstbeam import cst_beam_func
from limTOD import TODSim

sim = TODSim(beam_func=cst_beam_func("~/beams/HornDryGround"),
             sky_func=..., ...)

Three entry points

Function

Gives you

read_cst_farfield(path)

one file as (theta_deg, phi_deg, directivity) on its own grid

cst_beam_maps(directory, freq_MHz, nside=...)

(n_freq, npix) HEALPix maps, frequency-interpolated

cst_beam_func(directory)

a beam_func(freq=..., nside=...) for TODSim

cst_beam_func validates the directory and the options at construction, not at the first channel, and caches each file’s HEALPix resampling across calls — a sweep of 200 channels over a 61-file directory parses each file once rather than hundreds of times.

Conventions

Stated because getting one wrong returns a finite, correctly shaped, wrong beam.

Theta. CST’s Theta is measured from the model’s +z axis and maps directly onto the HEALPix colatitude: the boresight sits at the pole, which is what limTOD’s beam maps mean by beam-local.

The quantity. Abs(Dir.) is total directivity in dBi — a power quantity. Maps come back as \(10^{\mathrm{dBi}/10}\), which is the \(B\) of \(\int B T / \int B\). Nothing is normalized here. Divide by your own quadrature \(\int B\) downstream; that is the only way the band limit cancels exactly.

Frequency is in MHz throughout, as elsewhere in limTOD, and is read from the trailing number of each filename’s stem — HornDry70.5.txt is 70.5 MHz. Interpolation between bracketing files is linear in linear power. Extrapolation is refused: a beam invented outside the simulated band is not a beam.

Phi, which the file does not contain

CST’s Phi is measured from the model’s +x axis. limTOD’s beam-map phi = 0 is carried to the direction of increasing elevation. Which physical direction the CST +x axis points is a fact about how the horn was built and mounted, and it is not in the export — so it cannot be recovered here.

Two degrees of freedom are exposed instead:

Option

Meaning

phi0_deg

the CST azimuth that lands on the beam-map phi = 0 meridian

phi_sense

"ccw" if CST azimuth increases with beam-map phi, "cw" if it decreases

The defaults are the identity mapping, which is an assumption to check against the as-built horn, not a result. For a beam with real azimuthal structure the handedness is not a detail — RHINO’s horn varies by 30–60 % around the \(\theta = 30^\circ\) ring, and getting the sense backwards mirrors that structure into the wrong half of the sky while leaving every integral, every peak and every azimuthally-symmetric diagnostic unchanged.

Note

Unlike limTOD.uvbeam, whose azimuth convention is fixed by pyuvdata and is therefore locked numerically by a three-way orientation test, this one cannot be locked: the information is not in the file. What the tests do lock is that the knobs act correctly — that phi_sense is a reflection about phi = 0 rather than a relabelling, and that phi0_deg is a rotation that conserves the integral.

What a CST export looks like

Two header lines, then rows on a regular grid with theta running fastest inside each phi block:

Theta [deg.]  Phi [deg.]  Abs(Dir.)[dBi]  Abs(Theta)[dBi]  ...
--------------------------------------------------------------
     0.000      0.000   1.41421356000000e+01  ...
     2.000      0.000   1.41205511000000e+01  ...

Reshaping phi-fastest gives a correctly-shaped array with the samples transposed — a beam, just not this one — so the reader checks that the rows fill the complete grid they span and raises ValueError if they do not.

API

CST Studio far-field exports -> HEALPix beam maps.

The sibling of limTOD.uvbeam for the other format a measured or simulated horn arrives in: CST Microwave Studio far-field ASCII exports, one file per frequency. Three entry points:

  • read_cst_farfield() — one file into a regular (theta, phi) grid.

  • cst_beam_maps() — a directory of them onto HEALPix maps in limTOD’s beam-map convention (boresight at the pole, RING ordering), interpolated in frequency.

  • cst_beam_func() — the same wrapped as a beam_func(freq=..., nside=...) callable satisfying limTOD.TODSim’s contract, so a CST horn drops straight into a simulation.

Needs only healpy and scipy, both base dependencies — there is no extra to install for this module.

Conventions

Stated because getting one wrong returns a finite, correctly shaped, wrong beam.

  • Theta. CST’s Theta is measured from the model’s +z axis and maps directly onto the HEALPix colatitude: the boresight sits at the pole, which is what limTOD’s beam maps mean by beam-local.

  • The quantity. Abs(Dir.) is total directivity in dBi — a POWER quantity. Maps come back as 10 ** (dBi / 10), which is the B of int(B T) / int(B). Nothing is normalized here; ask the consumer to divide by its own quadrature int(B), which is the only way the band limit cancels exactly.

  • Phi, which is not derivable from the file. CST’s Phi is measured from the model’s +x axis; limTOD’s beam-map phi = 0 is carried to the direction of increasing elevation. Which physical direction the CST +x axis points is a fact about how the horn was built and mounted, and it is not in the export — so it cannot be recovered here. phi0_deg and phi_sense expose the two degrees of freedom (an offset and a handedness). Their defaults are the identity mapping, which is an assumption to check against the as-built horn, not a result. For a beam with real azimuthal structure the handedness is not a detail: RHINO’s horn varies by 30-60 % around the theta = 30 deg ring, so getting it backwards mirrors that structure into the wrong half of the sky while leaving every integral, every peak and every symmetric diagnostic unchanged.

    Unlike limTOD.uvbeam, whose azimuth convention is fixed by pyuvdata and is therefore locked numerically by a test, this one cannot be: the file does not contain the information. What the tests here lock instead is that the knobs do the right thing — that phi_sense is a reflection about phi = 0 rather than a relabelling, and that phi0_deg is a rotation that conserves the integral.

  • Frequency. In MHz throughout, as elsewhere in limTOD, and read from the trailing number of each filename’s stem: HornDry70.5.txt is 70.5 MHz. Interpolation between bracketing files is linear in linear power. Extrapolation is refused — a beam invented outside the simulated band is not a beam.

limTOD.cstbeam.read_cst_farfield(path)[source]

Read one CST far-field export into a regular (theta, phi) grid.

Parameters:

path (str or Path) – A CST Studio far-field ASCII export: two header lines, then Theta Phi Abs(Dir.) ... rows on a regular grid.

Returns:

  • theta_deg (ndarray, shape (n_theta,))

  • phi_deg (ndarray, shape (n_phi,))

  • directivity (ndarray, shape (n_theta, n_phi)) – Linear power, 10 ** (dBi / 10) — not dB.

Raises:

ValueError – If the rows do not fill a complete regular grid. An incomplete export would otherwise reshape into a plausible-looking beam with the samples in the wrong places.

Return type:

tuple[ndarray, ndarray, ndarray]

limTOD.cstbeam.cst_frequency_table(directory, *, suffix='.txt')[source]

Map frequency [MHz] to file for a directory of CST exports.

The frequency is the trailing number of the stem, in MHz — HornDry70.5.txt is 70.5. Files whose stem does not end in a number are ignored, so a README.txt alongside the exports is harmless.

Raises:

ValueError – If the directory holds no matching file.

Parameters:
Return type:

Dict[float, Path]

limTOD.cstbeam.cst_beam_maps(directory, freq_MHz, *, nside, suffix='.txt', phi0_deg=0.0, phi_sense='ccw', _cache=None)[source]

Sample a directory of CST exports onto HEALPix maps at given frequencies.

Parameters:
  • directory (str or Path) – Directory of per-frequency CST exports (see cst_frequency_table()).

  • freq_MHz (array_like, shape (n_freq,)) – Output frequencies [MHz].

  • nside (int) – HEALPix resolution of the output maps (RING ordering).

  • suffix (str) – File extension of the exports.

  • phi0_deg (float) – CST azimuth that lands on the beam-map phi = 0 meridian.

  • phi_sense ({"ccw", "cw"}) – Whether CST azimuth increases with beam-map phi. See the module docstring: this is a fact about the horn, not about the file.

  • _cache (Dict | None)

Returns:

Linear-power beam maps, unnormalized.

Return type:

ndarray, shape (n_freq, 12 * nside ** 2)

Raises:

ValueError – On an unknown phi_sense, or a requested frequency outside the range the directory covers.

Notes

Only the files actually bracketing a requested frequency are read: a production directory holds dozens, each a 65k-row parse.

limTOD.cstbeam.cst_beam_func(directory, *, suffix='.txt', phi0_deg=0.0, phi_sense='ccw')[source]

Wrap a directory of CST exports as a limTOD beam_func.

The returned callable satisfies limTOD.TODSim’s contract, beam_func(freq=..., nside=...) -> (npix,) with freq in MHz, so a CST horn drops straight into a simulation:

sim = TODSim(beam_func=cst_beam_func(horn_dir), sky_func=..., ...)

Chromatic by construction: each channel interpolates between the two bracketing exports.

Configuration errors — an unreadable directory, an unknown phi_sense — surface here rather than at the first call, and the per-file HEALPix resampling is cached across calls, so a simulation sweeping 200 channels through a 61-file directory parses each file once instead of hundreds of times.

Parameters:
Return type:

Callable[[…], ndarray]