Пример #1
0
    def _initialize(self,
                    phot_fmt,
                    time=None,
                    brightness=None,
                    err_brightness=None,
                    coords=None):
        """
        Internal function to import photometric data into the correct
        form using a few numpy ndarrays.

        Parameters:
            phot_fmt - Specifies type of photometry. Either 'flux' or 'mag'.
            time - Date vector of the data
            brightness - vector of the photometric measurements
            err_brightness - vector of the errors in the phot measurements.
            coords - Sky coordinates of the event, optional

        """
        if self._init_keys['add245'] and self._init_keys['add246']:
            raise ValueError('You cannot initialize MulensData with both ' +
                             'add_2450000 and add_2460000 being True')

        if time.dtype != np.float64:
            raise TypeError(('time vector in MulensData() must be of ' +
                             'numpy.float64 type, not {:}').format(time.dtype))

        # Adjust the time vector as necessary.
        if self._init_keys['add245']:
            time += 2450000.
        elif self._init_keys['add246']:
            time += 2460000.

        # Store the time vector
        self._time = time
        self._n_epochs = len(time)

        # Check that the number of epochs equals the number of observations
        if ((len(brightness) != self._n_epochs)
                or (len(err_brightness) != self._n_epochs)):
            raise ValueError('input data in MulesData have different lengths')

        # Store the photometry
        self._brightness_input = brightness
        self._brightness_input_err = err_brightness
        self._input_fmt = phot_fmt

        # Create the complementary photometry (mag --> flux, flux --> mag)
        if phot_fmt == "mag":
            self._mag = self._brightness_input
            self._err_mag = self._brightness_input_err
            (self._flux, self._err_flux) = Utils.get_flux_and_err_from_mag(
                mag=self.mag, err_mag=self.err_mag)
        elif phot_fmt == "flux":
            self._flux = self._brightness_input
            self._err_flux = self._brightness_input_err
            self._mag = None
            self._err_mag = None
        else:
            msg = 'unknown brightness format in MulensData'
            raise ValueError(msg)
Пример #2
0
    def get_chi2_format(self, data):
        """
        Microlensing model in the format used for chi^2 calculation.
        The output is in flux space in most cases, but can be in
        magnitudes depending on dataset format.

        Parameters :
            data: :py:class:`~MulensModel.mulensdata.MulensData`
                A dataset for which model will be returned.

        Returns :
            model: *np.ndarray*
                Microlensing model in flux units or magnitudes (depending on
                the settings of input data).

        """
        if data not in self._flux_blending:
            self._flux_blending[data] = 0.
            warnings.warn("Blending flux not set. This is strange...",
                          SyntaxWarning)

        if data.chi2_fmt == "mag":
            result = Utils.get_mag_from_flux(self.get_flux(data))
        elif data.chi2_fmt == "flux":
            result = self.get_flux(data)
        else:
            msg = 'Fit.get_chi2_format() unrecognized data input format'
            raise ValueError(msg)
        return result
Пример #3
0
 def set_satellite_data(self, theta):
     """set satellite dataset magnitudes and fluxes"""
     self._run_cpm(theta)
     n_0 = self.n_datasets - self.n_sat
     for i in range(self.n_sat):
         ii = n_0 + i
         # OLD:
         sat_residuals = self.cpm_sources[i].residuals[self._sat_masks[i]]
         flux = self._sat_models[i][self._sat_masks[i]] + sat_residuals
         # NEW - IF ACCEPTED!!! :
         # sat_residuals = self.cpm_sources[i].residuals[
         #     self.cpm_sources[i].residuals_mask]
         # flux = self._sat_models[i][self.cpm_sources[i].residuals_mask]
         # flux += sat_residuals
         #
         # Below we use private properties from MulensModel - this
         # should be corrected (maybe make new MulensData object and
         # delete the old one).
         self.event.datasets[ii]._flux = flux
         mag_and_err = Utils.get_mag_and_err_from_flux(
             flux,
             self.event.datasets[ii].err_flux,
             zeropoint=K2_MAG_ZEROPOINT)
         self.event.datasets[ii]._mag = mag_and_err[0]
         self.event.datasets[ii]._err_mag = mag_and_err[1]
