""" Layered Tides Module
"""
from typing import Dict, List, TYPE_CHECKING
from TidalPy.exceptions import (BadAttributeValueError, IncorrectMethodToSetStateProperty)
from .base import TidesBase
if TYPE_CHECKING:
from TidalPy.utilities.types import FloatArray
from TidalPy.structures.world_types import LayeredWorld
from TidalPy.structures.layers import PhysicalLayerType
from ..modes.mode_manipulation import DissipTermsArray
[docs]
class LayeredTides(TidesBase):
""" LayeredTides
Class used for layered planets (icy or rocky world_types)
Tides class stores model parameters and methods for heating and torque which are functions of
(T, P, melt_frac, w, e, theata)
Attributes
----------
tidal_heating_by_layer
negative_imk_by_layer_by_orderl
See Also
--------
TidalPy.tides.methods.TidesBase
"""
model = 'layered'
def __init__(self, world: 'LayeredWorld', store_config_in_world: bool = True, initialize: bool = True):
""" Constructor for LayeredTides class
Parameters
----------
world : TidalWorldType
The world where tides are being calculated.
store_config_in_world : bool = True
Flag that determines if the final model's configuration dictionary should be copied into the
`world.config` dictionary.
initialize : bool = True
If `True`, then an initial call to the tide's reinit method will be made at the end of construction.
"""
super().__init__(world, store_config_in_world=store_config_in_world, initialize=False)
# State properties
self._tidal_heating_by_layer = {layer: None for layer in self.world}
self._negative_imk_by_layer = {layer: None for layer in self.world}
# Configuration properties
self._tidal_input_getters_by_layer = dict()
self._world_tidal_input_getters = None
if initialize:
self.reinit(initial_init=True)
[docs]
def reinit(self, initial_init: bool = False):
""" Load configurations into the Tides class and import any config-dependent functions.
This reinit process is separate from the __init__ method because the Orbit class may need to overload some
configurations after class initialization.
Parameters
----------
initial_init : bool = False
This should be set to True the first time reinit is called.
Raises
------
BadAttributeValueError
"""
super().reinit(initial_init)
# Reset configuration properties
if not initial_init:
self._tidal_input_getters_by_layer = dict()
self._world_tidal_input_getters = None
# Pull out tidal inputs
for layer in self.world:
if layer.is_tidal:
def get_tidal_scale():
return layer.tidal_scale
# This system assumes that density, radius, and gravity will not change after initialization
def get_radius():
return layer.radius
def get_bulk_density():
return layer.density_bulk
def get_surf_gravity():
return layer.gravity_surface
for param in [get_tidal_scale, get_radius, get_bulk_density, get_surf_gravity]:
if param is None:
# How did that happen?
raise BadAttributeValueError
self._tidal_input_getters_by_layer[layer] = \
(get_tidal_scale, get_radius, get_bulk_density, get_surf_gravity)
else:
# Layer does not contribute to tides. This will be marked by a None in this list
self._tidal_input_getters_by_layer[layer] = None
# Pull out planet properties that may be used based on the configuration
if self.config['use_planet_params_for_love_calc']:
# TODO: These are used to calculate the effective rigidity. Should these be for the layer or for the planet
# as a whole?
# TODO: Tidal scale for world? -> planet_tidal_scale = lambda: self.world.tidal
def get_world_radius():
return self.world.radius
def get_world_density():
return self.world.density_bulk
def get_world_gravity():
return self.world.gravity_surface
self._world_tidal_input_getters = (get_world_radius, get_world_density, get_world_gravity)
[docs]
def complex_compliances_changed(self, collapse_tidal_modes: bool = True):
""" The complex compliances have changed. Make any necessary updates.
Parameters
----------
collapse_tidal_modes : bool = True
If `True`, then the world will tell its tides model to collapse tidal modes.
"""
# This is called from bottom-to-top starting in the ComplexCompliances class inside Rheology.
if collapse_tidal_modes:
self.collapse_modes()
[docs]
def clear_state(self):
""" Clear the state properties for the layered tides model """
super().clear_state()
# Clear tidal results stored for each layer
self._tidal_heating_by_layer = {layer: None for layer in self.world}
self._negative_imk_by_layer = {layer: None for layer in self.world}
[docs]
def collapse_modes(self) -> 'DissipTermsArray':
""" Calculate Global Love number based on current thermal state.
Requires a prior orbit_spin_changed() call as unique frequencies are used to calculate the complex compliances
used to calculate the Love numbers.
Returns
-------
tidal_heating : np.ndarray
Tidal heating [W]
This may be restricted to a specific layer or for an entire planet.
dUdM : np.ndarray
Tidal potential derivative with respect to the mean anomaly [J kg-1 radians-1]
This may be restricted to a specific layer or for an entire planet.
dUdw : np.ndarray
Tidal potential derivative with respect to the pericentre [J kg-1 radians-1]
This may be restricted to a specific layer or for an entire planet.
dUdO : np.ndarray
Tidal potential derivative with respect to the planet's node [J kg-1 radians-1]
This may be restricted to a specific layer or for an entire planet.
Raises
------
MissingAttributeError
See Also
--------
TidalPy.tides.Tides.orbit_spin_changed
"""
super().collapse_modes()
# Check to see if all the needed state properties are present and then begin calculations
if self.tidal_terms_by_frequency is not None:
nonNone_love_number = list()
nonNone_neg_imk = list()
nonNone_effective_q = list()
nonNone_tidal_heating = list()
nonNone_dUdM = list()
nonNone_dUdw = list()
nonNone_dUdO = list()
love_number_by_layer = dict()
neg_imk_by_layer = dict()
tidal_heating_by_layer = dict()
dUdM_by_layer = dict()
dUdw_by_layer = dict()
dUdO_by_layer = dict()
# Clear global parameters
self._effective_q_by_orderl = dict()
self._global_negative_imk_by_orderl = dict()
self._global_love_by_orderl = dict()
broke_out = False
for layer, other_inputs in self._tidal_input_getters_by_layer.items():
if other_inputs is None:
# Not a tidal layer
love_number_by_layer[layer] = None
neg_imk_by_layer[layer] = None
tidal_heating_by_layer[layer] = None
dUdM_by_layer[layer] = None
dUdw_by_layer[layer] = None
dUdO_by_layer[layer] = None
else:
# Find getter functions
tidal_scale_getter, radius_getter, bulk_density_getter, gravity_surf_getter = other_inputs
if self._world_tidal_input_getters is None:
# Use layer properties - set above do nothing here
pass
else:
# Use world properties
radius_getter, bulk_density_getter, gravity_surf_getter = self._world_tidal_input_getters
# Use getters to find key parameters
tidal_scale = tidal_scale_getter()
radius = radius_getter()
bulk_density = bulk_density_getter()
gravity_surf = gravity_surf_getter()
# Pull out variables that change often
shear_modulus = layer.shear_modulus
complex_compliances_by_frequency_list = layer.rheology.complex_compliances
if shear_modulus is None or complex_compliances_by_frequency_list is None:
# uh oh
broke_out = True
break
# Mode collapse will parse through tidal order-l and all unique frequencies and calculate global and
# localized dissipation values
tidal_heating, dUdM, dUdw, dUdO, love_number_by_orderl, negative_imk_by_orderl, \
effective_q_by_orderl = \
self.collapse_modes_func(
gravity_surf, radius, bulk_density, shear_modulus,
tidal_scale, self.tidal_host.mass, self.tidal_susceptibility,
complex_compliances_by_frequency_list,
self.tidal_terms_by_frequency, self.max_tidal_order_lvl,
cpl_ctl_method=False
)
# These will be summed for global values
nonNone_love_number.append(love_number_by_orderl)
nonNone_neg_imk.append(negative_imk_by_orderl)
nonNone_effective_q.append(effective_q_by_orderl)
nonNone_tidal_heating.append(tidal_heating)
nonNone_dUdM.append(dUdM)
nonNone_dUdw.append(dUdw)
nonNone_dUdO.append(dUdO)
# Accessed by layer methods for thermal evolution
tidal_heating_by_layer[layer] = tidal_heating
neg_imk_by_layer[layer] = negative_imk_by_orderl
dUdM_by_layer[layer] = dUdM
dUdw_by_layer[layer] = dUdw
dUdO_by_layer[layer] = dUdO
if not broke_out:
# Loop finished successfully. Store info in accessible containers
self._tidal_heating_by_layer = tidal_heating_by_layer
self._negative_imk_by_layer = neg_imk_by_layer
self._tidal_heating_global = sum(nonNone_tidal_heating)
self._dUdM = sum(nonNone_dUdM)
self._dUdw = sum(nonNone_dUdw)
self._dUdO = sum(nonNone_dUdO)
# Sum the parameters that are stored by order-l
for order_l in range(2, self.max_tidal_order_lvl + 1):
# TODO: Should all of these simply be sums of each layer's contribution?
self._effective_q_by_orderl[order_l] = \
sum(
[nonNone_effective_q_for_layer[order_l] for nonNone_effective_q_for_layer
in nonNone_effective_q]
)
self._global_negative_imk_by_orderl[order_l] = \
sum(
[nonNone_neg_imk_for_layer[order_l] for nonNone_neg_imk_for_layer
in nonNone_neg_imk]
)
self._global_love_by_orderl[order_l] = \
sum(
[nonNone_love_number_for_layer[order_l] for nonNone_love_number_for_layer
in nonNone_love_number]
)
# Now tell other methods to update now that derivatives and heating has been altered
self.world.dissipation_changed()
else:
# Broke out from the layer loop for some reason. Likely because this function was called when not
# everything was set.
# Let it go.
pass
# raise MissingAttributeError
# Return tidal heating and derivatives
return self.tidal_heating_global, self.dUdM, self.dUdw, self.dUdO
# # State properties
@property
def tidal_heating_by_layer(self) -> Dict['PhysicalLayerType', 'FloatArray']:
""" Tidal heating stored by layer instance [W] """
return self._tidal_heating_by_layer
@tidal_heating_by_layer.setter
def tidal_heating_by_layer(self, value):
raise IncorrectMethodToSetStateProperty
@property
def negative_imk_by_layer_by_orderl(self) -> Dict['PhysicalLayerType', List['FloatArray']]:
""" -Im[k2] stored by layer instance and by order l"""
return self._negative_imk_by_layer
@negative_imk_by_layer_by_orderl.setter
def negative_imk_by_layer_by_orderl(self, value):
raise IncorrectMethodToSetStateProperty