Source code for ctapipe.core.feature_generator
"""
Generate Features.
"""
from collections import ChainMap
from copy import deepcopy
from astropy.table import QTable, Table
from .component import Component
from .expression_engine import ExpressionEngine
from .traits import List, Tuple, Unicode
__all__ = [
"FeatureGenerator",
"FeatureGeneratorException",
"shallow_copy_table",
]
def shallow_copy_table(
table, output_cls: type[Table] | type[QTable] | None = None
) -> Table | QTable:
"""
Make a shallow copy of the table.
Data of the existing columns will be shared between shallow copies, but
adding / removing columns won't be seen in the original table. Metadata for
the new table will be a copy (not shallow) of the original metadata, so that
new metadata can be added without affecting the original table.
Parameters
----------
output_cls: type[Table] | type[QTable] | None
type of the output table. If None, use the input table type
"""
output_cls = output_cls or table.__class__
new_table = output_cls({col: table[col] for col in table.colnames}, copy=False)
new_table.meta = deepcopy(table.meta)
return new_table
class FeatureGeneratorException(TypeError):
"""Signal a problem with a user-defined selection criteria function"""
[docs]
class FeatureGenerator(Component):
"""
Generate features for astropy.table.Table.
Raises Exceptions in two cases:
1. If a feature already exists in the table
2. If a feature cannot be built with the given expression
"""
features = List(
Tuple(Unicode(), Unicode()),
help=(
"List of 2-Tuples of Strings: ('new_feature_name', 'expression to generate feature'). "
"You can use ``numpy`` as ``np`` and ``astropy.units`` as ``u``. "
"Several math functions are usable without the ``np``-prefix. "
"Use ``feature.quantity.to_value(unit)`` to create features without units."
),
).tag(config=True)
def __init__(self, config=None, parent=None, **kwargs):
super().__init__(config=config, parent=parent, **kwargs)
self.engine = ExpressionEngine(expressions=self.features)
self._feature_names = [name for name, _ in self.features]
[docs]
def __call__(self, table: Table | QTable, **kwargs) -> Table:
"""
Apply feature generation to the input table.
This method returns a shallow copy of the input table with the
new features added. Existing columns will share the underlying data,
however the new columns won't be visible in the input table.
Parameters
----------
table: QTable | Table
Input table. Internally a Table will be converted to a QTable so that
unit propagation works, so expressions should only rely on properties of QTables.
**kwargs:
Other objects that should be available in expressions. For example,
if a you pass ``subarray=subarray``, expressions can use that
object. This can also be special functions like ``f=my_function``,
which would allow an expression like ``"f(col1)"``.
Returns
-------
QTable|Table:
A new table with the same columns as the input, but with new columns
for each feature. The returned class depends on what was passed in.
"""
table_copy = shallow_copy_table(table, output_cls=QTable)
lookup = ChainMap(table_copy, kwargs)
for result, name in zip(self.engine(lookup), self._feature_names):
if name in table_copy.colnames:
raise FeatureGeneratorException(f"{name} is already a column of table.")
try:
table_copy[name] = result
except Exception as err:
raise err
return table.__class__(table_copy) # ensure the return type is what is expected
def __len__(self):
return len(self.features)