Пример #4
0
    def get_input_format(self, data=None):
        """
        Microlensing model in the same format as given dataset. The
        output is either in flux units or magnitudes, depending on
        format of the input data.

        Parameters :
            data: :py:class:`~MulensModel.mulensdata.MulensData`
                A dataset for which model will be returned.

        Returns :
            model: *np.ndarray*
                Microlensing model in flux units or magnitudes (depending on
                the format of input data).

        """
        if data is None:
            raise ValueError('Fit.get_input_format() dataset not provided')
        if data not in self._flux_blending:
            self._flux_blending[data] = 0.
            warnings.warn("Blending flux not set. This is strange...",
                          SyntaxWarning)

        # Return the model flux in either flux or magnitudes
        # (depending on data)
        if data.input_fmt == "mag":
            result = Utils.get_mag_from_flux(self.get_flux(data))
        elif data.input_fmt == "flux":
            result = self.get_flux(data)
        else:
            msg = 'Fit.get_input_format() unrecognized data input format'
            raise ValueError(msg)
        return result
Пример #5
0
    def _read_horizons_file(self):
        """
        reads standard output from JPL Horizons
        """
        # Read in the file
        self._get_start_end()
        data = np.genfromtxt(self._file_properties['file_name'],
                             dtype=[('date', 'S17'), ('ra_dec', 'S23'),
                                    ('distance', 'f8'), ('foo', 'S23')],
                             delimiter=[18, 29, 17, 24],
                             autostrip=True,
                             skip_header=self._file_properties['start_ind'] +
                             1,
                             skip_footer=(self._file_properties['line_count'] -
                                          self._file_properties['stop_ind']))

        # Fix time format
        for (i, date) in enumerate(data['date']):
            data['date'][i] = Utils.date_change(date)

        # Currently we assume HORIZONS works in UTC.
        dates = [text.decode('UTF-8') for text in data['date']]
        self._time = Time(dates, format='iso', scale='utc').tdb.jd

        ra_dec = [text.decode('UTF-8') for text in data['ra_dec']]
        xyz = SkyCoord(ra_dec,
                       distance=data['distance'],
                       unit=(u.hourangle, u.deg, u.au))
        self._xyz = xyz.cartesian
Пример #6
0
 def _get_y_value_y_err(self, phot_fmt, flux, flux_err):
     """
     just calculate magnitudes if needed, or return input otherwise
     """
     if phot_fmt == 'mag':
         return Utils.get_mag_and_err_from_flux(flux, flux_err)
     else:
         return (flux, flux_err)
Пример #7
0
    def __init__(self, s, q, n_points=10000):
        self._s = s
        self._q = q
        self._n_points = n_points

        self._n_caustics = Utils.get_n_caustics(s=self.s, q=self.q)
        self._get_phi()
        self._integrate()
        self._find_inflections_and_correct()
Пример #8
0
    def mag(self):
        """
        *np.ndarray*

        magnitude vector
        """
        if self._mag is None:
            (self._mag, self._err_mag) = Utils.get_mag_and_err_from_flux(
                flux=self.flux, err_flux=self.err_flux)
        return self._mag
Пример #9
0
    def flux(self):
        """
        *numpy.ndarray*

        Vector of the measured brightness in flux units.
        """
        if self._flux is None:
            (self._flux, self._err_flux) = Utils.get_flux_and_err_from_mag(
                mag=self.mag, err_mag=self.err_mag)
        return self._flux
Пример #10
0
 def _calculate_projected(self):
     """
     Calculate North and East directions projected on the plane of the sky.
     """
     direction = np.array(self.cartesian.xyz.value)
     if direction.shape == (3, 1):
         direction = direction[:, 0]
     north = np.array([0., 0., 1.])
     self._east_projected = Utils.vector_product_normalized(
         north, direction)
     self._north_projected = np.cross(direction, self._east_projected)
Пример #11
0
    def set_limb_coeff_u(self, bandpass, u):
        """
        Remembers limb darkening *u* coefficient for given band

        Parameters :
            bandpass: *str*
                Name of the filter.

            u: *float*
                The value of *u* coefficient.

        """
        self._gammas_for_band[bandpass] = Utils.u_to_gamma(u)
