From 2872b97c6d92592d50fb1794a08e5dc74eff171c Mon Sep 17 00:00:00 2001 From: Willow Smith Date: Wed, 13 Nov 2024 11:40:01 -0500 Subject: [PATCH 1/4] Lunar Array Compatibility Enable moon specific code with "world" keyword --- py21cmsense/_utils.py | 39 +++++++++---- py21cmsense/data/farview/P_Tb_ultimate.npy | Bin 0 -> 320 bytes py21cmsense/data/farview/kmag_ultimate.npy | Bin 0 -> 320 bytes py21cmsense/observation.py | 6 +- py21cmsense/observatory.py | 8 ++- py21cmsense/sensitivity.py | 4 +- py21cmsense/theory.py | 61 +++++++++++++++++++++ 7 files changed, 103 insertions(+), 15 deletions(-) create mode 100644 py21cmsense/data/farview/P_Tb_ultimate.npy create mode 100644 py21cmsense/data/farview/kmag_ultimate.npy diff --git a/py21cmsense/_utils.py b/py21cmsense/_utils.py index 6c44f15..c1b0b47 100644 --- a/py21cmsense/_utils.py +++ b/py21cmsense/_utils.py @@ -3,6 +3,9 @@ from astropy import units as un from astropy.coordinates import EarthLocation, SkyCoord from astropy.time import Time +from lunarsky import MoonLocation +from lunarsky import SkyCoord as LunarSkyCoord +from lunarsky import Time as LTime from pyuvdata import utils as uvutils from . import config @@ -34,7 +37,7 @@ def find_nearest(array, value): @un.quantity_input def phase_past_zenith( - time_past_zenith: un.day, bls_enu: np.ndarray, latitude, use_apparent: bool = True + time_past_zenith: un.day, bls_enu: np.ndarray, latitude, world, use_apparent: bool = True ): """Compute UVWs phased to a point rotated from zenith by a certain amount of time. @@ -51,6 +54,8 @@ def phase_past_zenith( The UVWs when phased to zenith. latitude The latitude of the center of the array, in radians. + world + Whether the telescope is on the Earth or Moon. Returns ------- @@ -59,20 +64,34 @@ def phase_past_zenith( """ # Generate ra/dec of zenith at time in the phase_frame coordinate system # to use for phasing - telescope_location = EarthLocation.from_geodetic(lon=0, lat=latitude) + if world == 'earth': + telescope_location = EarthLocation.from_geodetic(lon=0, lat=latitude) + else: + telescope_location = MoonLocation.from_selenodetic(lon=0, lat=latitude) # JD is arbitrary jd = 2454600 - zenith_coord = SkyCoord( - alt=90 * un.deg, - az=0 * un.deg, - obstime=Time(jd, format="jd"), - frame="altaz", - location=telescope_location, - ) - zenith_coord = zenith_coord.transform_to("icrs") + if world == 'earth': + zenith_coord = SkyCoord( + alt=90 * un.deg, + az=0 * un.deg, + obstime=Time(jd, format="jd"), + frame="altaz", + location=telescope_location, + ) + else: + zenith_coord = LunarSkyCoord( + alt=90 * un.deg, + az=0 * un.deg, + obstime=LTime(jd, format="jd"), + frame="lunartopo", + location=telescope_location, + ) + zenith_coord = zenith_coord.transform_to("icrs") + + zenith_coord.obstime.location = telescope_location obstimes = zenith_coord.obstime + time_past_zenith lsts = obstimes.sidereal_time("apparent", longitude=0.0).rad diff --git a/py21cmsense/data/farview/P_Tb_ultimate.npy b/py21cmsense/data/farview/P_Tb_ultimate.npy new file mode 100644 index 0000000000000000000000000000000000000000..ad87b6e31629691236e8326454df123d495a6e11 GIT binary patch literal 320 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I$7COVor3bhL411{yXqe@Jd_dBRxn0j$X$r*?Cf+U$wr_MMm)jyNDYtDHG z-sXlSFDG1Zm|?=K9LRFcq327?5t}t<9J=|8zBM!*bLjT|dg=bvLk{*6YvwM@-s=#3 z(^aSI&^CvjiK?@F&TVj5zNh2qx0}lyLT5BynVhk}p-Cw0;-}YB9W=`xdL7u=<4|zN z;9aarlf$QjYYzT&E^|hFWP?0_rBU(`x{TMhW<5KWPkSPo`ur;mf3$l z`BAqjV~zcrd8c*=@7riUtzB~JB+YI1J}c+;ept8Le!FJ=J|nM#_8av#F1|eDsJ$#x z&}OAyr|hp-6;5-zdBOgZ;cVVx+*j?{*-viy=y}UtaL%)j*4Yp2uN3y?HRz}gFOI~J9>Qp literal 0 HcmV?d00001 diff --git a/py21cmsense/observation.py b/py21cmsense/observation.py index f3a7e02..a39cccb 100755 --- a/py21cmsense/observation.py +++ b/py21cmsense/observation.py @@ -92,16 +92,16 @@ class Observation: time_per_day: tp.Time = attr.ib( 6 * un.hour, - validator=(tp.vld_physical_type("time"), ut.between(0 * un.hour, 24 * un.hour)), + validator=(tp.vld_physical_type("time"), ut.between(0 * un.hour, 655.2 * un.hour)), ) track: tp.Time | None = attr.ib( None, validator=attr.validators.optional( - [tp.vld_physical_type("time"), ut.between(0, 24 * un.hour)] + [tp.vld_physical_type("time"), ut.between(0 * un.hour, 655.2 * un.hour)] ), ) obs_duration: tp.Time = attr.ib( - validator=(tp.vld_physical_type("time"), ut.between(0, 24 * un.hour)), + validator=(tp.vld_physical_type("time"), ut.between(0 * un.hour, 655.2 * un.hour)), ) integration_time: tp.Time = attr.ib( 60 * un.second, validator=(tp.vld_physical_type("time"), ut.positive) diff --git a/py21cmsense/observatory.py b/py21cmsense/observatory.py index 15d3258..8f3ef78 100644 --- a/py21cmsense/observatory.py +++ b/py21cmsense/observatory.py @@ -51,6 +51,8 @@ class Observatory: of the array). Assumed to be in units of meters if no units are supplied. Can be used to limit antennas in arrays like HERA and SKA that have a "core" and "outriggers". The minimum is inclusive, and maximum exclusive. + world : string + A string specifiying whether the telescope is on the Earth or the moon. """ _antpos: tp.Length = attr.ib(eq=attr.cmp_using(eq=np.array_equal)) @@ -66,6 +68,7 @@ class Observatory: min_antpos: tp.Length = attr.ib( default=0.0 * un.m, validator=(tp.vld_physical_type("length"), ut.nonnegative) ) + world: str = attr.ib(default = 'earth') #Add validator stuff later @_antpos.validator def _antpos_validator(self, att, val): @@ -194,7 +197,7 @@ def projected_baselines( bl_wavelengths = baselines.reshape((-1, 3)) * self.metres_to_wavelengths - out = ut.phase_past_zenith(time_offset, bl_wavelengths, self.latitude) + out = ut.phase_past_zenith(time_offset, bl_wavelengths, self.latitude, self.world) out = out.reshape(*orig_shape[:-1], np.size(time_offset), orig_shape[-1]) if np.size(time_offset) == 1: @@ -225,7 +228,10 @@ def longest_baseline(self) -> float: @cached_property def observation_duration(self) -> un.Quantity[un.day]: """The time it takes for the sky to drift through the FWHM.""" + if self.world == "earth": return un.day * self.beam.fwhm / (2 * np.pi * un.rad) + else: + return 27.3 * un.day * self.beam.fwhm / (2 * np.pi * un.rad) def get_redundant_baselines( self, diff --git a/py21cmsense/sensitivity.py b/py21cmsense/sensitivity.py index 7447524..d8f0f8c 100644 --- a/py21cmsense/sensitivity.py +++ b/py21cmsense/sensitivity.py @@ -139,7 +139,7 @@ class PowerSpectrum(Sensitivity): horizon_buffer: tp.Wavenumber = attr.ib(default=0.1 * littleh / un.Mpc) foreground_model: str = attr.ib( - default="moderate", validator=vld.in_(["moderate", "optimistic"]) + default="moderate", validator=vld.in_(["moderate", "optimistic", "ultra_optimistic"]) ) theory_model: TheoryModel = attr.ib() @@ -457,6 +457,8 @@ def horizon_limit(self, umag: float) -> tp.Wavenumber: return horizon + self.horizon_buffer elif self.foreground_model in ["optimistic"]: return horizon * np.sin(self.observation.observatory.beam.first_null / 2) + elif self.foreground_model in ["ultra_optimistic"]: + return horizon def _average_sense_to_1d( self, sense: dict[tp.Wavenumber, tp.Delta], k1d: tp.Wavenumber | None = None diff --git a/py21cmsense/theory.py b/py21cmsense/theory.py index 489452a..df8f0c7 100644 --- a/py21cmsense/theory.py +++ b/py21cmsense/theory.py @@ -159,3 +159,64 @@ def delta_squared(self, z: float, k: np.ndarray) -> un.Quantity[un.mK**2]: ) return self.spline(k) << un.mK**2 + +class FarViewModel(TheoryModel): + """21cmFAST-based theory model explicitly for z=30, [Insert paper link here later]""" + + use_littleh: bool = False + + def __init__(self) -> None: + k_pth = ( + Path(__file__).parent + / "data/farview/kmag_ultimate.npy" + ) + + delta_pth = ( + Path(__file__).parent + / "data/farview/P_Tb_ultimate.npy" + ) + #Should at some point reorganize the data so these steps aren't necessary + k_fixed = np.load(k_pth) + power = np.load(delta_pth) + k_fixed = k_fixed[~np.isnan(power)] + power = power[~np.isnan(power)] + delta = (k_fixed**3 * power) / (2*np.pi**2) + + self.k = k_fixed + self.delta_squared_raw = delta + + self.spline = InterpolatedUnivariateSpline(self.k, self.delta_squared_raw, k=1) + + def delta_squared(self, z: float, k: np.ndarray) -> un.Quantity[un.mK**2]: + """Compute Delta^2(k, z) for the theory model. + + Parameters + ---------- + z + The redshift (should be a float). + k + The wavenumbers, either in units of 1/Mpc if use_littleh=False, or + h/Mpc if use_littleh=True. + + Returns + ------- + delta_squared + An array of delta_squared values in units of mK^2. + """ + if np.any(k > self.k.max()): + warnings.warn( + f"Extrapolating above the simulated theoretical k: {k.max()} > {self.k.max()}", + stacklevel=2, + ) + if np.any(k < self.k.min()): + warnings.warn( + f"Extrapolating below the simulated theoretical k: {k.min()} < {self.k.min()}", + stacklevel=2, + ) + if not 29.5 < z < 30.5: + warnings.warn( + f"Theory power corresponds to z=30, not z={z:.2f}", + stacklevel=2, + ) + + return self.spline(k) << un.mK**2 \ No newline at end of file From beb1ccbcb4e65e754182e9b14bddbc052d6aae31 Mon Sep 17 00:00:00 2001 From: Willow Smith Date: Mon, 25 Nov 2024 14:43:37 -0500 Subject: [PATCH 2/4] Lunar Array Capability --- src/py21cmsense/_utils.py | 38 +++++++++++++++++++++++++--------- src/py21cmsense/observation.py | 6 +++--- src/py21cmsense/observatory.py | 10 +++++++-- src/py21cmsense/sensitivity.py | 4 +++- 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/py21cmsense/_utils.py b/src/py21cmsense/_utils.py index 1b4ad4b..96bc3fc 100644 --- a/src/py21cmsense/_utils.py +++ b/src/py21cmsense/_utils.py @@ -5,7 +5,9 @@ from astropy.coordinates import EarthLocation, SkyCoord from astropy.time import Time from pyuvdata import utils as uvutils - +from lunarsky import MoonLocation +from lunarsky import SkyCoord as LunarSkyCoord +from lunarsky import Time as LTime def between(xmin, xmax): """Return an attrs validation function that checks a number is within bounds.""" @@ -39,7 +41,7 @@ def find_nearest(array, value): @un.quantity_input def phase_past_zenith( - time_past_zenith: un.day, bls_enu: np.ndarray, latitude, use_apparent: bool = True + time_past_zenith: un.day, bls_enu: np.ndarray, latitude, world, use_apparent: bool = True ): """Compute UVWs phased to a point rotated from zenith by a certain amount of time. @@ -56,6 +58,8 @@ def phase_past_zenith( The UVWs when phased to zenith. latitude The latitude of the center of the array, in radians. + world + Wether the telescope is on the Earth or Moon. Returns ------- @@ -64,20 +68,34 @@ def phase_past_zenith( """ # Generate ra/dec of zenith at time in the phase_frame coordinate system # to use for phasing - telescope_location = EarthLocation.from_geodetic(lon=0, lat=latitude) + if world == "earth": + telescope_location = EarthLocation.from_geodetic(lon=0, lat=latitude) + else: + telescope_location = MoonLocation.from_selenodetic(lon=0, lat=latitude) # JD is arbitrary jd = 2454600 + + if world == "earth": + zenith_coord = SkyCoord( + alt=90 * un.deg, + az=0 * un.deg, + obstime=Time(jd, format="jd"), + frame="altaz", + location=telescope_location, + ) + else: + zenith_coord = LunarSkyCoord( + alt=90 * un.deg, + az=0 * un.deg, + obstime=LTime(jd, format="jd"), + frame="lunartopo", + location=telescope_location, + ) - zenith_coord = SkyCoord( - alt=90 * un.deg, - az=0 * un.deg, - obstime=Time(jd, format="jd"), - frame="altaz", - location=telescope_location, - ) zenith_coord = zenith_coord.transform_to("icrs") + zenit_coord.obstime.location = telescope_location obstimes = zenith_coord.obstime + time_past_zenith lsts = obstimes.sidereal_time("apparent", longitude=0.0).rad diff --git a/src/py21cmsense/observation.py b/src/py21cmsense/observation.py index f012873..ffea8ec 100644 --- a/src/py21cmsense/observation.py +++ b/src/py21cmsense/observation.py @@ -94,16 +94,16 @@ class Observation: time_per_day: tp.Time = attr.ib( 6 * un.hour, - validator=(tp.vld_physical_type("time"), ut.between(0 * un.hour, 24 * un.hour)), + validator=(tp.vld_physical_type("time"), ut.between(0 * un.hour, 655.2 * un.hour)), ) track: tp.Time | None = attr.ib( None, validator=attr.validators.optional( - [tp.vld_physical_type("time"), ut.between(0, 24 * un.hour)] + [tp.vld_physical_type("time"), ut.between(0, 655.2 * un.hour)] ), ) lst_bin_size: tp.Time = attr.ib( - validator=(tp.vld_physical_type("time"), ut.between(0, 24 * un.hour)), + validator=(tp.vld_physical_type("time"), ut.between(0, 655.2 * un.hour)), ) integration_time: tp.Time = attr.ib( 60 * un.second, validator=(tp.vld_physical_type("time"), ut.positive) diff --git a/src/py21cmsense/observatory.py b/src/py21cmsense/observatory.py index f1bba59..9bce0ca 100644 --- a/src/py21cmsense/observatory.py +++ b/src/py21cmsense/observatory.py @@ -68,6 +68,8 @@ class Observatory: By default it is, so that the beam-crossing time is ``tday * FWHM / (2pi cos(lat))``. This affects both the thermal and sample variance calculations. + world: string + A string specifying whether the telescope is on the Earth or the moon. """ _antpos: tp.Length = attr.ib(eq=attr.cmp_using(eq=np.array_equal)) @@ -84,6 +86,7 @@ class Observatory: default=0.0 * un.m, validator=(tp.vld_physical_type("length"), ut.nonnegative) ) beam_crossing_time_incl_latitude: bool = attr.ib(default=True, converter=bool) + world: str = attr.ib(default = 'earth') #add validator stuff later @_antpos.validator def _antpos_validator(self, att, val): @@ -262,7 +265,7 @@ def projected_baselines( bl_wavelengths = baselines.reshape((-1, 3)) * self.metres_to_wavelengths - out = ut.phase_past_zenith(time_offset, bl_wavelengths, self.latitude) + out = ut.phase_past_zenith(time_offset, bl_wavelengths, self.latitude, self.world) out = out.reshape(*orig_shape[:-1], np.size(time_offset), orig_shape[-1]) if np.size(time_offset) == 1: @@ -294,7 +297,10 @@ def longest_baseline(self) -> float: def observation_duration(self) -> un.Quantity[un.day]: """The time it takes for the sky to drift through the FWHM.""" latfac = np.cos(self.latitude) if self.beam_crossing_time_incl_latitude else 1 - return un.day * self.beam.fwhm / (2 * np.pi * un.rad * latfac) + if self.world == "earth": + return un.day * self.beam.fwhm / (2 * np.pi * un.rad * latfac) + else: + return 27.3 * un.day * self.beam.fwhm/(2 * np.pi * un.rad * latfac) def get_redundant_baselines( self, diff --git a/src/py21cmsense/sensitivity.py b/src/py21cmsense/sensitivity.py index 754e8d7..121a867 100644 --- a/src/py21cmsense/sensitivity.py +++ b/src/py21cmsense/sensitivity.py @@ -151,7 +151,7 @@ class PowerSpectrum(Sensitivity): horizon_buffer: tp.Wavenumber = attr.ib(default=0.1 * littleh / un.Mpc) foreground_model: str = attr.ib( - default="moderate", validator=vld.in_(["moderate", "optimistic"]) + default="moderate", validator=vld.in_(["moderate", "optimistic", "ultra_optimistic"]) ) theory_model: TheoryModel = attr.ib() @@ -471,6 +471,8 @@ def horizon_limit(self, umag: float) -> tp.Wavenumber: return horizon + self.horizon_buffer elif self.foreground_model in ["optimistic"]: return horizon * np.sin(self.observation.observatory.beam.first_null / 2) + elif self.foreground_model in ["ultra_optimistic"]: + return horizon def _average_sense_to_1d( self, sense: dict[tp.Wavenumber, tp.Delta], k1d: tp.Wavenumber | None = None From a30706f82d49ef2f20189bb3e280d66cff6891c4 Mon Sep 17 00:00:00 2001 From: Willow Smith Date: Tue, 3 Dec 2024 23:29:01 -0500 Subject: [PATCH 3/4] Updating Theory Model For FarView --- .../py21cmsense}/data/farview/P_Tb_ultimate.npy | Bin .../py21cmsense}/data/farview/kmag_ultimate.npy | Bin src/py21cmsense/theory.py | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename {py21cmsense => src/py21cmsense}/data/farview/P_Tb_ultimate.npy (100%) rename {py21cmsense => src/py21cmsense}/data/farview/kmag_ultimate.npy (100%) diff --git a/py21cmsense/data/farview/P_Tb_ultimate.npy b/src/py21cmsense/data/farview/P_Tb_ultimate.npy similarity index 100% rename from py21cmsense/data/farview/P_Tb_ultimate.npy rename to src/py21cmsense/data/farview/P_Tb_ultimate.npy diff --git a/py21cmsense/data/farview/kmag_ultimate.npy b/src/py21cmsense/data/farview/kmag_ultimate.npy similarity index 100% rename from py21cmsense/data/farview/kmag_ultimate.npy rename to src/py21cmsense/data/farview/kmag_ultimate.npy diff --git a/src/py21cmsense/theory.py b/src/py21cmsense/theory.py index de418b7..00fc06b 100644 --- a/src/py21cmsense/theory.py +++ b/src/py21cmsense/theory.py @@ -256,4 +256,4 @@ def delta_squared(self, z: float, k: np.ndarray) -> un.Quantity[un.mK**2]: stacklevel=2, ) - return self.spline(k) << un.mK**2 \ No newline at end of file + return self.spline(k) << un.mK**2 From aeb60548eb25880ade48b6b35f419840e4c85485 Mon Sep 17 00:00:00 2001 From: Willow Smith Date: Wed, 4 Dec 2024 12:02:52 -0500 Subject: [PATCH 4/4] Typo --- src/py21cmsense/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py21cmsense/_utils.py b/src/py21cmsense/_utils.py index 96bc3fc..f7b0944 100644 --- a/src/py21cmsense/_utils.py +++ b/src/py21cmsense/_utils.py @@ -95,7 +95,7 @@ def phase_past_zenith( zenith_coord = zenith_coord.transform_to("icrs") - zenit_coord.obstime.location = telescope_location + zenith_coord.obstime.location = telescope_location obstimes = zenith_coord.obstime + time_past_zenith lsts = obstimes.sidereal_time("apparent", longitude=0.0).rad