Skip to content

Migrating to pyturb 2.0

pyturb 2.0 adopts the axis convention of the rest of its AO family — aobasis, makewfs, solvephase, shmpipeline-ao and HCIPy: arrays are indexed (y, x), with x along axis 1 (columns) and y along axis 0 (rows). pyturb 1.x labelled axis 0 as x. Array shapes, pixel centres ((i - (n-1)/2) * pitch), units and the turbulence statistics are unchanged; what changed is which array axis every x/y-labelled input refers to.

What changed

Input pyturb 1.x pyturb 2.0
Layer.wind_direction [deg, wind blows from] from axis 0 toward axis 1 from +x (axis 1) toward +y (axis 0)
Layer.wind_vector (v_axis0, v_axis1) (vx, vy) = (v_axis1, v_axis0)
opd(directions=[(thx, thy)]), (thx, thy, altitude) thx along axis 0 thx along x (axis 1), thy along y (axis 0)
opd_at(x, y, direction=...) x along axis 0 x along axis 1, y along axis 0
FourierFlowScreen.translate(sx, sy) sx along axis 0 sx along x (axis 1), sy along y (axis 0)
analysis.zernike_basis / zernike_decompose x = axis 0 (Z2 varies down the rows) x = axis 1 (Z2 varies along the columns), as aobasis
theory.zernike_temporal_psd(..., wind_direction) relative to the 1.x Zernike frame relative to the 2.0 frame (numerically unchanged)
to_config() pyturb_config_version: 1 pyturb_config_version: 2 (v1 still loads, converted)

The meaning of wind_direction is otherwise the same: it is the direction the wind blows from, the pattern moves along -wind_vector, and with wind_direction=0 the pattern now travels toward decreasing column index. InfinitePhaseScreen is unchanged: it extrudes along axis 0, which is now called y.

Reproducing 1.x results exactly

Each 1.x call maps to a 2.0 call that produces the same arrays (verified to float64 round-off on both engines, with directions, LGS sources and opd_at):

pyturb 1.x pyturb 2.0, same arrays
Layer(..., wind_direction=a) Layer(..., wind_direction=90 - a)
with_wind(..., direction=a), discretize_cn2(..., wind_direction=a), from_profile(..., wind_direction=a), from_cn2(..., wind_direction=a) the same with 90 - a
v0, v1 = layer.wind_vector (components along axes 0, 1) v1, v0 = layer.wind_vector
opd(t, directions=[(a, b)]) opd(t, directions=[(b, a)])
opd(t, directions=[(a, b, h)]) opd(t, directions=[(b, a, h)])
opd_at(p, q, t, direction=(a, b)) opd_at(q, p, t, direction=(b, a))
flow.translate(a, b) flow.translate(b, a)
zernike_basis(J, n) zernike_basis(J, n).transpose(0, 2, 1)
zernike_decompose(phase, J) zernike_decompose(phase.swapaxes(-1, -2), J)
Atmosphere.from_config(cfg_v1) unchanged: version-1 configs are converted on load

Before:

import pyturb  # 1.x

layers = [pyturb.Layer(0.0, 0.7, wind_speed=8.0, wind_direction=0.0),
          pyturb.Layer(1e4, 0.3, wind_speed=30.0, wind_direction=30.0)]
atm = pyturb.Atmosphere(layers, r0=0.15, n=128, field_of_view=20, seed=1)
opds = atm.opd(0.01, directions=[(10.0, 0.0), (0.0, 5.0)])

After, the same arrays:

import pyturb  # 2.0

layers = [pyturb.Layer(0.0, 0.7, wind_speed=8.0, wind_direction=90.0),
          pyturb.Layer(1e4, 0.3, wind_speed=30.0, wind_direction=60.0)]
atm = pyturb.Atmosphere(layers, r0=0.15, n=128, field_of_view=20, seed=1)
opds = atm.opd(0.01, directions=[(0.0, 10.0), (5.0, 0.0)])

For new code, prefer stating the physics in the new frame directly (e.g. wind_direction=0 for wind along the columns) over mechanically applying the mapping.

Zernike modes

The 2.0 basis is the 1.x basis reflected about the array diagonal: as arrays, new[k] == old[k].T for every mode, and the 2.0 coefficients of a phase screen equal the 1.x coefficients of its transpose. Mode by mode the reflection (theta -> 90° - theta) is a signed permutation of the 1.x modes with the same radial order n and azimuthal order |m|:

  • m = 0 (piston, defocus, spherical, ...): unchanged.
  • odd |m|: the cosine and sine modes swap, both with sign (-1)^((|m|-1)/2).
  • even |m| > 0: no swap; the cosine mode gets sign (-1)^(|m|/2) and the sine mode (-1)^(|m|/2 + 1).

In Noll order (old 1.x mode on the left, as an array on the same grid):

old new old new old new
Z2 (tip) +Z3 Z9 −Z10 Z16 +Z17
Z3 (tilt) +Z2 Z10 −Z9 Z17 +Z16
Z4 +Z4 Z11 +Z11 Z18 −Z19
Z5 +Z5 Z12 −Z12 Z19 −Z18
Z6 −Z6 Z13 +Z13 Z20 +Z21
Z7 +Z8 Z14 +Z14 Z21 +Z20
Z8 +Z7 Z15 −Z15

So a 1.x coefficient vector c_old becomes c_new[k] = sign * c_old[j] for each old Zj = sign * new Zk row (the relation is exact; checked to 1e-14 over the first 66 modes). In 2.0, Noll Z2 (cos theta) varies along the columns and matches aobasis.ZernikeBasisGenerator on aobasis.positions_from_mask positions to machine precision.

theory.zernike_temporal_psd returns the same numbers as before: it relates a wind direction to the Zernike frame, and both moved together. Pass it the same wind_direction you give the Layer; with wind_direction=0, Z2 is the along-wind tilt.

Site profiles

The named profiles keep their published wind_direction numbers; they are now read in the 2.0 frame. Profiles carry no sky orientation (most directions are illustrative placeholders), so this changes which array axis a layer blows along, not any published quantity (r0, theta0, tau0 and the wind speeds are unchanged). To replay a 1.x run of a named profile exactly, convert its directions:

import pyturb

layers = pyturb.get_profile("keck")
legacy = pyturb.with_wind(layers, [layer.wind_speed for layer in layers],
                          [90.0 - layer.wind_direction for layer in layers])
atm = pyturb.Atmosphere(legacy, seeing=0.8, n=128, seed=1)  # == 1.x "keck"

Saved configs and metadata

Atmosphere.to_config() (and the "config" entry of atm.metadata saved by pyturb.save) now writes pyturb_config_version: 2. from_config still reads version-1 configs written by pyturb 1.x, converting each layer's wind_direction to (90 - a) mod 360, so an old saved run replays the frames it recorded.

HCIPy

pyturb arrays already flattened onto HCIPy grids with .ravel(); with 2.0, x and y also agree, so direction tuples carry over without the axis swap 1.x needed. See Interop for the velocity sign.

Internal engine

pyturb.extrude.ExtrudedAtmosphere (used by engine="extrude", not part of the top-level API) still works in array-axis order: layer_wind entries are (v_axis0, v_axis1). Its direction arguments are renamed slope0/slope1 (tangents along axes 0 and 1) and sample_points takes pix0/pix1.