Пример #12
0
    def _magnitude_to_sat_flux(self, magnitude):
        """
        translates magnitude in reference frame (OGLE I-band in most cases)
        to satellite flux scale
        """
        if self._sat_blending_flux != 0.:
            warnings.warn(
                "self._sat_blending_flux is not 0. - not sure if this works")

        flux = Utils.get_flux_from_mag(magnitude)
        (fs, fb) = self.event.model.get_ref_fluxes()
        magnification = (flux - fb) / fs[0]
        flux_sat = self._magnification_to_sat_flux(magnification)
        return flux_sat
Пример #13
0
    def get_limb_coeff_u(self, bandpass):
        """
        Gives limb darkening *u* coefficient for given band.

        Parameters :
            bandpass: *str*
                Name of the filter.

        Returns :
            u: *float*
                The value of *u* coefficient.

        """
        gamma = self.get_limb_coeff_gamma(bandpass=bandpass)
        return Utils.gamma_to_u(gamma)
Пример #14
0
    def plot_sat_magnitudes(self, **kwargs):
        """Plot satellite data in reference magnitude system"""
        raise NotImplementedError("plot_sat_magnitudes")
        data_ref = self.event.model.data_ref
        (fs, fb) = self.event.model.get_ref_fluxes()
        n = self.n_datasets - self.n_sat

        for i in range(self.n_sat):
            times = self._sat_times[i] - 2450000.
            (fs_sat, fb_sat) = self.event.model.get_ref_fluxes(n+i)
            mags = self._sat_magnifications[i]
            flux = (mags * self._sat_flux - fb_sat) * (fs[0] / fs_sat[0]) + fb # _sat_flux is not defined anymore
            plt.plot(times, Utils.get_mag_from_flux(flux), 
                zorder=np.inf, # We want the satellite models to be at the very top. 
                **kwargs)
        self.event.model.data_ref = data_ref
Пример #15
0
    def get_model_magnitudes(self, **kwargs):
        """
        Calculate model in magnitude space

        Parameters :
            ``**kwargs``:
                see :py:func:`get_model_fluxes()`

        Returns :
            model_mag: *np.ndarray*
                The model magnitude evaluated for each datapoint.
        """
        model_flux = self.get_model_fluxes(**kwargs)
        model_mag = Utils.get_mag_from_flux(model_flux)

        return model_mag
Пример #16
0
    def _calculate(self, n_points=5000):
        """
        Solve the caustics polynomial to calculate the critical curve
        and caustic structure.

        Based on Eq. 6 Cassan 2008 modified so origin is center of
        mass and larger mass is on the left. Uses complex coordinates.
        """
        # Find number of angles so that 4*n_angles is the multiple of 4 that
        # is closest to n_points.
        n_angles = int(n_points / 4. + .5)

        # Initialize variables
        self._x = []
        self._y = []
        self._critical_curve = self.CriticalCurve()

        # Distance between primary mass and center of mass
        xcm_offset = self.q * self.s / (1. + self.q)

        # Solve for the critical curve (and caustic) in complex coordinates.
        for phi in np.linspace(0., 2. * np.pi, n_angles, endpoint=False):
            # Change the angle to a complex number
            e_iphi = self.shear_G.conjugate() + (
                1 - self.convergence_K) * complex(cos(phi), -sin(phi))

            # Coefficients of Eq. 6
            coeff_4 = 1.
            coeff_3 = -2. * self.s
            coeff_2 = Utils.complex_fsum([self.s**2, -1 / e_iphi])
            coeff_1 = 1. / e_iphi * (2. * self.s / (1. + self.q))  # The
            # additional parenthesis make it more stable numerically.
            coeff_0 = -self.s**2 * 1 / e_iphi * 1 / (1. + self.q)

            # Find roots
            coeff_list = [coeff_0, coeff_1, coeff_2, coeff_3, coeff_4]
            roots = np.polynomial.polynomial.polyroots(coeff_list)
            # Store results
            shift = -xcm_offset + self.convergence_K + self.shear_G.real
            for root in roots:
                self._critical_curve.x.append(root.real + shift)
                self._critical_curve.y.append(root.imag)

                source_plane_position = self._solve_lens_equation(root)
                self._x.append(source_plane_position.real + shift)
                self._y.append(source_plane_position.imag + self.shear_G.imag)
