# Licensed under a 3-clause BSD style license - see LICENSE.rst
import importlib
import inspect
import re
from sofia_redux.scan import custom
from sofia_redux.scan.channels import mode as mode_module
from sofia_redux.scan.coordinate_systems import grid as grid_module
from sofia_redux.scan.coordinate_systems import projection as projection_module
from sofia_redux.scan.simulation \
import source_models as simulated_source_module
custom_module_path = custom.__name__
__all__ = ['fix_instrument_name', 'get_instrument_module_path',
'to_class_name', 'to_module_name',
'get_instrument_path_and_class_name', 'get_class_for',
'channel_class_for',
'channel_data_class_for',
'channel_group_class_for',
'frames_class_for',
'frames_instance_for',
'info_class_for', 'get_integration_class',
'get_scan_class', 'default_modality_mode', 'get_grid_class',
'get_projection_class', 'get_simulated_source_class']
[docs]
def fix_instrument_name(name):
"""
Return a valid name for the given instrument.
Parameters
----------
name : str
The name of the instrument.
Returns
-------
name : str
"""
if not isinstance(name, str):
return ''
name = name.lower().strip()
if name == 'hawc+':
return 'hawc_plus'
elif name == 'fifi-ls' or name == 'fifils':
return 'fifi_ls'
return name
[docs]
def get_instrument_module_path(instrument):
"""
Return the module path to custom SOFSCAN instrument
Parameters
----------
instrument : str
The name of the instrument.
Returns
-------
module_path : str
The dot-separated module path for subsequent import via
:func:`importlib.import_module`.
"""
return f'{custom_module_path}.{fix_instrument_name(instrument)}'
[docs]
def to_class_name(name):
"""
Return the associated class type name for a given string.
Capitalizes the first letter of the instrument, upper-cases any character
following an underscore, and finally remove all underscores. For example,
converts 'hawc_plus` to `HawcPlus`.
Parameters
----------
name : str
The name to convert.
Returns
-------
class_name : str
"""
if name is None:
return ''
name = str(name)
if len(name) == 0:
return name
if len(name) == 1:
return name.upper()
return ''.join([s[0].upper() + s[1:] for s in name.split('_')])
[docs]
def to_module_name(class_name):
"""
Convert a class name to the base module it may be found in.
Inserts an underscore before any uppercase character or digit
(excluding the first) and converts the string to lower case.
Examples
--------
>>> print(to_module_name('TestClass1'))
test_class_1
Parameters
----------
class_name : str
Returns
-------
module_name : str
"""
if class_name is None:
return ''
class_name = str(class_name)
if len(class_name) == 0:
return class_name
result = class_name[0]
for letter in class_name[1:]:
if letter.isupper() or letter.isdigit():
result += f'_{letter}'
else:
result += letter
return result.lower()
[docs]
def get_instrument_path_and_class_name(instrument):
"""
Get the module path and class name prefix for a given instrument name.
Parameters
----------
instrument : str
The name of the instrument.
Returns
-------
instrument_module_path, class_prefix : str, str
The instrument module path such as 'sofia_redux.scan.custom.hawc_plus'
and the class name prefix for the instrument such as 'HawcPlus'.
"""
instrument = fix_instrument_name(instrument)
module_path = get_instrument_module_path(instrument)
instrument_class_name = to_class_name(instrument)
return module_path, instrument_class_name
[docs]
def get_class_for(instrument, module_path_name, other_module=None):
"""
Return an instrument specific class for a given module.
The SOFSCAN custom classes for any instrument should be placed in the
package in the following format:
scan.custom.<instrument>.<...>
For example, a custom channel data class for the HAWC_PLUS instrument
should be placed at:
scan.custom.hawc_plus.channels.channel_data.channel_data
and contain a class called ChannelData. Class names must always match the
`module_path_name` final path, begin with an upper case character, and
mark new words with an upper-case character. For example, the class
AbcDefGhi should be in the abc_def_ghi module.
Examples
--------
>>> print(get_class_for('sofia', 'frames'))
<class 'sofia_redux.scan.custom.sofia.frames.frames.SofiaFrames'>
Parameters
----------
instrument : str
The name of the instrument.
module_path_name : str
The dot-separated module path for the required class excluding the
custom instrument path. For example, 'channels.channel_data'.
other_module : str, optional
Usually, the class will be retrieved from a module matching the last
path level in `module_path_name`. For example, ChannelData will
usually be retrieved from
<instrument_path>.channels.channel_data.channel_data.
Set this to an empty string ('') to retrieve from
<instrument_path>.channels.channel_data or <other> to retrieve from
<instrument_path>.channels.channel_data.<other>.
Returns
-------
class
"""
base_path, class_prefix = get_instrument_path_and_class_name(instrument)
class_type = module_path_name.split('.')[-1]
class_suffix = to_class_name(class_type)
class_name = f'{class_prefix}{class_suffix}'
module_path = f'{base_path}.{module_path_name}'
if other_module is None:
full_path = f'{module_path}.{class_type}'
elif other_module == '':
full_path = module_path
else:
full_path = f'{module_path}.{other_module}'
# Allow errors
module = importlib.import_module(full_path)
retrieved_class = getattr(module, class_name)
return retrieved_class
[docs]
def channel_class_for(instrument):
"""
Returns a Channels instance for a given instrument.
Parameters
----------
instrument : str
The name of the instrument.
Returns
-------
Channels
"""
return get_class_for(instrument, 'channels')
[docs]
def channel_data_class_for(instrument):
"""
Returns a ChannelData instance for a given instrument.
Parameters
----------
instrument : str
The name of the instrument.
Returns
-------
channel_data_class : class (ChannelData)
"""
return get_class_for(instrument, 'channels.channel_data')
[docs]
def channel_group_class_for(instrument):
"""
Returns the appropriate ChannelGroup class for a given instrument.
Parameters
----------
instrument : str
The name of the instrument.
Returns
-------
class (ChannelGroup)
"""
return get_class_for(instrument, 'channels.channel_group')
[docs]
def frames_class_for(instrument):
"""
Returns the appropriate ChannelGroup class for a given instrument.
Parameters
----------
instrument : str
The name of the instrument.
Returns
-------
class (ChannelGroup)
"""
return get_class_for(instrument, 'frames')
[docs]
def frames_instance_for(instrument):
"""
Return a Frames instance for a given instrument.
Parameters
----------
instrument : str
The name of the instrument.
Returns
-------
Frames
"""
return frames_class_for(instrument)()
[docs]
def info_class_for(instrument):
"""
Return an Info instance given an instrument name.
Parameters
----------
instrument : str
The name of the instrument
Returns
-------
Info
"""
return get_class_for(instrument, 'info')
[docs]
def get_integration_class(instrument):
"""
Return an Integration instance given an instrument name.
Parameters
----------
instrument : str
The name of the instrument.
Returns
-------
Integration : class
"""
return get_class_for(instrument, 'integration')
[docs]
def get_scan_class(instrument):
"""
Return the appropriate scan class for an instrument name.
Parameters
----------
instrument : str
The instrument name.
Returns
-------
Scan : class
"""
return get_class_for(instrument, 'scan')
[docs]
def default_modality_mode(modality):
"""
Return a default mode class based on modality class.
For example, A CoupledModality should return CoupledMode. If
no analogous mode is found, a default base Mode will be returned.
Parameters
----------
modality : Modality
Returns
-------
class
The correct default mode class for the given modality.
"""
if inspect.isclass(modality):
modality_class = modality
else:
modality_class = modality.__class__
mode_module_path = mode_module.__name__
modality_name = modality_class.__name__.split('.')[-1]
mode_name = modality_name.replace('Modality', 'Mode')
mode_sub_path = re.sub(r'(?<!^)(?=[A-Z])', '_', mode_name).lower()
mode_path = f'{mode_module_path}.{mode_sub_path}'
try:
module = importlib.import_module(mode_path)
mode_class = getattr(module, mode_name)
except ModuleNotFoundError: # Return a basic mode.
mode_path = f'{mode_module_path}.mode'
module = importlib.import_module(mode_path)
mode_class = getattr(module, 'Mode')
return mode_class
[docs]
def get_grid_class(name):
"""
Returns a Grid class of the given name
Parameters
----------
name : str
The name of the grid.
Returns
-------
Grid : class
"""
if name in [None, '']:
return None
grid_path = grid_module.__name__
module_path = f'{grid_path}.{name}'
class_name = ''.join(
[s[0].upper() + s[1:] for s in name.split('_')])
if re.match(r'\dd', class_name[-2:]):
class_name = class_name[:-1] + class_name[-1].upper()
elif class_name.endswith('2d1'):
class_name = class_name[:-3] + '2D1'
module = importlib.import_module(module_path) # Allow errors
grid_class = getattr(module, class_name)
return grid_class
[docs]
def get_projection_class(name):
"""
Returns a Projection class of the given name
Parameters
----------
name : str
The name of the grid omitting the "projection" suffix.
Returns
-------
Projection : class
"""
if name in [None, '']:
return None
projection_path = projection_module.__name__
module_path = f'{projection_path}.{name}_projection'
class_name = ''.join(
[s[0].upper() + s[1:] for s in name.split('_')]) + 'Projection'
module = importlib.import_module(module_path) # Allow errors
projection_class = getattr(module, class_name)
return projection_class
[docs]
def get_simulated_source_class(name):
"""
Return a simulated source of the given name.
Parameters
----------
name : str
The name of the simulated source model.
Returns
-------
SimulatedSource
"""
if name in [None, '']:
return None
path = simulated_source_module.__name__
module_path = f'{path}.{name}'
class_name = to_class_name(name)
module = importlib.import_module(module_path)
source_class = getattr(module, class_name)
return source_class