<SYSTEM_TASK:>
Solve the following problem using Python, implementing the functions described below, one line at a time
<END_TASK>
<USER_TASK:>
Description:
 def determine_eigen_directions(metricParams, preserveMoments=False, vary_fmax=False, vary_density=None):
 """ This function will calculate the coordinate transfomations that are needed to rotate from a coordinate system described by the various Lambda components in the frequency expansion, to a coordinate system where the metric is Cartesian. Parameters metricParams : metricParameters instance Structure holding all the options for construction of the metric. preserveMoments : boolean, optional (default False) Currently only used for debugging. If this is given then if the moments structure is already set within metricParams then they will not be recalculated. vary_fmax : boolean, optional (default False) If set to False the metric and rotations are calculated once, for the full range of frequency [f_low,f_upper). If set to True the metric and rotations are calculated multiple times, for frequency ranges [f_low,f_low + i*vary_density), where i starts at 1 and runs up until f_low + (i+1)*vary_density > f_upper. Thus values greater than f_upper are *not* computed. The calculation for the full range [f_low,f_upper) is also done. vary_density : float, optional If vary_fmax is True, this will be used in computing the frequency ranges as described for vary_fmax. Returns -------- metricParams : metricParameters instance Structure holding all the options for construction of the metric. **THIS FUNCTION ONLY RETURNS THE CLASS** The following will be **added** to this structure metricParams.evals : Dictionary of numpy.array Each entry in the dictionary corresponds to the different frequency ranges described in vary_fmax. If vary_fmax = False, the only entry will be f_upper, this corresponds to integrals in [f_low,f_upper). This entry is always present. Each other entry will use floats as keys to the dictionary. These floats give the upper frequency cutoff when it is varying. Each numpy.array contains the eigenvalues which, with the eigenvectors in evecs, are needed to rotate the coordinate system to one in which the metric is the identity matrix. metricParams.evecs : Dictionary of numpy.matrix Each entry in the dictionary is as described under evals. Each numpy.matrix contains the eigenvectors which, with the eigenvalues in evals, are needed to rotate the coordinate system to one in which the metric is the identity matrix. metricParams.metric : Dictionary of numpy.matrix Each entry in the dictionary is as described under evals. Each numpy.matrix contains the metric of the parameter space in the Lambda_i coordinate system. metricParams.moments : Moments structure See the structure documentation for a description of this. This contains the result of all the integrals used in computing the metrics above. It can be used for the ethinca components calculation, or other similar calculations. """


    evals = {}
    evecs = {}
    metric = {}
    unmax_metric = {}

    # First step is to get the moments needed to calculate the metric
    if not (metricParams.moments and preserveMoments):
        get_moments(metricParams, vary_fmax=vary_fmax,
                    vary_density=vary_density)

    # What values are going to be in the moments
    # J7 is the normalization factor so it *MUST* be present
    list = metricParams.moments['J7'].keys()

    # We start looping over every item in the list of metrics
    for item in list:
        # Here we convert the moments into a form easier to use here
        Js = {}
        for i in range(-7,18):
            Js[i] = metricParams.moments['J%d'%(i)][item]

        logJs = {}
        for i in range(-1,18):
            logJs[i] = metricParams.moments['log%d'%(i)][item]

        loglogJs = {}
        for i in range(-1,18):
            loglogJs[i] = metricParams.moments['loglog%d'%(i)][item]

        logloglogJs = {}
        for i in range(-1,18):
            logloglogJs[i] = metricParams.moments['logloglog%d'%(i)][item]

        loglogloglogJs = {}
        for i in range(-1,18):
            loglogloglogJs[i] = metricParams.moments['loglogloglog%d'%(i)][item]

        mapping = generate_mapping(metricParams.pnOrder)

        # Calculate the metric
        gs, unmax_metric_curr = calculate_metric(Js, logJs, loglogJs,
                                          logloglogJs, loglogloglogJs, mapping)
        metric[item] = numpy.matrix(gs)
        unmax_metric[item] = unmax_metric_curr

        # And the eigenvalues
        evals[item],evecs[item] = numpy.linalg.eig(gs)

        # Numerical error can lead to small negative eigenvalues.
        for i in range(len(evals[item])):
            if evals[item][i] < 0:
                # Due to numerical imprecision the very small eigenvalues can
                # be negative. Make these positive.
                evals[item][i] = -evals[item][i]
            if evecs[item][i,i] < 0:
                # We demand a convention that all diagonal terms in the matrix
                # of eigenvalues are positive.
                # This is done to help visualization of the spaces (increasing
                # mchirp always goes the same way)
                evecs[item][:,i] = - evecs[item][:,i]

    metricParams.evals = evals
    metricParams.evecs = evecs
    metricParams.metric = metric
    metricParams.time_unprojected_metric = unmax_metric

    return metricParams