Пример #17
0
    def plot_sat_magnitudes(self, **kwargs):
        """
        Plot satellite model in reference magnitude system
        """
        if not self._MM:
            raise NotImplementedError('not yet coded in pixel_lensing')
        data_ref = self.event.model.data_ref
        (fs, fb) = self.event.model.get_ref_fluxes()
        n = self.n_datasets - self.n_sat
        if 'zorder' not in kwargs:
            kwargs['zorder'] = np.inf

        for i in range(self.n_sat):
            times = self._sat_times[i] - 2450000.
            flux = self._sat_magnifications[i] * fs[0] + fb
            plt.plot(times, Utils.get_mag_from_flux(flux), **kwargs)
        self.event.model.data_ref = data_ref
Пример #18
0
    def v_Earth_projected(self, full_BJD):
        """
        Earth velocity at *full_BJD* projected on the plane of sky towards
        given coordinates.

        Parameters :
            full_BJD: *float*
                Epoch for which projected velocity is requested. In most cases
                it is
                :py:attr:`~MulensModel.modelparameters.ModelParameters.t_0_par`

        Returns :
            v_Earth_perp_N: *float*
                North component of Earth's projected velocity in km/s.

            v_Earth_perp_E: *float*
                East component of Earth's projected velocity in km/s.
        """
        velocity = Utils.velocity_of_Earth(full_BJD)

        v_Earth_perp_N = np.dot(velocity, self.north_projected)
        v_Earth_perp_E = np.dot(velocity, self.east_projected)

        return (v_Earth_perp_N, v_Earth_perp_E)
