Visibility data weights
=====================

Pyralysis distinguishes **MS data weights** (``WEIGHT``, ``WEIGHT_SPECTRUM``,
``SIGMA``, ``SIGMA_SPECTRUM``) from **imaging weights** handled by
:mod:`pyralysis.transformers.weighting_schemes` (``Natural``, ``Uniform``,
``Robust``), which populate ``IMAGING_WEIGHT_SPECTRUM`` for gridding.

CASA convention (see `CASA data weights <https://casadocs.readthedocs.io/en/v6.7.2/notebooks/data_weights.html>`_):

.. math::

   \sigma_{ij}(\nu) = \mathrm{noise\ per\ visibility},\quad
   w_{ij}(\nu) = 1 / \sigma_{ij}(\nu)^2

:class:`~pyralysis.optimization.terms.ChiSquared` reads ``IMAGING_WEIGHT_SPECTRUM``.
After thermal noise injection, data and imaging weights should both reflect the
radiometer :math:`\sigma` so :math:`\chi^2` is consistent with the simulated noise.
For the imaging-weighting schemes themselves, see :doc:`gridding`.

Thermal noise injection
-----------------------

:class:`~pyralysis.injectors.ThermalNoiseInjector` writes weight columns by default
(``update_weights=True``) using the same :math:`\sigma` as the noise draw:

.. code-block:: python

   from pyralysis.injectors import ThermalNoiseInjector

   thermal = ThermalNoiseInjector(
       system_temperature=50.0,
       integration_time=10.0,
       channel_bandwidth=1e6,
       update_weights=True,
       sync_imaging_weights=True,
   )
   dataset = thermal.apply(dataset)

Set ``update_weights=False`` only for deliberate experiments, such as testing how a
misweighted objective behaves. Multiplicative noise injectors (gain, bandpass,
phase, antenna gain) do **not** modify MS weights.

Analytic estimator
------------------

:class:`~pyralysis.visibility_weights.AnalyticWeightEstimator` computes weights from
the radiometer / SEFD model without modifying ``DATA``:

.. code-block:: python

   from pyralysis.visibility_weights import AnalyticWeightEstimator

   estimator = AnalyticWeightEstimator(
       system_temperature=50.0,
       integration_time=10.0,
       channel_bandwidth=1e6,
   )
   dataset = estimator.apply_dataset(dataset)

Use this to precompute theoretical weights before noise injection or to refresh
weights after loading a measurement set.

Empirical estimator
-------------------

:class:`~pyralysis.visibility_weights.EmpiricalWeightEstimator` estimates weights
from residual scatter grouped by baseline (``statwt``-inspired):

.. code-block:: python

   from pyralysis.visibility_weights import EmpiricalWeightEstimator

   empirical = EmpiricalWeightEstimator(use_model=True, minsamp=2)
   dataset = empirical.apply_dataset(dataset)

When ``MODEL_DATA`` is present and ``use_model=True``, residuals are
``DATA - MODEL_DATA``. Otherwise, Pyralysis subtracts a per-baseline mean before
estimating scatter. Empirical weights reflect all noise in ``DATA`` (thermal plus
uncorrected calibration errors); analytic weights track thermal noise only.

Recommended simulation order
------------------------------

1. Multiplicative injectors (gain, bandpass, phase) — see :doc:`simulation`
2. :class:`~pyralysis.injectors.ThermalNoiseInjector` (last among injectors)
3. Optional :class:`~pyralysis.visibility_weights.EmpiricalWeightEstimator` for
   data-driven comparison
4. Imaging weighting schemes if not using natural (data) weights
