diff --git a/.github/workflows/run-docs-code.yaml b/.github/workflows/run-docs-code.yaml index 841b403..9875cf3 100644 --- a/.github/workflows/run-docs-code.yaml +++ b/.github/workflows/run-docs-code.yaml @@ -41,6 +41,17 @@ jobs: pip install papermill ipykernel conda list + - name: Install Poetry + run: | + pipx install poetry + poetry --version + - name: Install ska-ost-array + run: | + git clone https://gitlab.com/ska-telescope/ost/ska-ost-array-config.git + cd ska-ost-array-config + poetry install + cd .. + - name: Install ipykernel run: python -m ipykernel install --user --name sense --display-name "sense" diff --git a/.github/workflows/testsuite.yaml b/.github/workflows/testsuite.yaml index 102e0b9..ea1204c 100644 --- a/.github/workflows/testsuite.yaml +++ b/.github/workflows/testsuite.yaml @@ -40,6 +40,16 @@ jobs: run: | pip install .[test] conda list + - name: Install Poetry + run: | + pipx install poetry + poetry --version + - name: Install ska-ost-array + run: | + git clone https://gitlab.com/ska-telescope/ost/ska-ost-array-config.git + cd ska-ost-array-config + poetry install + cd .. - name: Run Tests run: | diff --git a/docs/tutorials/using_builtin_observatories.ipynb b/docs/tutorials/using_builtin_observatories.ipynb index 92178d6..ef203a2 100644 --- a/docs/tutorials/using_builtin_observatories.ipynb +++ b/docs/tutorials/using_builtin_observatories.ipynb @@ -146,6 +146,47 @@ "print(obs.frequency)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also create an observatory from a specific SKA configuration provided by `ska-ost-array-config` package (see https://gitlab.com/ska-telescope/ost/ska-ost-array-config for installation instructions) using the `from_ska` method:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "obs = Observatory.from_ska(\n", + " subarray_type=\"AA*\", array_type=\"low\", Trcv=100.0 * un.K, frequency=75.0 * un.MHz\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can create a custom array by passing additional parameters to the array class (see `ska-ost-array-config` documentation). For example, to select all core stations, add 6 stations in the E1 cluster, and remove two stations from the core array:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "custom_obs = Observatory.from_ska(\n", + " subarray_type=\"custom\",\n", + " array_type=\"low\",\n", + " Trcv=100.0 * un.K,\n", + " frequency=75.0 * un.MHz,\n", + " custom_stations=\"C*, E1-*\",\n", + " exclude_stations=\"C1,C2\",\n", + ")" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/src/py21cmsense/observatory.py b/src/py21cmsense/observatory.py index 33b6a99..f75603c 100644 --- a/src/py21cmsense/observatory.py +++ b/src/py21cmsense/observatory.py @@ -57,7 +57,7 @@ class Observatory: Note that longitude is not required, as we assume an isotropic sky. Trcv Receiver temperature, either a temperature Quantity, or a callable that - taakes a single frequency Quantity and returns a temperature Quantity. + takes a single frequency Quantity and returns a temperature Quantity. min_antpos, max_antpos The minimum/maximum radial distance to include antennas (from the origin of the array). Assumed to be in units of meters if no units are supplied. @@ -204,7 +204,8 @@ def from_profile(cls, profile: str, frequency: tp.Frequency | None = None, **kwa ---------- profile A string label identifying the observatory. Available built-in observatories - can be obtained with :func:`get_builtin_profiles`. + can be obtained with :func:`get_builtin_profiles`. For more up-to-date SKA profiles, + check the :func:`from_ska` method. frequency The frequency at which to specify the observatory. @@ -222,6 +223,56 @@ def from_profile(cls, profile: str, frequency: tp.Frequency | None = None, **kwa obj = cls.from_yaml(fl, frequency=frequency) return obj.clone(**kwargs) + @classmethod + def from_ska( + cls, + subarray_type: str, + array_type: str = "low", + Trcv: tp.Temperature | Callable = 100 * un.K, # noqa N803 + frequency: tp.Frequency | None = 150.0 * un.MHz, + **kwargs, + ) -> Observatory: + """Instantiate an SKA Observatory. + + Parameters + ---------- + subarray_type + The type of subarray to use. Options are "AA4", "AA*", "AA1", "AA2", "AA0.5", + and "custom" + array_type, optional + The type of array to use. Options are "low" and "mid". + Default is "low". + Trcv, optional + Receiver temperature, either a temperature Quantity, or a callable that + takes a single frequency Quantity and returns a temperature Quantity. + Default is 100 K. + frequency, optional + The frequency at which to specify the observatory. Default is 150 MHz. + + Other Parameters + ---------------- + All other parameters passed will be passed into the LowSubArray or MidSubArray class. + See the documentation of the ska-ost-array-config package for more information. + """ + try: + from ska_ost_array_config.array_config import LowSubArray, MidSubArray + except ImportError as exception: # pragma: no cover + raise ImportError( + "ska-ost-array-config package is required, " + + "see https://gitlab.com/ska-telescope/ost/ska-ost-array-config" + ) from exception + + if array_type == "low": + subarray = LowSubArray(subarray_type, **kwargs) + elif array_type == "mid": + subarray = MidSubArray(subarray_type, **kwargs) + else: + raise ValueError("array_type must be 'low' or 'mid'.") + antpos = subarray.array_config.xyz.data * un.m + _beam = beam.GaussianBeam(frequency=frequency, dish_size=35.0 * un.m) + lat = subarray.array_config.location.lat.rad * un.rad + return cls(antpos=antpos, beam=_beam, latitude=lat, Trcv=Trcv) + @cached_property def baselines_metres(self) -> tp.Meters: """Raw baseline distances in metres for every pair of antennas. @@ -473,7 +524,7 @@ def grid_baselines( -------- grid_baselines_coherent : Coherent sum over baseline groups of the output of this method. - grid_basleine_incoherent : + grid_baseline_incoherent : Incoherent sum over baseline groups of the output of this method. """ if baselines is not None: diff --git a/tests/test_observatory.py b/tests/test_observatory.py index 59ef827..a4850a3 100644 --- a/tests/test_observatory.py +++ b/tests/test_observatory.py @@ -7,7 +7,8 @@ import pytest import pyuvdata from astropy import units -from astropy.coordinates import EarthLocation +from astropy.coordinates import EarthLocation, SkyCoord +from astropy.time import Time from py21cmsense import Observatory from py21cmsense.baseline_filters import BaselineRange @@ -205,6 +206,60 @@ def test_from_yaml(bm): Observatory.from_yaml(3) +def test_from_ska(): + pytest.importorskip("ska_ost_array_config") + + from ska_ost_array_config import UVW + from ska_ost_array_config.array_config import LowSubArray + from ska_ost_array_config.simulation_utils import simulate_observation + + obs = Observatory.from_ska(subarray_type="AA*", array_type="low", frequency=300.0 * units.MHz) + low_aastar = LowSubArray(subarray_type="AA*") + assert obs.antpos.shape == low_aastar.array_config.xyz.data.shape + Observatory.from_ska(subarray_type="AA*", array_type="mid", frequency=300.0 * units.MHz) + obs = Observatory.from_ska(subarray_type="AA4", array_type="low", frequency=300.0 * units.MHz) + low_aa4 = LowSubArray(subarray_type="AA4") + assert obs.antpos.shape == low_aa4.array_config.xyz.data.shape + obs = Observatory.from_ska( + subarray_type="custom", + array_type="low", + Trcv=100.0 * units.K, + frequency=150.0 * units.MHz, + custom_stations="C*,E1-*", + exclude_stations="C1,C2", + ) + low_custom = LowSubArray( + subarray_type="custom", custom_stations="C*,E1-*", exclude_stations="C1,C2" + ) # selects all core stations, 6 stations in the E1 cluster, excludes core stations C1 and C2 + assert obs.antpos.shape == low_custom.array_config.xyz.data.shape + + # Simulate visibilities and retreive the UVW values + ref_time = Time.now() + zenith = SkyCoord( + alt=90 * units.deg, + az=0 * units.deg, + frame="altaz", + obstime=ref_time, + location=low_custom.array_config.location, + ).icrs + vis = simulate_observation( + array_config=low_custom.array_config, + phase_centre=zenith, + start_time=ref_time, + ref_freq=50e6, # Dummy value. We are after uvw values in [m] + chan_width=1e3, # Dummy value. We are after uvw values in [m] + n_chan=1, + ) + uvw = UVW.UVW(vis, ignore_autocorr=False) + uvw_m = uvw.uvdist_m + assert np.allclose(obs.longest_baseline / obs.metres_to_wavelengths, uvw_m.max() * units.m) + + with pytest.raises(ValueError, match="array_type must be"): + Observatory.from_ska( + subarray_type="AA*", array_type="non-existent", frequency=300.0 * units.MHz + ) + + def test_get_redundant_baselines(bm): a = Observatory(antpos=np.array([[0, 0, 0], [1, 0, 0], [2, 0, 0]]) * units.m, beam=bm)