Пример #19
0
    def standard_plot(self,
                      t_start,
                      t_stop,
                      ylim,
                      title=None,
                      label_list=None,
                      color_list=None,
                      line_width=1.5,
                      legend_order=None,
                      separate_residuals=False,
                      model_line_width=4.,
                      legend_kwargs=None,
                      fluxes_y_axis=None,
                      ground_model_zorder=None,
                      sat_model_zorder=None):
        """
        Make plot of the event and residuals.

        Parameters :
            XXX

            ylim: [*float*, *float*]
                A list o two values that are used to set y axis limits
                (in mag).

            fluxes_y_axis: *list* or *np.ndarray* of *floats*
                K2 fluxes which will be marked on right side of Y axis.

            ground_model_zorder: *float*
                Passed to pyplot to control if ground model is plotted
                at the top or bottom.

            sat_model_zorder: *float*
                Passed to pyplot to control if satellite model is
                plotted at the top or bottom. Defualts to *np.inf*.
        """
        if not self._MM:
            raise NotImplementedError('not yet coded in pixel_lensing')
        if (label_list is None) != (color_list is None):
            raise ValueError('wrong input in standard_plot')
        if not separate_residuals:
            grid_spec = gridspec.GridSpec(2,
                                          1,
                                          height_ratios=[5, 1],
                                          hspace=0.12)
        else:
            grid_spec = gridspec.GridSpec(3,
                                          1,
                                          height_ratios=[5, 1, 1],
                                          hspace=0.13)
        plt.figure()
        plt.subplot(grid_spec[0])
        if title is not None:
            plt.title(title)
        alphas = [0.5] * self.n_datasets
        for i in range(self.n_sat):
            alphas[-(i + 1)] = 1.

        self.event.plot_model(color='black',
                              subtract_2450000=True,
                              t_start=t_start + 2450000.,
                              t_stop=t_stop + 2450000.,
                              label="ground-based model",
                              lw=model_line_width,
                              zorder=ground_model_zorder)
        self.plot_sat_magnitudes(color='orange',
                                 lw=2,
                                 label="K2 model",
                                 zorder=sat_model_zorder)

        if color_list is not None:
            color_list_ = color_list
        else:
            if self.n_sat == 0:
                color_list_ = None
            else:
                color_list_ = ['black'] * (self.n_datasets - self.n_sat)
                color_list_ += ['red'] * self.n_sat
        zorder_list = np.arange(self.n_datasets, 0, -1)
        zorder_list[1] = self.n_datasets + 1

        self.event.plot_data(  # alpha_list=alphas,
            zorder_list=zorder_list,
            mfc='none',
            lw=line_width,
            mew=line_width,
            marker='o',
            markersize=6,
            subtract_2450000=True,
            color_list=color_list_,
            label_list=label_list)
        if ylim is not None:
            plt.ylim(ylim[0], ylim[1])
        else:
            ylim = plt.ylim()
        plt.xlim(t_start, t_stop)
        plt.gca().tick_params(top=True, direction='in')

        if legend_kwargs is None:
            legend_kwargs = dict()
        self._legend_standard_plot(legend_order, legend_kwargs, color_list,
                                   label_list, alphas)

        y_K2_max = self._magnitude_to_sat_flux(np.min(ylim))
        y_K2_min = self._magnitude_to_sat_flux(np.max(ylim))
        print("Y-axis limits:")
        print("   mag:      {:.3f} {:.3f}".format(*ylim))
        print("   K2 flux:  {:.2f} {:.2f}".format(y_K2_min, y_K2_max))

        if fluxes_y_axis is not None:
            y_color = 'red'
            y_label = r'K2 differential counts [e$^-$s$^{-1}$]'

            min_ = np.min(fluxes_y_axis)
            if min_ < y_K2_min or np.max(fluxes_y_axis) > y_K2_max:
                raise ValueError('ylim incompatible with fluxes_y_axis')

            (fs, fb) = self.event.model.get_ref_fluxes()
            mags = self._sat_flux_to_magnification(np.array(fluxes_y_axis))
            mags_fluxes = Utils.get_mag_from_flux(fb + fs[0] * mags)
            ax2 = plt.gca().twinx()
            ax2.set_ylabel(y_label).set_color(y_color)
            ax2.spines['right'].set_color(y_color)
            ax2.set_ylim(ylim[0], ylim[1])
            ax2.tick_params(axis='y', colors=y_color)
            plt.yticks(mags_fluxes.tolist(), fluxes_y_axis, color=y_color)

        plt.subplot(grid_spec[1])
        kwargs_ = dict(mfc='none', lw=line_width, mew=line_width)
        if not separate_residuals:
            self.event.plot_residuals(subtract_2450000=True, **kwargs_)
            plt.xlim(t_start, t_stop)
        else:
            plt.plot([0., 3000000.], [0., 0.], color='black')
            self.event.datasets[-1].plot(phot_fmt='mag',
                                         show_errorbars=True,
                                         subtract_2450000=True,
                                         model=self.event.model,
                                         plot_residuals=True,
                                         **kwargs_)
            plt.ylim(0.29, -0.29)  # XXX
            plt.ylabel('K2 residuals')
            plt.xlim(t_start, t_stop)
            plt.gca().tick_params(top=True, direction='in')
            plt.gca().tick_params(right=True, direction='in')

            plt.subplot(grid_spec[2])
            plt.plot([0., 3000000.], [0., 0.], color='black')
            for data in self.event.datasets[:-1]:
                data.plot(phot_fmt='mag',
                          show_errorbars=True,
                          subtract_2450000=True,
                          model=self.event.model,
                          plot_residuals=True,
                          **kwargs_)
            plt.ylabel('Residuals')
            plt.xlim(t_start, t_stop)
        plt.gca().tick_params(top=True, direction='in')
        plt.gca().tick_params(right=True, direction='in')
