"""
Main brightness calculator for Lumos
"""
import numpy as np
import lumos.constants
import lumos.conversions
import lumos.geometry
import astropy.coordinates
[docs]def get_earthshine_panels(sat_z, angle_past_terminator, density):
"""
Creates a mesh of pixels on Earth's surface which are visible to the satellite and illuminated
by the sun.
:param sat_z: The height of the satellite above the center of Earth (meters)
:type sat_z: float
:param angle_past_terminator: The angle of the satellite past the terminator (radians)
:type angle_past_terminator: float
:param density: The density of the pixels. Grid will have size density x density.
:type density: int
:returns:
- (x, y, z) - Positions of pixels (meters)
- (nx, ny, nz) - Normal vectors of pixels
- areas - Areas of pixels (:math:`m^2`)
"""
R = lumos.constants.EARTH_RADIUS
max_angle = np.arccos(R / sat_z)
angles_off_plane = np.linspace(-max_angle, max_angle, density)
angles_on_plane = np.linspace(angle_past_terminator, max_angle, density)
d_phi = abs( angles_off_plane[1] - angles_off_plane[0] )
d_theta = abs( angles_on_plane[1] - angles_on_plane[0] )
angles_on_plane, angles_off_plane = np.meshgrid(angles_on_plane, angles_off_plane)
angles_on_plane, angles_off_plane = angles_on_plane.flatten(), angles_off_plane.flatten()
# Set up panel positions
nz = 1 / np.sqrt( 1 + np.tan(angles_on_plane)**2 + np.tan(angles_off_plane)**2 )
nx = np.tan(angles_off_plane) * nz
ny = np.tan(angles_on_plane) * nz
# Clip the panels which aren't visible to the satellite
visible_to_sat = np.arccos(nz) < max_angle
angles_off_plane, angles_on_plane = angles_off_plane[visible_to_sat], angles_on_plane[visible_to_sat]
nx, ny, nz = nx[visible_to_sat], ny[visible_to_sat], nz[visible_to_sat]
# Calculate Jacobian determinant to get panel areas
x, y, z = nx * R, ny * R, nz * R
phi = angles_off_plane
theta = angles_on_plane
dx_dr = nx / nz * z / R
dx_dphi = z**3 / (R**2 * np.cos(phi)**2 * np.cos(theta)**2)
dx_dtheta = - (ny / nz * nx / nz* z**3 ) / (R**2 * np.cos(theta)**2 )
dy_dr = np.tan(theta) * z / R
dy_dphi = - ( ny / nz * nx / nz * z**3 ) / (R**2 * np.cos(phi)**2 )
dy_dtheta = dx_dphi
dz_dr = z / R
dz_dphi = - (nx / nz * z**3 ) / (R**2 * np.cos(phi)**2)
dz_dtheta = - (ny / nz * z**3 ) / (R**2 * np.cos(theta)**2)
determinant = (
dx_dr * (dy_dphi * dz_dtheta - dy_dtheta * dz_dphi) -
dy_dr * (dx_dphi * dz_dtheta - dx_dtheta * dz_dphi) +
dz_dr * (dx_dphi * dy_dtheta - dx_dtheta * dy_dphi) )
areas = determinant * d_phi * d_theta
return x, y, z, nx, ny, nz, areas
[docs]def get_intensity_satellite_frame(
sat_surfaces,
sat_height,
angle_past_terminator,
observer_position,
include_sun = True,
include_earthshine = True,
earth_panel_density = 150,
earth_brdf = None):
'''
Calculates flux scattered by a satellite and seen by an observer.
:param sat_surfaces: List of surfaces on satellite
:type sat_surfaces: list[lumos.geometry.Surface]
:param sat_height: Height of satellite above geodetic nadir (meters)
:type sat_height: float
:param angle_past_terminator: Angle of satellite past terminator (radians)
:type angle_past_terminator: float
:param observer_position: Position of an observer, measured in brightness frame (meters)
:type observer_position: :class:`np.ndarray`
:param include_sun: Whether to include flux scattered by the satellite from the sun
:type include_sun: bool, optional
:param include_earthshine: Whether to include flux scattered by the satellite from earthshine
:type include_earthshine: bool, optional
:param earth_panel_density: There will be earth_panel_density x earth_panel_density panels in the earthshine mesh
:type earth_panel_density: int, optional
:param earth_brdf: A function representing the BRDF of Earth's surface
:type earth_brdf: function
:return: Flux of light incident on the observer (W / m^2)
:rtype: float
'''
horizon_angle = np.arccos(lumos.constants.EARTH_RADIUS / (lumos.constants.EARTH_RADIUS + sat_height))
if angle_past_terminator > horizon_angle:
# Inside earth's shadow
return 0
observer_x, observer_y, observer_z = observer_position[0], observer_position[1], observer_position[2]
if np.arccos(observer_z / lumos.constants.EARTH_RADIUS) > horizon_angle + np.deg2rad(1):
# Not visible to observer
# Fudging this by 1 degree makes plots much nicer on edges
return 0
vector_2_sun = (0, np.cos(angle_past_terminator), - np.sin(angle_past_terminator))
sat_z = sat_height + lumos.constants.EARTH_RADIUS
# Distances from observers to satellite
dist_sat_2_obs = np.sqrt( observer_x**2 + observer_y**2
+ (observer_z - sat_z)**2 )
# Unit vectors from observers to satellite
sat_obs_x = observer_x / dist_sat_2_obs
sat_obs_y = observer_y / dist_sat_2_obs
sat_obs_z = (observer_z - sat_z) / dist_sat_2_obs
if include_earthshine:
panel_x, panel_y, panel_z, panel_nx, panel_ny, panel_nz, panel_areas = \
get_earthshine_panels(sat_z, angle_past_terminator, earth_panel_density)
# Distances from earthshine panels to satellite
dist_panels_2_sat = np.sqrt(panel_x**2 + panel_y**2 + (sat_z - panel_z)**2 )
panel_sat_x = - panel_x / dist_panels_2_sat
panel_sat_y = - panel_y / dist_panels_2_sat
panel_sat_z = (sat_z - panel_z) / dist_panels_2_sat
# Panel Normalization
panel_normalizations = (panel_ny * vector_2_sun[1] + panel_nz * vector_2_sun[2])
panel_brdfs = earth_brdf(vector_2_sun,
(panel_nx, panel_ny, panel_nz),
(panel_sat_x, panel_sat_y, panel_sat_z))
intensity = 0
for surface in sat_surfaces:
if callable(surface.normal):
surface_normal = surface.normal(angle_past_terminator)
else:
surface_normal = surface.normal
sun_contribution = 0
earth_contribution = 0
surface_normalization = surface_normal[1] * vector_2_sun[1] + surface_normal[2] * vector_2_sun[2]
surface_normalization = np.clip(surface_normalization, 0, None)
observer_normalization = surface_normal[0] * sat_obs_x + surface_normal[1] * sat_obs_y + surface_normal[2] * sat_obs_z
observer_normalization = np.clip(observer_normalization, 0, None)
if include_sun:
surface_brdf = surface.brdf(vector_2_sun,
surface_normal,
(sat_obs_x, sat_obs_y, sat_obs_z))
sun_contribution = sun_contribution + surface_brdf * surface_normalization * observer_normalization
if include_earthshine:
surface_normalizations = np.clip(
- surface_normal[0] * panel_sat_x \
- surface_normal[1] * panel_sat_y \
- surface_normal[2] * panel_sat_z,
0,
None
)
panel_observing_normalization = np.clip(
panel_nx * panel_sat_x
+ panel_ny * panel_sat_y
+ panel_nz * panel_sat_z,
0,
None
)
surface_brdf = surface.brdf( (-panel_sat_x, -panel_sat_y, -panel_sat_z),
surface_normal,
(sat_obs_x, sat_obs_y, sat_obs_z) )
earth_contribution = earth_contribution \
+ np.sum(panel_brdfs * surface_brdf
* panel_normalizations * observer_normalization
* surface_normalizations * panel_observing_normalization
* panel_areas / dist_panels_2_sat**2 )
intensity = intensity + lumos.constants.SUN_INTENSITY * surface.area * (sun_contribution + earth_contribution) / dist_sat_2_obs ** 2
return intensity
[docs]def get_brightness_coords(
sat_alt,
sat_az,
sat_height,
sun_alt,
sun_az):
"""
Converts from HCS observer coordinates to brightness coordinates.
:param sat_alt: Altitude of satellite (degrees)
:type sat_alt: float or :class:`np.ndarray`
:param sat_az: Altitude of satellite (degrees)
:type sat_az: float or :class:`np.ndarray`
:param sat_height: Geodetic height of satellite (meters)
:type sat_height: float or :class:`np.ndarray`
:param sun_alt: Altitude of sun (degrees)
:type sun_alt: float
:param sun_az: Azimuth angle of sun (degrees)
:type sun_az: float
:returns:
- (obs_x, obs_y, obs_z) - Vector from satellite to observer in brightness frame (meters)
- angle_past_terminator - Angle of satellite past terminator (radians)
"""
sat_x, sat_y, sat_z = lumos.conversions.altaz_to_unit(sat_alt, sat_az)
sun_x, sun_y, sun_z = lumos.conversions.altaz_to_unit(sun_alt, sun_az)
phi = np.arccos(sat_z) - np.arcsin(lumos.constants.EARTH_RADIUS / (lumos.constants.EARTH_RADIUS + sat_height) * np.sqrt(sat_x**2 + sat_y**2) )
theta = np.arctan2(sat_y, sat_x)
Zx = np.sin(phi) * np.cos(theta)
Zy = np.sin(phi) * np.sin(theta)
Zz = np.cos(phi)
dot = Zx * sun_x + Zy * sun_y + Zz * sun_z
beta = 1 / np.sqrt(1 - dot**2)
alpha = - beta * dot
Yx = alpha * Zx + beta * sun_x
Yy = alpha * Zy + beta * sun_y
Yz = alpha * Zz + beta * sun_z
flip = np.sign(Yx * sun_x + Yy * sun_y + Yz * sun_z)
Yx, Yy, Yz = flip * Yx, flip * Yy, flip * Yz
Xx = Yy * Zz - Zy * Yz
Xy = Zx * Yz - Yx * Zz
Xz = Yx * Zy - Zx * Yy
T11, T12, T13, \
T21, T22, T23, \
T31, T32, T33 = lumos.functions.inv_3(Xx, Yx, Zx,
Xy, Yy, Zy,
Xz, Yz, Zz)
dist_to_sat = np.sqrt(lumos.constants.EARTH_RADIUS**2
+ (lumos.constants.EARTH_RADIUS + sat_height)**2
- 2 * lumos.constants.EARTH_RADIUS * (lumos.constants.EARTH_RADIUS + sat_height) * np.cos(phi))
obs_x = - dist_to_sat * (T11 * sat_x + T12 * sat_y + T13 * sat_z)
obs_y = - dist_to_sat * (T21 * sat_x + T22 * sat_y + T23 * sat_z)
obs_z = (lumos.constants.EARTH_RADIUS + sat_height) - dist_to_sat * (T31 * sat_x + T32 * sat_y + T33 * sat_z)
angle_past_terminator = -np.arcsin(T31 * sun_x + T32 * sun_y + T33 * sun_z)
return obs_x, obs_y, obs_z, angle_past_terminator
[docs]def get_intensity_observer_frame(
sat_surfaces,
sat_heights,
sat_altitudes,
sat_azimuths,
sun_altitude,
sun_azimuth,
include_sun = True,
include_earthshine = True,
earth_panel_density = 150,
earth_brdf = None):
"""
Calculates the flux of a satellite seen by an observer.
:param sat_surfaces: List of surfaces on satellite
:type sat_surfaces: list[lumos.geometry.Surface]
:param sat_heights: Heights of satellite above geodetic nadir (meters)
:type sat_heights: float or :class:`np.ndarray`
:param sat_altitudes: Satellite altitudes in HCS frame (degrees)
:type sat_altitudes: float or :class:`np.ndarray`
:param sat_azimuths: Satellite azimuths in HCS frame (degrees)
:type sat_azimuths: float or :class:`np.ndarray`
:param sun_altitude: Altitude of sun in HCS frame (degrees)
:type sun_altitude: float
:param sun_azimuth: Azimuth of sun in HCS frame (degrees)
:type sun_azimuth: float
:param include_sun: Whether to include flux scattered by the satellite from the sun
:type include_sun: bool, optional
:param include_earthshine: Whether to include flux scattered by the satellite from earthshine
:type include_earthshine: bool, optional
:param earth_panel_density: There will be earth_panel_density x earth_panel_density panels in the earthshine mesh
:type earth_panel_density: int, optional
:param earth_brdf: A function representing the BRDF of Earth's surface
:type earth_brdf: function
:return: Flux of light incident on the observer (W / m^2)
:rtype: float or :class:`np.ndarray`
"""
if sun_altitude > 0:
raise ValueError(f"Observatory is in daylight! Sun Altitude = {sun_altitude}")
sat_alts, sat_azs = np.asarray(sat_altitudes), np.asarray(sat_azimuths)
output_shape = sat_alts.shape
if not isinstance(sat_heights, np.ndarray):
sat_heights = sat_heights * np.ones(output_shape)
sat_heights, sat_alts, sat_azs = sat_heights.flatten(), sat_alts.flatten(), sat_azs.flatten()
intensity = np.zeros_like(sat_alts)
obs_x, obs_y, obs_z, angle_past_terminator = get_brightness_coords(sat_alts, sat_azs, sat_heights,
sun_altitude, sun_azimuth)
for (i, (x, y, z, alpha, h)) in enumerate(zip(obs_x, obs_y, obs_z, angle_past_terminator, sat_heights)):
intensity[i] = get_intensity_satellite_frame(
sat_surfaces, h, alpha, (x, y, z),
include_sun, include_earthshine, earth_panel_density, earth_brdf)
intensity = intensity.reshape(output_shape)
return intensity
[docs]def get_sun_alt_az(time, observer_location):
"""
Convenience function for finding the altitude and azimuth of the sun
:param time: Time of observation
:type time: :class:`astropy.time.Time`
:param observer_location: Location of observation
:type observer_location: :class:`astropy.coordinates.EarthLocation`
"""
aa_frame = astropy.coordinates.AltAz(obstime = time, location = observer_location)
sun_altaz = astropy.coordinates.get_sun(time).transform_to(aa_frame)
return sun_altaz.alt.degree, sun_altaz.az.degree