Phase 90 — bundled Observatories table
observatory("VLA") # -> MPosition{ITRF}(...) (case-insensitive)
query(main, "mscal.ha() > 0") # array centre from OBSERVATION.TELESCOPE_NAME- New
src/measures/observatories.jl—observatory(name)returns the ITRF position of a known telescope (~50 entries: VLA / EVLA / ALMA / ACA / APEX / ATCA / GBT / WSRT / GMRT / LOFAR / MWA / ASKAP / MeerKAT / SKA-MID / SKA-LOW / Effelsberg / NOEMA / SMA / JCMT / Arecibo / FAST / …). A bundled snapshot of casacore'sgeodetic/Observatoriesdata table, converted to ITRF Cartesian; station-array placeholders (VLBA / EVN, position 0,0,0) omitted. - The suffix-less
mscal.ha()/mscal.hadec()/mscal.azel()/mscal.pa()/mscal.itrf()/mscal.delay()now use the real array centre —OBSERVATION.TELESCOPE_NAME(per row viaOBSERVATION_ID) looked up in the table — instead of antenna 0. A lookup miss falls back to antenna 0 with a one-time warning. observatoryexported.
- New
test/aqua_tests.jlrunsAqua.test_all(ambiguities, undefined exports, stale deps, compat bounds, unbound type parameters, project extras, persistent tasks — all clean).Base.delete!/Base.insert!(thetaql-style write commands, Phases 30-31, which accept a path string) are whitelisted for the type-piracy check. - Filled in the missing
[compat]bounds Aqua flagged —Dates,Mmap,Random(stdlibs), and the test-onlyAqua/Casacore/CxxWrap/Testextras. Every dependency now carries a bound. - README gains an Aqua badge. The CI workflow (Phase 67) already runs the full test suite, so Aqua runs on every PR.
measconvert for MFrequency / MRadialVelocity now handles the two remaining casacore velocity frames:
LGROUP— the Local Group barycentre (casacoreMeasTable::calcVelocityLGROUP, 308 km/s).CMB— the cosmic-microwave-background rest frame (the dipole, F. Ghigo's 369.5 km/s toward the galactic-coordinate direction).
VEL_LGROUP / VEL_CMB added to src/constants.jl; the LGROUP_BARY / CMB_BARY routes in ext/SOFAExt.jl are the same Doppler-shift form as LSRK↔BARY. Round-trip verified; the constants are copied verbatim from MeasTable.cc (no casatools oracle for these two).
Also: corrected the stale "RefOff is parsed-and-carried" comment — it is ignored (no standard MS column carries one).
query(main, "PHASE_DIR[1] > 10h30m AND PHASE_DIR[2] BETWEEN 40d AND 50d")- The tokenizer recognises a
<number><unit>run whose unit is a sexagesimal token (h/h30m/h30m15s→ hour angle,d/d51m/d51m16→ degrees) and emits a:numtoken with the value in radians (via the Phase-86_parse_sexagesimal)._sexagesimal_unitdoes the classification. - A unit that isn't a sexagesimal token (
30deg,1.4GHz,10m) stays a Phase-69 quantity literal — no regression. 10hnow means "10 hours of hour angle = 150°", not a 10-hour duration (matches casacore TaQL); use arithmetic on seconds for a duration.
query(main, "mscal.el1('10h42m31.3, 45d51m16')") # a J2000 direction
query(t, "abs(RA - angle('10h30m')) < 0.01") # angle() in any expression_parse_sexagesimal(s, kind)(src/taql/functions.jl) —kind∈:ra(h/m/s time, ×15 → degrees) /:dec(d/m/s degrees). Accepts10h42m31.3s,10:42:31.3,10 42 31.3, a leading sign, or a bare decimal (degrees). Returns radians.- A new
angle('...')TaQL-lite function — sexagesimal string → radians (hin the string ⇒ hour angle). mscal.*direction argument (Phase 85) now also accepts a comma-separated sexagesimal'RA, DEC'string, in addition to the[ra, dec]radian pair.- Non-goal: a bare sexagesimal literal in the grammar (
WHERE RA > 10h30m) — needs a tokenizer + node change;angle('10h30m')covers it.
The Phase-77 direction functions (ha / hadec / azel / az / el / pa / itrf / delay) take an optional direction argument instead of FIELD.PHASE_DIR, mirroring casacore's derivedmscal help text.
query(main, "mscal.el1('SUN') > 0.35") # elevation of the Sun
query(main; select = ["az" => "mscal.az1('DELAY_DIR')"])
query(main, "mscal.hadec1([2.0, 0.5])") # a fixed J2000 direction- The argument is a solar-system body name (
'SUN'…'NEPTUNE','MOON'), a FIELD direction column ('PHASE_DIR'/'DELAY_DIR'/'REFERENCE_DIR'— ephemeris-aware via Phase 82), or a[ra, dec]J2000 pair in radians. No argument →FIELD.PHASE_DIR(unchanged). mscal.last(sidereal time) andmscal.uvw_j2000are direction-intrinsic and stay 0-argument.TQLMScalgains adirfield; the sentinel is"mscal.<fn>::<dir>"._mscal_columnsresolves the per-row J2000 direction (_djfor) and keys its frame-conversion memo by(antenna, direction, TIME).
Phase 84 — mscal.corr() / mscal.feed() selection
The two remaining derivedmscal selection UDFs, completing the mscal.* selection set.
query(main, "mscal.corr('RR,LL')") # rows whose pol setup has RR or LL
query(main, "mscal.feed('0 & 1') AND mscal.baseline('DA*')")mscal.corr('spec')— a comma-list of correlation names (RR/XX/I/ … via the Phase-78_STOKES_NAMES) or integer Stokes codes. A per-rowBool:trueif the row's polarization setup (POLARIZATION.CORR_TYPEviaDATA_DESCRIPTION.POLARIZATION_ID) shares any code with the request.mscal.feed('spec')— themscal.baselineantenna-grammar form onFEED1/FEED2(L & Rfeed-pair,L && R,!, comma-lists of ids /N~Mranges), with numeric feed ids only.- Both in
src/taql/mscal.jl—_parse_corr_types,_mssel_onecorr/feedbranches (feed reuses_mssel_baseline_pred). mscal.corr/mscal.feedare notregisterUDF-registered in the casacore build here (only advertised in the help text), so the real-derivedmscalcross-check skips them cleanly.
mscal.spw('spec') now takes the MSSelection spwid:chanlist form, and a companion mscal.chan('spec') returns the per-row channel mask.
query(main, "mscal.spw('0:5~20')") # spw 0, chans 5-20 non-empty
query(main, "any(mscal.chan('0:100~200MHz'))") # rows with a channel in the band
query(main; select = ["m" => "mscal.chan('0:5~20;40~50')"]) # per-row BitVectorspecis a comma-list of<spwterm>[:<chanlist>].<spwterm>is a single MSSelection-lite term (N/N~M/>N/<N/ a name / glob //regex/againstSPECTRAL_WINDOW.NAME/*).<chanlist>is a;-list ofa(single 0-based index),a~b(inclusive range),a~b^step,f1~f2GHz(aCHAN_FREQrange,Hz/kHz/MHz/GHz), or<f/>f.mscal.spwstays a per-rowBool— a channelled spw matches only if at least one of the row's channels is selected (so0:100~200on a 64-channel spw excludes the row).mscal.chanreturns a per-rowBitVectorof the spw's channel count (all-false for an unselected spw), forany(...)/count(...)/ a Phase-60 masked array.- All in
src/taql/mscal.jl—_parse_spw_spec/_parse_chan_elem/_chan_mask;_mssel_one'sspwbranch gains the channelled path, newchanbranch. Threading unchanged. mscal.spw('0:5~20')row count cross-checked against realderivedmscal.- Non-goals: velocity units on a channel range (casacore disables them too);
^stepon a frequency range; per-DATA_DESC_IDdistinct channel geometry within one spw.
Phase 82 — solar-system ephemeris (MeasComet) tables
A moving-target FIELD row — a non-negative EPHEMERIS_ID with a matching EPHEM<id>_*.tab polynomial position table — now gets its time-dependent direction from that table, closing the Phase-76 plan94-accuracy gap for comets and planets.
e = field_ephemeris(subtable(ms, "FIELD"), 0) # nothing if not a moving target
d = ephemeris_direction(e, 60454.4) # MDirection at that MJD
measure(fld, "PHASE_DIR", 1; epoch = MEpoch{UTC}(60454.4))
query(main, "mscal.hadec1()") # uses the ephemeris automatically- New
src/measures/ephemeris.jl:Ephemerisstruct +open_ephemeris(reads theMJD0/dMJD/NAME/posrefsyskeywords +MJD/RA/DEC/Rho/RadVelcolumns),ephemeris_direction/_radvel/_distance(linear interpolation of the (ρ,RA,Dec) Cartesian vector between bracketing rows — casacore'sMeasComet::get), andfield_ephemeris(fld, field_id)(globsEPHEM<id>_*.tabin the FIELD directory). measure(t, col, row; epoch)— new kwarg; for a direction cell of a FIELD table with an ephemeris it returns the ephemeris position (shifted by the storedPHASE_DIRoffset)._mscal_columns(mscal.*)_fielddir— an ephemeris field is evaluated per(field, TIME)(converted to TDB), then to J2000.write_tablegains akeywords::AbstractDictkwarg for scalar / string-array table-level keywords (used to build ephemeris fixtures).- Verified by an
open_ephemeris/ephemeris_directionround-trip and an end-to-endmscal.hadec1()on a copied MS with a patched moving-target FIELD. Nocasatoolsoracle (needs a real ephemeris table +me.framecomet— documented gap). - Non-goals: polynomial (
numpoly > 0)PHASE_DIRinterpolation; theDiskLong/DiskLatsub-Earth point; the aipsrcmeasures.comet.directorylookup;MeasCometas aMeasFramefor aCOMET-coded direction column outside FIELD.
Phase 103 — mscal.pbresponse per-baseline + elliptical/squint specs
query(main, "mscal.pbresponsebl('gaussian:0.008727') > 0.5") # both antennas tracking
update!(ms; set = ["DATA" =>
"mscal.pbcorrbl(DATA, 'ellipse:0.012:0.008:0.3:squint:0.0005:0.0')"])mscal.pbresponse's beam-spec mini-language gains an"ellipse:HMAJ: HMIN:PA"form (EllipticalGaussianBeam) and an optional trailing":squint:DLON:DLAT"on any spec (wraps the base beam in aSquintBeam) —_pb_response_fn(src/taql/mscal.jl) now returns anoffset::(dlon,dlat) -> powerclosure uniformly; a circular beam (gaussian/airy) ignores the offset direction viapower_response's existing generic 2-D-offset fallback (Phase 100), so the rewrite is a no-op for the Phase 101/102 forms.- The main-loop dispatch now computes the real 2-D tangent-plane offset (
pointing_offset, Phase 100) of the nominal direction from the antenna's actualPOINTING.DIRECTION, instead of the Phase 101 great-circle-magnitude-only_tql_angdist— required for a direction- aware ellipse/squint beam, and value-identical to the old scalar path for a circular beam (same magnitude, direction discarded downstream). - New
mscal.pbresponsebl('spec' [, dir])— the same beam evaluated at both ANTENNA1's and ANTENNA2's own actual pointing (needsANTENNA2) and multiplied — the joint baseline response, for e.g.query(main, "mscal.pbresponsebl(...) > threshold")to cut baselines where either antenna has drifted off source.mscal.pbcorrbl/mscal.pbattenblare the write-path sugar (valexpr / mscal. pbresponsebl(...)/valexpr * mscal.pbresponsebl(...)), mirroring Phase 102'spbcorr/pbatten. - Verified via controlled synthetic POINTING fixtures (the Phase 101/102 pattern — no CASA/casacore oracle exists for a MeasurementSets-only extension): both antennas of one baseline given independently known pointing offsets, the per-baseline response checked against the hand-computed product of the two Gaussian responses; the ellipse/squint results checked against
power_response/SquintBeamcalled directly on an independently recomputedpointing_offset;pbcorrbl∘pbattenblround-trips a known DATA value; a squint exactly onto the source gives response ≈ 1 regardless of the base beam's own off-axis response there. - Non-goals: a
PolynomialBeamspec form; a per-baseline ellipse/squint variant beyond whatmscal.pbresponsebl('ellipse:...')already gives (each antenna's own response, still direction-aware); an Observatories-array-centre (suffix-less) per-baseline response (baseline responses are inherently per-antenna-pair).
Phase 104 — complete the meas.* TaQL UDF subset
query(main, "meas.freq('TOPO', 'LSRK', 1.4e9, TIME/86400, X, Y, Z, RA, DEC) > 1.399e9")
query(main, "meas.doppler('RADIO', 'BETA', 0.01) > 0.009") # no SOFA needed
query(fld, "meas.riseset(RA, DEC, TIME/86400, X, Y, Z)[1] < TIME/86400")meas.freq('SSCALE', 'TSCALE', freq, mjd, x, y, z, ra, dec)/meas.rv('SSCALE', 'TSCALE', v, mjd, x, y, z, ra, dec)— frequency / radial-velocity frame conversion (topo/geo/bary/lsrk/lsrd/galacto/lgroup/cmb), built on the existingMFrequency/MRadialVelocitymeasconvertmachinery (Phases 66/71/88) via a new_meas_full_frame(mjd,x,y,z,ra,dec)helper (epoch + ITRF position + J2000 source direction — the full frame a spectral conversion needs).meas.doppler('SCONV', 'TCONV', value)— Doppler-convention algebra (radio/optical(z)/ratio/beta(true,relativistic)/gamma), a thin wrapper over the Phase 72MDopplermeasconvert— pure arithmetic, the only newmeas.*function that needs noimport SOFA.meas.riseset(ra, dec, mjd, x, y, z[, elev0])→[rise_mjd, set_mjd]— the rise/set UTC MJD of a J2000 direction for the day containingmjd, from the standard hour-angle-at-elevation formula (cos H₀ = (sin elev₀ − sin φ·sin δ) / (cos φ·cos δ), apparent place at local noon) plus a Newton inversion of the sidereal-time relation (_mjd_for_lst, 3 iterations against the real SOFAgst06a— the mean sidereal rate makes LST close enough to linear in UT1 over a day that this converges to sub-second precision).(NaN, NaN)if the source never reacheselev0that day;(⌊mjd⌋, ⌊mjd⌋+1)if circumpolar. New core stub_riseset(src/measures/types.jl, mirrors_lst's stub/ext split) + the real implementation inext/SOFAExt.jl. Not a byte-exact port of casacore's own iterativeRise/Setsearch — an independently-derived, documented approximation (no casacore/CASA oracle for a MeasurementSets-only convenience wrapper)._meas_two_scale_argsfactors the "first two arguments are string literal frame/convention names" validation shared bymeas.freq/meas.rv/meas.doppler.- Verified:
meas.freq/meas.rvcross-checked directly againstmeasconverton an equivalentMeasFrame;meas.doppleragainstmeasconvert(MDoppler{...}, ...);meas.risesetchecked for rise-before-set + positive elevation at the rise/set midpoint (for a source below the horizon at the UTC-day boundary), a tighter elevation cutoff narrowing the window, and the NaN/circumpolar sentinel paths. - This closes the last item in Phase 97's
meas.*non-goals list (meas.riseset,meas.freq/meas.doppler/meas.rv); the column-MEASINFO-driven direction-argument form andmeas.pos/meas.itrfxyz/meas.wgsposition UDFs remain non-goals.
Phase 105 — mscal.riseset(): automatic rise/set for the row's own antenna
query(main, "mscal.riseset1()[1] < TIME/86400") # already risen
groupby(main, "FIELD_ID"; select = ["s" => "gmean(mscal.riseset1(0.2)[2])"])mscal.riseset[1|2]([elev0][, dir])→[rise_mjd, set_mjd]wires the Phase 104meas.riseset/_risesetmachinery into the automatic per-rowmscal.*geometry — ANTENNA1's/ANTENNA2's own ITRF position (1/2suffix; bare = array centre, same convention as every othermscal.*direction function) anddir(defaultFIELD.PHASE_DIR, same optional-direction-argument grammar asmscal.el1('SUN')etc.), for the UTC day containing the row'sTIME.elev0(rad, default 0) is a numeric literal baked into the function's parsed key (likemscal.pbresponse's beam spec), not a per-row expression.- Memoized per
(antenna-or-centre, direction, UTC day)rather than per exactTIME— rise/set only changes once a day, so this is effectively free even over a MAIN table with many integrations per day. - Bug found and fixed during implementation (only visible once a query used two different
elev0values in one call): the memo key didn't includeelev0, somscal.riseset1(0.2)silently returned theelev0=0.0result whenever both were evaluated in the same_mscal_columnscall. Fixed by keying the memo on(antenna, direction, day, elev0). - No casacore/CASA oracle (mscal.* extension over Phase 104's own meas.riseset, itself independently derived). Verified: parser unit tests for the encoded key + dir/elev0 threading; on the sample MS, both rise and set are finite for a real antenna/field/day, the
2suffix gives ANTENNA2's own (slightly different) window, and a tighter elevation cutoff never widens the window (rise2 >= rise,set2 <= setat every sampled row) — this is exactly the assertion that caught the memo-key bug above.
Phase 106 — meas.pos() / meas.itrfxyz() / meas.wgs(): position UDFs
query(ant, "meas.wgs(X, Y, Z)[3] > 2000") # height > 2 km
query(cat, "meas.itrfxyz(LON, LAT, 0.0) == meas.pos('WGS84', 'ITRF', X, Y, Z)")meas.pos('SSCALE', 'TSCALE', x, y, z)—MPositionframe conversion (itrf/wgs84). This closed a real, previously-undiscovered gap:MPositionhad nomeasconvertmethod at all before this phase —measconvert(::MPosition, ...)always hit the generic core stub's "needs SOFA.jl" error, even with SOFA loaded, since no_mconvmethod existed for it. The fix (ext/SOFAExt.jl) is an identity on(x,y,z): casacore stores the same geocentric Cartesian vector under bothITRFandWGS84— the refs only differ in which ellipsoid a geodetic (lon/lat/height) view of that vector uses, so there is nothing to rotate.meas.itrfxyz(lon, lat, height)/meas.wgs(x, y, z)— the real conversion: WGS84 geodetic ↔ geocentric Cartesian ITRF, viaSOFA.gd2gc/SOFA.gc2gd(the same pair_frame_sitealready uses internally for AZELGEO). New core stubs_geodetic_to_itrf/_itrf_to_geodetic(src/measures/types.jl, mirror the_lst/_risesetstub/ext split) + real implementations inext/SOFAExt.jl.- Closes the very last item in Phase 97's
meas.*non-goals list (the column-MEASINFO-driven direction-argument form remains a non-goal — everymeas.*argument is still an explicit expression, not inferred from a column's ownMEASINFO). - Verified:
meas.posagainstmeasconvert(MPosition{ITRF}(...), WGS84)directly (exact — no arithmetic, an identity);meas.wgs∘meas.itrfxyzround-trips a real VLA-antenna ITRF position toatol = 1e-6m.
Phase 107 — fix the UnitfulExt precompile method-overwrite bug
WARNING: Method definition _tql_known_unit(AbstractString) in module
MeasurementSets at src/tables/units.jl:98 overwritten in module
UnitfulExt at ext/UnitfulExt.jl:84.
ERROR: Method overwriting is not permitted during Module precompilation.- Root cause:
_tql_known_unit(s::AbstractString)(core,src/tables/ units.jl) andMS._tql_known_unit(s::AbstractString)(ext/ UnitfulExt.jl) used the identical signature — a genuine redefinition, not an added dispatch. Every other core/extension stub pair in this package (_tql_quantity,_tql_unit_attach,_tql_write_strip,_ms_ustring,_quantity_column_spec,_lst,_riseset,_geodetic_to_itrf,_itrf_to_geodetic, every_mconvmethod, …) gives the core fallback a strictly looser signature (args..., an untyped positional, or an abstract/Uniontype) so the extension's concrete method is a genuine specialization, not an overwrite — Julia forbids the latter during extension precompilation._tql_known_unitwas the one place that pattern was broken. - Fix: drop the
::AbstractStringannotation on the core definition (_tql_known_unit(s) = ...) — one line, matches the convention every other stub in the file already follows (_tql_write_strip(x, u)is the closest sibling: untyped core,::Unitful.AbstractQuantity-typed extension). - Swept every other
MS._*extension method across all four extensions (SOFAExt,EarthOrientationExt,HDF5Ext,UnitfulExt) against its core counterpart — confirmed no other instance of this bug exists. - Verified:
import Unitful, UnitfulAngles, UnitfulAstrothenusing MeasurementSetsprecompiles cleanly (no warning, no error);_tql_known_unitcorrectly defers to the real_ms_uparse-backed check when the extension is loaded (e.g."erg"— not in the core_COMMON_UNITSfallback set — now resolvestrue); the fullunits_tests.jl+taql_query_tests.jlstandalone run is unchanged (760 tests, no count change — a pure precompile-hygiene fix).
Phase 108 — running* / boxed* sliding-window array reductions
query(main, "runningmedian(DATA, 2)[1,1] > 0") # 5-channel median smooth
query(cat, "boxedaverage(SPECTRUM, 4) > threshold") # 4-channel block averagerunning<X>(arr, hwidth)/boxed<X>(arr, bwidth),X∈average(mean)/median/min/max/variance/stddev/sum— array-cell sliding-window smoothing (one MAIN row's own array, reduced along its own axis/axes), the last item on the Phase 25 "not yet in TaQL-lite" list.runningis a centred window ([i−h, i+h]per axis, shrinking at the edges — output the same shape as the input);boxedis non-overlapping bins of sizebwidth(output shapecld(n, b)per axis, a partial trailing bin ifbwidthdoesn't divide evenly). The width argument is a scalar (same on every axis) or an array literal (ndims(arr)elements, one per axis) —runningaverage(V, [1,3])smooths axis 1 with half-width 1 and axis 2 with half-width 3.- Shared generic engine (
src/taql/functions.jl):_tql_window_widths(scalar-or-per-axis-array → anInttuple, arity-checked),_running_reduce/_boxed_reduce(a plainCartesianIndicesloop — no attempt at a separable/incremental-sum fast path; these operate on one row's small array cell, not a bulk column).min/max/sumkeep the input's element type;average/median/variance/stddevpromote toFloat64(matching the plain, non-runningmean/median/ etc. reductions already in the function library). - Masked-array (
TQLMArray) input is a documented non-goal — passarraydata(...)first; a non-array (scalar) first argument raises a clearArgumentErrorrather than silently treating it as a 1-element window. - No casacore/CASA oracle for this phase (self-contained numerical routines) — verified by hand-computed 1-D and 2-D references (edge- window shrinking, partial trailing bins, per-axis widths), agreement with a direct
Statistics.var/std/mediancall on the same explicit window, and a query-string round-trip against the same functions called directly on each row's array.
Phase 109 — mscal.stokes() pseudo output types
query(main, "mscal.stokes(DATA, 'Ptotal')[1,1] > threshold") # total polarized intensity
query(main, "mscal.stokes(DATA, 'I,Ptotal')") # physical + pseudo, one callmscal.stokes(col, 'types')'stypesnow also accepts casacore's derived pseudo output types:Ptotal = √(Q²+U²+V²),Plinear = √(Q²+U²),Pangle = ½·atan2(U,Q)(rad),PFtotal/PFlinear(the same totals divided byI) — non-linear combinations of Stokes I/Q/U/V, unlike every othermscal.stokesoutput (a plain matrix multiply against the correlation cell).- Implementation (
src/taql/mscal.jl): pseudo types are encoded as negative internal codes (_STOKES_PSEUDO_CODES, never collide with a real 1-20 correlation code, thread through the existing_stokes_key/_stokes_setupssentinel machinery unchanged).StokesSetupgains anouttypesfield (to tell pseudo rows apart) and aniquvmat(the input frame's own I,Q,U,V conversion matrix, built once, lazily — only when a pseudo type is actually requested)._stokes_convert'sComplexmethod computes the ordinary linear rows as before, then — only if any output is a pseudo type — computes I,Q,U,V per channel once and derives each pseudo row's value from it; a query mixing physical and pseudo types in onemscal.stokes(DATA, 'I,Ptotal')call shares that single I,Q,U,V pass. Bool(FLAG) / real (WEIGHT) input with a pseudo type requested raises a clearArgumentError— the pseudo formulas are only meaningful for a complex (DATA-like) cell.- Closes the pseudo-output-type non-goal from Phase 78.
- No casacore/CASA oracle for the pseudo-type formulas themselves (matches the documented
Stokes::StokesTypesdefinitions) — verified by injecting a knownDATAcell (RR=3+1i, RL=0.5-0.2i, LR=0.5+0.2i, LL=2-1i→I=5, Q=1, U=-0.4, V=1) viaedit()and checking every pseudo type's value against the formula computed directly from the same I/Q/U/V; theI==0edge case (fractional forms → 0, not NaN/Inf) checked directly onStokesSetup.
Phase 110 — column-MEASINFO-driven meas.<frame>() direction argument
query(fld, "meas.azel('PHASE_DIR', TIME/86400, X, Y, Z)[2] > 0") # was: ..., 'J2000', PHASE_DIR[1], PHASE_DIR[2], ...meas.<frame>(['SRC',] lon, lat[, mjd[, x, y, z]])gains a second form:meas.<frame>('COLNAME', mjd[, x, y, z])— instead of a literal'SRC'frame name plus two explicitlon/latexpressions, a single string names a direction column, and the source frame is read from that column's ownMEASINFO(measinfo(t, colname), fixedRefonly — a per-rowVarRefColcolumn raises a clear error pointing atmeasure(t, col, row)instead). Closes the one remainingmeas.*non-goal from Phase 97.- Disambiguated from the existing numeric form at parse time, not by argument count: the first string literal is a column name iff it is not a recognized frame name (
_MEAS_DIR_FRAMES— j2000/b1950/ app/galactic/gal/ecliptic/ecl/azel/hadec/itrf/icrs) — someas.b1950('J2000', RA, DEC)(existing) andmeas.b1950('PHASE_DIR', TIME/86400)(new) both parse to the right form with no new syntax. - New AST node
TQLMeasColDir(src/taql/functions.jl) threads through the query engine the same waymscal.*/mscal.stokesdo: a"::measframe::COLNAME"sentinel resolved once per table (not per row) by a new_measframe_split/_measframe_colspair, wired into both_tql_cols(query/groupby/join) and_vtq_prepare!(VirtualTaQLColumn— also fixed a stale docstring there that incorrectly claimed measures functions weren't supported in CALC expressions at all). - No casacore/CASA oracle (a MeasurementSets-only convenience over the existing
measconvertmachinery) — verified againstmeasconvertcalled directly on the same column value, and against the equivalent explicit-'SRC'-plus-lon,latnumeric form on the same data; error paths (noMEASINFO, a non-directionMEASINFO, aVarRefColcolumn) each checked.
Phase 111 — UPDATE/DELETE ORDER BY + LIMIT
update!(t; set=["FLAG" => "true"], where="SNR < 3", orderby=["TIME"], limit=100) # the 100 oldest
delete!(t; where="SCAN_NUMBER == 5", orderby=["TIME" => :desc], limit=20) # the 20 newest
taql(t, "DELETE FROM t WHERE A > 3 ORDER BY TIME DESC LIMIT 2")update!/delete!gainorderby/limitkwargs — TaQL's "update/ delete the N oldest/newest rows matching a condition" form.orderbyis the same shape asquery's do-block form (a bare column name/Symbol, ascending, orname => :asc/name => :desc); the matched rows are sorted by it (reusing the existingTQLOrderKey/_apply_orderbymachinery fromORDER BY), thenlimit(anInteger) keeps only the firstlimitof them — or, forlimit < 0, the last|limit|— before the mutation runs.taql()'sUPDATE/DELETEstring forms parse a trailingORDER BY k [ASC|DESC], … [LIMIT n]clause (new_taql_orderby_listhelper) and pass it through toupdate!/delete!.update!'s whole-column fast path (t[c][:] = [...], used whenwhere === nothing) is now also gated onorderby/limitbeing unset — aorderby/limit-restricted update always goes through the per-matched-row path, even with nowhere.- Closes the Phase 30 non-goal ("
DELETE/UPDATEORDER BY+LIMIT… chainquerythendelete!by the selected condition instead"). - Correction, found only once the full suite ran against real Casacore.jl: real TaQL's own
UPDATE/DELETEORDER BY ... LIMIT ndoes not sort the matched rows by the given key beforeLIMITtruncates them — a live spike showedUPDATE $1 SET A=A+100 WHERE A>3 ORDER BY T LIMIT 3gives byte-identical results to the same command withORDER BY Tdeleted entirely;T's actual values play no role. (LIMIT's own row-selection turned out direction/sign-dependent in a way not worth fully reverse-engineering for this phase.) This package'sorderby/limitare a deliberate MeasurementSets extension implementing the genuinely useful "N oldest/newest rows" semantics the original non-goal text described — a real sort-then- limit — not a port of real TaQL's own behaviour; documented plainly in theupdate!/delete!docstrings. The planned real-TaQL cross-check test was replaced with the hand-computed-selection tests (already present) plus a comment recording the live-spike finding, in keeping with this project's established pattern for a documented MeasurementSets-only semantic choice. - Verified via hand-computed row selections (ascending/descending, positive/negative limit, the Julia and
taqlstring forms agreeing).
Phase 112 — Hypercolumn_* keyword preservation on copy
copytable(dst, readtable(src)) # src's Hypercolumn_* private keywords now survivecopytable/copyms/write_ms(theTablesource path) now preserve a source table'sHypercolumn_<name>private-keyword declarations (casacoreTableDesc::defineHypercolumn) — closing the fidelity gap documented since Phase 11.- Found and fixed a real bug, not just a missing feature:
_copy_table_colswas unconditionally passingprivate=Record()to_write_table_core, discarding the entire table-level private keyword set on every copy — even though the outer_copy_tablemethods already correctly defaultedprivateto the source's own (t.desc.private) and threaded it all the way down as a parameter that was then silently dropped at the very last call site. - New
_filter_hypercolumns(private, keptnames)(src/tables/create.jl): passes every non-Hypercolumn_*private keyword through unconditionally, and preserves aHypercolumn_<name>entry only when every column it names (HCdatanames/HCcoordnames/HCidnames) is still present, under the same name, in the copy's actual output column set — a renamed or dropped column silently drops just that one stale declaration rather than writing a reference to a column that no longer exists. - Our own reader never needed this keyword at all (Phase 11 — a hypercube's layout comes entirely from the storage manager's own on-disk header); this only matters for an external tool that inspects the
TableDescdirectly (e.g.tb.getdminfo()). - A
RefTable/ConcatTablesource still drops the private keyword set (_copy_table'sRefTable/ConcatTablemethods keep their own deliberateprivate=Record()default — a selection/projection may rename or drop the very columns a declaration names) — only the plain-Tablesource path (copyms/write_ms's common case) changed. - Verified: a hand-built
Hypercolumn_TestCubekeyword (via_write_table_core'sprivate=kwarg directly) survives a fullcopytableround-trip intact;_filter_hypercolumnsunit-tested for both the "all referenced columns kept" and "one is missing" cases, plus an unrelated private key passing through either way. No casacore/CASA oracle needed (pure keyword passthrough, not a new binary format).
Phase 113 — taql(): INSERT INTO t SELECT ... FROM 'path'
taql(t, "INSERT INTO t SELECT * FROM 'src.ms/POINTING'")
taql(t, "INSERT INTO t SELECT A AS X, B FROM 'src' WHERE A > 7")taql()'sINSERTstring form gains the row-copying variant:INSERT INTO t SELECT col [AS a], … FROM 'path' [WHERE cond] [LIMIT n]—*selects every source column as-is, an explicit list may rename a source column to matcht's own name (A AS X),WHEREis an ordinary TaQL-lite condition over the source table, andLIMITreusesinsert!'s existing cycling/truncating semantics. Implemented as a thin wrapper: parses the clause, builds aqueryof the source table, and callsinsert!(target; values=result, limit)— no new mutation machinery. Closes the Phase 31 non-goal.- Unlike Phase 111's
UPDATE/DELETEORDER BY/LIMITsurprise, this form was live-verified against real Casacore.jl first (INSERT INTO $1 SELECT A, B FROM 'src' WHERE A > 7,SELECT *, and anASrename all spiked before writing the test) and matches our own implementation's semantics exactly — a genuine real-TaQL cross-check testset was added, not just hand-computed references.
Phase 114 — array-cell values in taql INSERT VALUES string form
taql(t, "INSERT INTO t (A, V) VALUES (9, [[1.0,2.0],[3.0,4.0]])")taql()'sINSERT INTO t VALUES (...)string form now accepts an array-cell value as a bracketed literal — flat ([1.0, 2.0, 3.0], already worked onceTQLArrayLitstopped beingIN-only) or nested ([[1.0,2.0],[3.0,4.0]], new) — instead of requiring the Juliainsert!(t; values=["V" => matrix])form for an array-shaped column. New_taql_const-internal_nest_to_array: a rectangular nesting of vectors becomes a real multi-dimensionalArray(stack); a ragged one is left as nestedVectors (which then fails, clearly, when written to an array-shaped column).- Element order matches real casacore TaQL's own nested-array-literal convention, confirmed by a live cross-check: the nesting is reshaped column-major — each inner vector becomes one column of the result, not one row (
[[1,2],[3,4]]→[1 3; 2 4], not[1 2; 3 4]) —stack's own default axis order happens to match casacore's exactly, so no transpose/reshape juggling was needed once this was spiked against real TaQL (INSERT INTO $1 (A, V) VALUES (9.0, [[1.0,2.0],[3.0,4.0]])on a real casacore table, then compared cell-for-cell against our own reader's output for the same insert). Closes the remaining half of the Phase 31 non-goal (the row-copyingINSERT ... SELECT ... FROMhalf closed in Phase 113).
Phase 115 — mscal.baseline() ;-separated multi-term specs
query(main, "mscal.baseline('DA01&DV01;DA02&DV02')")- Investigated the standing Phase 80 non-goal ("baseline regex lists,
blregexlist") against real casacore and found the plan's own characterization did not hold:mscal.baseline('[0,1]&2')and'{0,1}&2'are both flatly rejected by realtableCommand("mismatched [ and ]" / parse error near{) — no bracketed antenna-name-list syntax exists in this casacore build. A first implementation attempt (splitting the whole spec on every top-level comma into separate OR'd pair-terms, with[...]grouping antenna names within one term) was built, then discarded before being committed once live cross-checking showed real casacore does the opposite: a plain comma extends one antenna-set list — even across&— rather than separating whole pair-terms ('DA01,DA02&DV01'is one pair-term with a 2-antenna LHS, not two terms; this already worked with zero code changes, since the pre-Phase-115 code only ever split on the first&, leaving every comma inside each side to_mssel_idset's own union logic). - The genuine gap, found instead:
;is casacore's real multiple-baseline-pair-term separator ('DA01&DV01;DA02&DV02', OR'd) — confirmed live across 7+ combinations (single/multi-antenna sides, duplicate terms,&&/&&&terms, 2–3 terms), every one matching a plain OR of each term's own match set exactly. !negation combined with;is deliberately NOT implemented: 4 further live probes ('!A&B;C&D','A&B;!C&D', and their variants) showed the second half of a;-list is silently dropped whichever term carries the!— not a reproducible boolean combination, almost certainly a real casacore parser limitation rather than a defined feature. Replicating a bug isn't useful, so combining!with;raises a clearArgumentErrorinstead of a silent wrong answer; a;-free leading!(already supported before this phase) is unaffected._mssel_baseline_predsplit into_mssel_baseline_term_pred(one&-pair term, unchanged logic) + a thin wrapper doing the;-split!-combination guard + OR.
- Real-TaQL cross-check testset (5 multi-term specs, all matching); docs updated (the Phase 80 comment block,
_mssel_baseline_pred's own docstring-comment recording the specific probes).
Phase 116 — RetypedArrayEngine feasibility investigation
- Investigated the one remaining unsupported data manager's Phase 40 hand-wave ("needs the C++ source-type class") by reading
casacore/ tables/DataMan/RetypedArrayEngine.{h,tcc}end to end. Confirmed genuinely infeasible, not just under-scoped:SinRetypedArrayEngine<S,T>is a C++ class template parameter — itsdataTypeId()/set()/get()conversion functions and binary layout are arbitrary code compiled into whatever third-party program created the table, with no fixed wire format to target; the on-disk DM type string itself (className()) is built fromS::dataTypeId(), andregisterClass()must be called explicitly per instantiation by that program (casacore's ownlibcasa_tablesnever auto-registers any, unlike Dysco / BitFlags / ForwardColumn).grep -rl RetypedArrayEngineacross the entire casacore source tree turns up zero real callers anywhere in casacore itself — including every Measurement Set-related file — only its own demo/test code (DataMan/test/dRetypedArrayEngine.{cc,h}). - Outcome: still unsupported, as before — the original assessment was correct. The only change is a clearer, investigation-backed error message + header comment in
src/datamanagers/forwardcol.jl's_UnsupportedDM(cites the specific finding instead of the terser original text), so a future reader hitting it understands it's a structural dead end rather than a missing feature to file. No behavior/test change (the existingengine_tests.jldispatch test —_dmtype("RetypedArrayEngine<Float>") === _UnsupportedDM— already covers the unchanged code path).
Phase 117 — primary-beam width-parameter validation
GaussianBeam(-1.0, 1.4e9) # ArgumentError: hpbw must be finite positive
AiryBeam(25.0; blockage = 25.0) # ArgumentError: 0 <= blockage < diameter
EllipticalGaussianBeam(0.005, 0.02, 0, 1.4e9) # ArgumentError: hpbw_major >= hpbw_minor- Every
PrimaryBeamconstructor (GaussianBeam,AiryBeam,PolynomialBeam,EllipticalGaussianBeam,SquintBeam) and everypower_responsemethod'sfreq/offset argument now validates its inputs — a non-positive/non-finite width, diameter, or frequency, ablockageoutside[0, diameter)(blockage == diameteris a0/0singularity in the annular-Airy formula,> diameteris unphysical),hpbw_major < hpbw_minor(silently unenforced before this phase despite the docstring's≥claim), or a NaN/Inf offset/coefficient now raises a clearArgumentErrorimmediately instead of silently propagating to a NaN/Inf power response several calls downstream. - New shared helpers
_pb_finite/_pb_positive/_pb_check_freq/_pb_check_offset(src/beam/beam.jl); every constructor gained (or kept, forPolynomialBeam's pre-existing default one) an inner constructor doing the check-then-convert;power_responsemethods checkfreqand theθ/(dlon,dlat)offset at entry.SquintBeam's ownpower_responsedelegates itsfreqcheck to the wrapped base beam (no duplicate check) but validates its own offset before subtracting the squint. - A useful side effect:
mscal.pbresponse('ellipse:HMIN:HMAJ:PA')(a transposed hmaj/hmin typo in a spec string) now raises a clear error at parse time instead of silently computing a rotated-wrong beam. - 41 new tests (
test/beam_tests.jl, "Phase 117 parameter validation"); full existing beam +mscal.pbresponse/pbcorr/pbattentest suites (104 + 500 tests) pass unchanged — no valid existing usage anywhere in the codebase violated any of the new invariants.
Phase 118 — mscal.baseline() antenna diameter/mount selection investigation
- Investigated whether real casacore's baseline-selection surface has any selection-by-physical-property (
DISH_DIAMETER,MOUNT) syntax beyond name/id/glob/regex. Tracedmscal.baseline(spec)(derivedmscal/DerivedMC/UDFMSCal.cc:445-465, theBASELINEcase ofUDFMSCal::getDataNode) to confirm it is a direct pass-through to real casacore's ownMSAntennaGram/MSAntennaParse(msAntennaGramParseCommand) — not a separate mini-grammar, so its full syntax surface is exactly whatever the real MSSelection antenna grammar supports.grep -rin "diameter|mount"across every file inms/MSSel/(all grammar.yy/.llfiles, every*Parse.cc) and a direct read ofMSAntennaIndex.h's public interface (matchAntennaName/matchAntennaRegexOrPattern/matchStationName/matchAntennaNameAndStation/matchId— id, name, and station only) both confirm zero diameter/mount selection anywhere in casacore's own antenna-selection machinery. Confirmed absent, not missing — matches the earlier (correct) assumption; no code change needed. Achievable today anyway via a plainWHERE/joinonANTENNA.DISH_DIAMETER/ANTENNA.MOUNT(already fully general), just not throughmscal.baseline's own spec-string syntax (which real casacore doesn't have either). - Incidental discovery, flagged for a future phase, not implemented here (out of this phase's chosen scope): reading
MSAntennaGram.yy/.llend to end while investigating turned up the real form of the Phase 80 non-goal "blregexlist" — Phase 115 investigated and discarded a[name1,name2]-bracket-list guess (real casacore rejects it). The actual grammar production isblregexlist: BLREGEX (COMMA BLREGEX)*, where aBLREGEXtoken is a/…/-delimited regex whose body contains a literal&(the lexer's own discriminator,MSAntennaGram.ll:76-86: a/…/regex containing&becomesBLREGEXinstead of a plain per-nameREGEX) — matched viaMSAntennaParse::selectBLRegexagainst the whole"name1&name2"baseline string, not against each antenna name separately. Separately,MSAntennaGram.yy:150-163showsgbaseline: NOT baseline | baselineandindexcombexpr: gbaseline | indexcombexpr SEMICOLON gbaseline— each;-joined term can syntactically carry its own independentNOT, which suggests Phase 115's live-probed "!combined with;silently drops the other term" finding may have a real, traceable explanation in howMSAntennaParseaccumulates results across;-joined terms (rather than being a bug) — worth revisiting with this grammar-level context before either implementing realBLREGEXsupport or reconsidering the Phase 115!+;refusal.
Phase 119 — mscal.baseline() real BLREGEX support
query(main, "mscal.baseline('/DA01&DV.*/')") # DA01 as ANTENNA1, any DV* as ANTENNA2
query(main, "mscal.baseline('/^DA01&DV01/')") # NOT that exact ordered pair- Implements the real "blregexlist" mechanism found while investigating Phase 118, closing the Phase 80 non-goal correctly this time (Phase 115's
[name1,name2]-bracket guess was a different, real-casacore- rejected idea). Confirmed by readingMSAntennaGram.yy/.ll+MSAntennaParse::selectBLRegexin full: a spec element is a/…/regex whose body contains a literal&— the real lexer's own discriminator between a per-nameREGEXand aBLREGEX— FULL- matched against the whole"name_i&name_j"string for every ordered pair of antenna indices (self-pairs included); a literal leading^inside the slashes negates just that one pattern (NOT the regex anchor — casacore repurposes the character); several comma-separated patterns OR their match sets; the outer!this package already supports negates the whole list's result, not the first element. - Every one of the above was live-verified against real Casacore.jl before writing any code or tests (this session's established discipline, since a bracket-list guess was wrong once already): 10 representative specs run through both
mscal.baselineand realtableCommand, all row counts matching exactly — including the subtle "negated pattern OR'd with a plain one still unions, doesn't intersect" case and the "outer!distributes over the whole list, not just the first pattern" case (!/A&B/,/C&D/=NOT(A∪C&D), notNOT(A)∪C&D). - New
_mssel_is_regex_elem/_mssel_is_blregexlist(the detection — checked before the existing&&&/&&/&counting logic in_mssel_baseline_term_pred, since a BLREGEX pattern's literal&would otherwise misfire that counting) and_mssel_blregex_pred(the match-matrix builder, a direct port ofselectBLRegex's nested loop — trivial cost,O(n_antennas²))._mssel_baseline_pred/_mssel_baseline_term_predgain an optionalnames::Vector{String}kwarg, threaded frommscal.baseline's own call site (which already readsANTENNA.NAME);mscal.feed(no name table — feed ids are bare integers) passesnothing, so a BLREGEX-shaped feed spec now raises a clear error instead of silently misparsing the pattern's&as an ordinary L&R split. - Composes cleanly with the Phase 115
;-multi-term machinery (no interaction with that phase's!+;refusal, which is about a different ambiguity —;-joined wholebaselineterms, not one blregexlist's own internal comma list). - 26 new tests (
test/taql_mscal_tests.jl, "mscal.baseline() regex pair lists (BLREGEX)"), including the real-TaQL cross-check above; full existing mscal suite (500 tests) unchanged.