Пример #20
0
    def get_residuals(
            self, phot_fmt=None, source_flux=None, blend_flux=None, bad=False,
            type=None):
        """
        Calculate the residuals for each datapoint relative to the model.

        Parameters :
            phot_fmt: *str*, optional
                specify whether the residuals should be returned in
                magnitudes ('mag') or in flux ('flux'). Default is
                'mag'. If 'scaled', will return the residuals in magnitudes
                scaled to source_flux, blend_flux.

            source_flux, blend_flux: *float*
                reference source and blend fluxes for scaling the residuals

            bad: *bool*
                Default is *False*. If *True* recalculates the data
                magnification for each point to ensure that there are values
                even for bad datapoints.

            type:
                DEPRECATED, see "phot_fmt" above.

        Returns :
            residuals: *np.ndarray*
                the residuals for the corresponding dataset.

            errorbars: *np.ndarray*
                the scaled errorbars for each point. For plotting
                errorbars for the residuals.
        """
        if type is not None:
            if type == 'mag':
                warnings.warn(
                    '"mag" returns residuals in the original data flux' +
                    'system. To scale the residuals, use "scaled".')
            warnings.warn(
                'type keyword will be deprecated. Use "phot_fmt" instead.',
                FutureWarning)
            phot_fmt = type

        if bad:
            self._calculate_magnifications(bad=True)

        if phot_fmt == 'mag':
            residuals = self._dataset.mag - self.get_model_magnitudes()
            errorbars = self._dataset.err_mag
        elif phot_fmt == 'flux':
            residuals = self._dataset.flux - self.get_model_fluxes()
            errorbars = self._dataset.err_flux
        elif phot_fmt == 'scaled':
            if source_flux is None or blend_flux is None:
                raise ValueError(
                    'If phot_fmt=scaled, source_flux and blend_flux must ' +
                    'also be specified.')

            magnification = self._data_magnification
            if self._model.n_sources == 1:
                model_flux = source_flux * magnification
            else:
                model_flux = source_flux[0] * magnification[0]
                model_flux += source_flux[1] * magnification[1]
            model_flux += blend_flux
            model_mag = Utils.get_mag_from_flux(model_flux)
            (flux, err_flux) = self.scale_fluxes(source_flux, blend_flux)
            (mag, errorbars) = Utils.get_mag_and_err_from_flux(flux, err_flux)
            residuals = mag - model_mag
        else:
            raise ValueError(
                'phot_fmt must be one of "mag", "flux", or "scaled". Your ' +
                'value: {0}'.format(phot_fmt))

        return (residuals, errorbars)
