<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Creating a feature
Features are derived from multiple analytes within a single trial. For example, product yield is a function of the substrate consumed, and product produced. Features are registered to the SingleTrial class and are composed of two components
Step1:  Then, we can build our features
Step2:  Finally, we register the feature
Step3:  and test it
<ASSISTANT_TASK:>
Python Code:
from impact.core.features import BaseAnalyteFeature, BaseAnalyteFeatureFactory
Explanation: Creating a feature
Features are derived from multiple analytes within a single trial. For example, product yield is a function of the substrate consumed, and product produced. Features are registered to the SingleTrial class and are composed of two components: the feature, and the feature manager.
The feature manager ensures that the feature is created as soon as all the required analytes are available. Here we build the OD normalization feature as an example
First we import the base features:
End of explanation
class ODNormalizedData(BaseAnalyteFeature):
    # The constructor should accept all required analytes as parameters 
    def __init__(self, biomass, reporter):
        self.biomass = biomass
        self.reporter = reporter
        self.normalized_data = None
    
    # This data property assures that the data is returned, or calculated as needed
    @property
    def data(self):
        if self.normalized_data is None: self.calculate()
        return self.normalized_data
    
    # This is where the property is actually calculated and set
    def calculate(self):
        self.normalized_data = self.reporter.data_vector/self.biomass.data_vector
        
# The feature factory watches for those analytes
class ODNormalizedDataFactory(BaseAnalyteFeatureFactory):
    # define what the feature
    requires = ['biomass','reporter']
    name = 'od_normalized_data'
    
    # constructor should initialize variables until all required analytes are present,
    # this will ensure that despite the order analytes are added, feature will be calculated appropriately
    def __init__(self):
        self.biomass = None
        self.reporter = None
    
    # define how to handle new analytes
    def add_analyte_data(self, analyte_data):
        if analyte_data.trial_identifier.analyte_type == 'reporter':
            self.reporter = analyte_data
        elif analyte_data.trial_identifier.analyte_type == 'biomass':
            self.biomass = analyte_data   
            
        if self.reporter is not None and self.biomass is not None:
            setattr(analyte_data,self.name,ODNormalizedData(biomass,reporter))
Explanation: Then, we can build our features
End of explanation
import impact.core.SingleTrial as SingleTrial
SingleTrial.register_feature(ODNormalizedDataFactory)
Explanation: Finally, we register the feature
End of explanation
from impact.core.AnalyteData import Biomass, Reporter
from impact.core.TrialIdentifier import ReplicateTrialIdentifier as TI
t = [0,1,2,3,4]
biomass_data = [0.1,0.2,0.4,0.8,0.8]
reporter_data = [1000,2000,3000,4000,5000]
biomass = Biomass()
biomass.time_vector = t
biomass.data_vector = biomass_data
ti = TI()
ti.analyte_name = 'OD'
ti.analyte_type = 'biomass'
biomass.trial_identifier = ti
reporter = Reporter()
reporter.time_vector = t
reporter.data_vector = reporter_data
ti = TI()
ti.analyte_name = 'gfp'
ti.analyte_type = 'reporter'
reporter.trial_identifier = ti
trial = SingleTrial()
trial.add_analyte_data(biomass)
trial.add_analyte_data(reporter)
import numpy as np
calculated_data = trial.analyte_dict['gfp'].od_normalized_data.data
expected_data = np.array(reporter_data)/biomass_data
print(calculated_data)
print(expected_data)
print(all(calculated_data==expected_data))
Explanation: and test it
End of explanation
<END_TASK>