Пример #21
0
    def chi2_gradient(self, parameters, fit_blending=None):
        """
        Calculate chi^2 gradient (also called Jacobian), i.e.,
        :math:`d chi^2/d parameter`.

        Parameters :
            parameters: *str* or *list*, required
                Parameters with respect to which gradient is calculated.
                Currently accepted parameters are: ``t_0``, ``u_0``, ``t_eff``,
                ``t_E``, ``pi_E_N``, and ``pi_E_E``. The parameters for
                which you request gradient must be defined in py:attr:`~model`.

            fit_blending: *boolean*, optional
                Are we fitting for blending flux? If not then blending flux is
                fixed to 0.  Default is the same as
                :py:func:`MulensModel.fit.Fit.fit_fluxes()`.

        Returns :
            gradient: *float* or *np.ndarray*
                chi^2 gradient
        """
        if not isinstance(parameters, list):
            parameters = [parameters]
        implemented = {'t_0', 't_E', 'u_0', 't_eff', 'pi_E_N', 'pi_E_E'}
        if len(set(parameters) - implemented) > 0:
            raise NotImplementedError((
                "chi^2 gradient is implemented only for {:}\nCannot work " +
                "with {:}").format(implemented, parameters))
        gradient = {param: 0 for param in parameters}

        if self.model.n_sources != 1:
            raise NotImplementedError("Sorry, chi2 for binary sources is " +
                                      "not implemented yet")
        if self.model.n_lenses != 1:
            raise NotImplementedError(
                'Event.chi2_gradient() works only ' +
                'single lens models currently')
        as_dict = self.model.parameters.as_dict()
        if 'rho' in as_dict or 't_star' in as_dict:
            raise NotImplementedError(
                'Event.chi2_gradient() is not working ' +
                'for finite source models yet')

        # Define a Fit given the model and perform linear fit for fs and fb
        self._update_data_in_model()
        self.fit = Fit(
            data=self.datasets, magnification=self.model.data_magnification)
        # For binary source cases, the above line would need to be replaced,
        # so that it uses self.model.fit.
        if fit_blending is not None:
            self.fit.fit_fluxes(fit_blending=fit_blending)
        else:
            self.fit.fit_fluxes()

        for (i, dataset) in enumerate(self.datasets):
            (data, err_data) = dataset.data_and_err_in_chi2_fmt()
            factor = data - self.fit.get_chi2_format(data=dataset)
            factor *= -2. / err_data**2
            if dataset.chi2_fmt == 'mag':
                factor *= -2.5 / (log(10.) * Utils.get_flux_from_mag(data))
            factor *= self.fit.flux_of_sources(dataset)[0]

            kwargs = {}
            if dataset.ephemerides_file is not None:
                kwargs['satellite_skycoord'] = dataset.satellite_skycoord
            trajectory = Trajectory(
                    dataset.time, self.model.parameters,
                    self.model.get_parallax(), self.coords, **kwargs)
            u_2 = trajectory.x**2 + trajectory.y**2
            u_ = np.sqrt(u_2)
            d_A_d_u = -8. / (u_2 * (u_2 + 4) * np.sqrt(u_2 + 4))
            factor *= d_A_d_u

            factor_d_x_d_u = (factor * trajectory.x / u_)[dataset.good]
            sum_d_x_d_u = np.sum(factor_d_x_d_u)
            factor_d_y_d_u = (factor * trajectory.y / u_)[dataset.good]
            sum_d_y_d_u = np.sum(factor_d_y_d_u)
            dt = dataset.time[dataset.good] - as_dict['t_0']

            # Exactly 2 out of (u_0, t_E, t_eff) must be defined and
            # gradient depends on which ones are defined.
            if 't_eff' not in as_dict:
                t_E = as_dict['t_E'].to(u.day).value
                if 't_0' in parameters:
                    gradient['t_0'] += -sum_d_x_d_u / t_E
                if 'u_0' in parameters:
                    gradient['u_0'] += sum_d_y_d_u
                if 't_E' in parameters:
                    gradient['t_E'] += np.sum(factor_d_x_d_u * -dt / t_E**2)
            elif 't_E' not in as_dict:
                t_eff = as_dict['t_eff'].to(u.day).value
                if 't_0' in parameters:
                    gradient['t_0'] += -sum_d_x_d_u * as_dict['u_0'] / t_eff
                if 'u_0' in parameters:
                    gradient['u_0'] += sum_d_y_d_u + np.sum(
                            factor_d_x_d_u * dt / t_eff)
                if 't_eff' in parameters:
                    gradient['t_eff'] += np.sum(
                            factor_d_x_d_u * -dt *
                            as_dict['u_0'] / t_eff**2)
            elif 'u_0' not in as_dict:
                t_E = as_dict['t_E'].to(u.day).value
                t_eff = as_dict['t_eff'].to(u.day).value
                if 't_0' in parameters:
                    gradient['t_0'] += -sum_d_x_d_u / t_E
                if 't_E' in parameters:
                    gradient['t_E'] += (
                            np.sum(factor_d_x_d_u * dt) -
                            sum_d_y_d_u * t_eff) / t_E**2
                if 't_eff' in parameters:
                    gradient['t_eff'] += sum_d_y_d_u / t_E
            else:
                raise KeyError(
                    'Something is wrong with ModelParameters in ' +
                    'Event.chi2_gradient():\n', as_dict)

            # Below we deal with parallax only.
            if 'pi_E_N' in parameters or 'pi_E_E' in parameters:
                parallax = {
                    'earth_orbital': False,
                    'satellite': False,
                    'topocentric': False}
                trajectory_no_piE = Trajectory(
                    dataset.time, self.model.parameters, parallax, self.coords,
                    **kwargs)
                dx = (trajectory.x - trajectory_no_piE.x)[dataset.good]
                dy = (trajectory.y - trajectory_no_piE.y)[dataset.good]
                delta_E = dx * as_dict['pi_E_E'] + dy * as_dict['pi_E_N']
                delta_N = dx * as_dict['pi_E_N'] - dy * as_dict['pi_E_E']
                det = as_dict['pi_E_N']**2 + as_dict['pi_E_E']**2

                if 'pi_E_N' in parameters:
                    gradient['pi_E_N'] += np.sum(
                        factor_d_x_d_u * delta_N + factor_d_y_d_u * delta_E)
                    gradient['pi_E_N'] /= det
                if 'pi_E_E' in parameters:
                    gradient['pi_E_E'] += np.sum(
                        factor_d_x_d_u * delta_E - factor_d_y_d_u * delta_N)
                    gradient['pi_E_E'] /= det

        if len(parameters) == 1:
            out = gradient[parameters[0]]
        else:
            out = np.array([gradient[p] for p in parameters])
        return out