Ejemplo n.º 1
0
    def from_data_source(cls, data_source, redshift, area,
                         exp_time, source_model, point_sources=False,
                         parameters=None, center=None, dist=None, 
                         cosmology=None, velocity_fields=None):
        r"""
        Initialize a :class:`~pyxsim.photon_list.PhotonList` from a yt data
        source. The redshift, collecting area, exposure time, and cosmology
        are stored in the *parameters* dictionary which is passed to the
        *source_model* function.

        Parameters
        ----------
        data_source : :class:`~yt.data_objects.data_containers.YTSelectionContainer`
            The data source from which the photons will be generated.
        redshift : float
            The cosmological redshift for the photons.
        area : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity`
            The collecting area to determine the number of photons. If units are
            not specified, it is assumed to be in cm^2.
        exp_time : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity`
            The exposure time to determine the number of photons. If units are
            not specified, it is assumed to be in seconds.
        source_model : :class:`~pyxsim.source_models.SourceModel`
            A source model used to generate the photons.
        point_sources : boolean, optional
            If True, the photons will be assumed to be generated from the exact
            positions of the cells or particles and not smeared around within
            a volume. Default: False
        parameters : dict, optional
            A dictionary of parameters to be passed for the source model to use,
            if necessary.
        center : string or array_like, optional
            The origin of the photon spatial coordinates. Accepts "c", "max", or
            a coordinate. If not specified, pyxsim attempts to use the "center"
            field parameter of the data_source.
        dist : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity`
            The angular diameter distance, used for nearby sources. This may be
            optionally supplied instead of it being determined from the
            *redshift* and given *cosmology*. If units are not specified, it is
            assumed to be in kpc. To use this, the redshift must be set to zero.
        cosmology : :class:`~yt.utilities.cosmology.Cosmology`, optional
            Cosmological information. If not supplied, we try to get the
            cosmology from the dataset. Otherwise, LCDM with the default yt 
            parameters is assumed.
        velocity_fields : list of fields
            The yt fields to use for the velocity. If not specified, the 
            following will be assumed:
            ['velocity_x', 'velocity_y', 'velocity_z'] for grid datasets
            ['particle_velocity_x', 'particle_velocity_y', 'particle_velocity_z'] for particle datasets

        Examples
        --------
        >>> thermal_model = ThermalSourceModel(apec_model, Zmet=0.3)
        >>> redshift = 0.05
        >>> area = 6000.0 # assumed here in cm**2
        >>> time = 2.0e5 # assumed here in seconds
        >>> sp = ds.sphere("c", (500., "kpc"))
        >>> my_photons = PhotonList.from_data_source(sp, redshift, area,
        ...                                          time, thermal_model)
        """
        ds = data_source.ds

        if parameters is None:
            parameters = {}
        if cosmology is None:
            if hasattr(ds, 'cosmology'):
                cosmo = ds.cosmology
            else:
                cosmo = Cosmology()
        else:
            cosmo = cosmology
        if dist is None:
            if redshift <= 0.0:
                msg = "If redshift <= 0.0, you must specify a distance to the " \
                      "source using the 'dist' argument!"
                mylog.error(msg)
                raise ValueError(msg)
            D_A = cosmo.angular_diameter_distance(0.0, redshift).in_units("Mpc")
        else:
            D_A = parse_value(dist, "kpc")
            if redshift > 0.0:
                mylog.warning("Redshift must be zero for nearby sources. "
                              "Resetting redshift to 0.0.")
                redshift = 0.0

        if isinstance(center, string_types):
            if center == "center" or center == "c":
                parameters["center"] = ds.domain_center
            elif center == "max" or center == "m":
                parameters["center"] = ds.find_max("density")[-1]
        elif iterable(center):
            if isinstance(center, YTArray):
                parameters["center"] = center.in_units("code_length")
            elif isinstance(center, tuple):
                if center[0] == "min":
                    parameters["center"] = ds.find_min(center[1])[-1]
                elif center[0] == "max":
                    parameters["center"] = ds.find_max(center[1])[-1]
                else:
                    raise RuntimeError
            else:
                parameters["center"] = ds.arr(center, "code_length")
        elif center is None:
            if hasattr(data_source, "left_edge"):
                parameters["center"] = 0.5*(data_source.left_edge+data_source.right_edge)
            else:
                parameters["center"] = data_source.get_field_parameter("center")

        parameters["fid_exp_time"] = parse_value(exp_time, "s")
        parameters["fid_area"] = parse_value(area, "cm**2")
        parameters["fid_redshift"] = redshift
        parameters["fid_d_a"] = D_A
        parameters["hubble"] = cosmo.hubble_constant
        parameters["omega_matter"] = cosmo.omega_matter
        parameters["omega_lambda"] = cosmo.omega_lambda

        if redshift > 0.0:
            mylog.info("Cosmology: h = %g, omega_matter = %g, omega_lambda = %g" %
                       (cosmo.hubble_constant, cosmo.omega_matter, cosmo.omega_lambda))
        else:
            mylog.info("Observing local source at distance %s." % D_A)

        D_A = parameters["fid_d_a"].in_cgs()
        dist_fac = 1.0/(4.*np.pi*D_A.value*D_A.value*(1.+redshift)**2)
        spectral_norm = parameters["fid_area"].v*parameters["fid_exp_time"].v*dist_fac

        source_model.setup_model(data_source, redshift, spectral_norm)

        p_fields, v_fields, w_field = determine_fields(ds,
                                                       source_model.source_type,
                                                       point_sources)

        if velocity_fields is not None:
            v_fields = velocity_fields

        if p_fields[0] == ("index", "x"):
            parameters["data_type"] = "cells"
        else:
            parameters["data_type"] = "particles"

        citer = data_source.chunks([], "io")

        photons = defaultdict(list)

        for chunk in parallel_objects(citer):

            chunk_data = source_model(chunk)

            if chunk_data is not None:
                ncells, number_of_photons, idxs, energies = chunk_data
                photons["num_photons"].append(number_of_photons)
                photons["energy"].append(energies)
                photons["pos"].append(np.array([chunk[p_fields[0]].d[idxs],
                                                chunk[p_fields[1]].d[idxs],
                                                chunk[p_fields[2]].d[idxs]]))
                photons["vel"].append(np.array([chunk[v_fields[0]].d[idxs],
                                                chunk[v_fields[1]].d[idxs],
                                                chunk[v_fields[2]].d[idxs]]))
                if w_field is None:
                    photons["dx"].append(np.zeros(ncells))
                else:
                    photons["dx"].append(chunk[w_field].d[idxs])

        source_model.cleanup_model()

        photon_units = {"pos": ds.field_info[p_fields[0]].units,
                        "vel": ds.field_info[v_fields[0]].units,
                        "energy": "keV"}
        if w_field is None:
            photon_units["dx"] = "kpc"
        else:
            photon_units["dx"] = ds.field_info[w_field].units

        concatenate_photons(ds, photons, photon_units)

        c = parameters["center"].to("kpc")

        if sum(ds.periodicity) > 0:
            # Fix photon coordinates for regions crossing a periodic boundary
            dw = ds.domain_width.to("kpc")
            le, re = find_object_bounds(data_source)
            for i in range(3):
                if ds.periodicity[i] and photons["pos"].shape[0] > 0:
                    tfl = photons["pos"][:,i] < le[i]
                    tfr = photons["pos"][:,i] > re[i]
                    photons["pos"][tfl,i] += dw[i]
                    photons["pos"][tfr,i] -= dw[i]

        # Re-center all coordinates
        if photons["pos"].shape[0] > 0:
            photons["pos"] -= c

        mylog.info("Finished generating photons.")
        mylog.info("Number of photons generated: %d" % int(np.sum(photons["num_photons"])))
        mylog.info("Number of cells with photons: %d" % photons["dx"].size)

        return cls(photons, parameters, cosmo)
Ejemplo n.º 2
0
    def from_data_source(cls,
                         data_source,
                         redshift,
                         area,
                         exp_time,
                         source_model,
                         point_sources=False,
                         parameters=None,
                         center=None,
                         dist=None,
                         cosmology=None,
                         velocity_fields=None):
        r"""
        Initialize a :class:`~pyxsim.photon_list.PhotonList` from a yt data
        source. The redshift, collecting area, exposure time, and cosmology
        are stored in the *parameters* dictionary which is passed to the
        *source_model* function.

        Parameters
        ----------
        data_source : :class:`~yt.data_objects.data_containers.YTSelectionContainer`
            The data source from which the photons will be generated.
        redshift : float
            The cosmological redshift for the photons.
        area : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity`
            The collecting area to determine the number of photons. If units are
            not specified, it is assumed to be in cm^2.
        exp_time : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity`
            The exposure time to determine the number of photons. If units are
            not specified, it is assumed to be in seconds.
        source_model : :class:`~pyxsim.source_models.SourceModel`
            A source model used to generate the photons.
        point_sources : boolean, optional
            If True, the photons will be assumed to be generated from the exact
            positions of the cells or particles and not smeared around within
            a volume. Default: False
        parameters : dict, optional
            A dictionary of parameters to be passed for the source model to use,
            if necessary.
        center : string or array_like, optional
            The origin of the photon spatial coordinates. Accepts "c", "max", or
            a coordinate. If not specified, pyxsim attempts to use the "center"
            field parameter of the data_source.
        dist : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity`
            The angular diameter distance, used for nearby sources. This may be
            optionally supplied instead of it being determined from the
            *redshift* and given *cosmology*. If units are not specified, it is
            assumed to be in kpc. To use this, the redshift must be set to zero.
        cosmology : :class:`~yt.utilities.cosmology.Cosmology`, optional
            Cosmological information. If not supplied, we try to get the
            cosmology from the dataset. Otherwise, LCDM with the default yt 
            parameters is assumed.
        velocity_fields : list of fields
            The yt fields to use for the velocity. If not specified, the 
            following will be assumed:
            ['velocity_x', 'velocity_y', 'velocity_z'] for grid datasets
            ['particle_velocity_x', 'particle_velocity_y', 'particle_velocity_z'] for particle datasets

        Examples
        --------
        >>> thermal_model = ThermalSourceModel(apec_model, Zmet=0.3)
        >>> redshift = 0.05
        >>> area = 6000.0 # assumed here in cm**2
        >>> time = 2.0e5 # assumed here in seconds
        >>> sp = ds.sphere("c", (500., "kpc"))
        >>> my_photons = PhotonList.from_data_source(sp, redshift, area,
        ...                                          time, thermal_model)
        """
        ds = data_source.ds

        if parameters is None:
            parameters = {}
        if cosmology is None:
            if hasattr(ds, 'cosmology'):
                cosmo = ds.cosmology
            else:
                cosmo = Cosmology()
        else:
            cosmo = cosmology
        if dist is None:
            if redshift <= 0.0:
                msg = "If redshift <= 0.0, you must specify a distance to the " \
                      "source using the 'dist' argument!"
                mylog.error(msg)
                raise ValueError(msg)
            D_A = cosmo.angular_diameter_distance(0.0,
                                                  redshift).in_units("Mpc")
        else:
            D_A = parse_value(dist, "kpc")
            if redshift > 0.0:
                mylog.warning("Redshift must be zero for nearby sources. "
                              "Resetting redshift to 0.0.")
                redshift = 0.0

        if isinstance(center, string_types):
            if center == "center" or center == "c":
                parameters["center"] = ds.domain_center
            elif center == "max" or center == "m":
                parameters["center"] = ds.find_max("density")[-1]
        elif iterable(center):
            if isinstance(center, YTArray):
                parameters["center"] = center.in_units("code_length")
            elif isinstance(center, tuple):
                if center[0] == "min":
                    parameters["center"] = ds.find_min(center[1])[-1]
                elif center[0] == "max":
                    parameters["center"] = ds.find_max(center[1])[-1]
                else:
                    raise RuntimeError
            else:
                parameters["center"] = ds.arr(center, "code_length")
        elif center is None:
            if hasattr(data_source, "left_edge"):
                parameters["center"] = 0.5 * (data_source.left_edge +
                                              data_source.right_edge)
            else:
                parameters["center"] = data_source.get_field_parameter(
                    "center")

        parameters["fid_exp_time"] = parse_value(exp_time, "s")
        parameters["fid_area"] = parse_value(area, "cm**2")
        parameters["fid_redshift"] = redshift
        parameters["fid_d_a"] = D_A
        parameters["hubble"] = cosmo.hubble_constant
        parameters["omega_matter"] = cosmo.omega_matter
        parameters["omega_lambda"] = cosmo.omega_lambda

        if redshift > 0.0:
            mylog.info(
                "Cosmology: h = %g, omega_matter = %g, omega_lambda = %g" %
                (cosmo.hubble_constant, cosmo.omega_matter,
                 cosmo.omega_lambda))
        else:
            mylog.info("Observing local source at distance %s." % D_A)

        D_A = parameters["fid_d_a"].in_cgs()
        dist_fac = 1.0 / (4. * np.pi * D_A.value * D_A.value *
                          (1. + redshift)**2)
        spectral_norm = parameters["fid_area"].v * parameters[
            "fid_exp_time"].v * dist_fac

        source_model.setup_model(data_source, redshift, spectral_norm)

        p_fields, v_fields, w_field = determine_fields(
            ds, source_model.source_type, point_sources)

        if velocity_fields is not None:
            v_fields = velocity_fields

        if p_fields[0] == ("index", "x"):
            parameters["data_type"] = "cells"
        else:
            parameters["data_type"] = "particles"

        citer = data_source.chunks([], "io")

        photons = defaultdict(list)

        for chunk in parallel_objects(citer):

            chunk_data = source_model(chunk)

            if chunk_data is not None:
                ncells, number_of_photons, idxs, energies = chunk_data
                photons["num_photons"].append(number_of_photons)
                photons["energy"].append(energies)
                photons["pos"].append(
                    np.array([
                        chunk[p_fields[0]].d[idxs], chunk[p_fields[1]].d[idxs],
                        chunk[p_fields[2]].d[idxs]
                    ]))
                photons["vel"].append(
                    np.array([
                        chunk[v_fields[0]].d[idxs], chunk[v_fields[1]].d[idxs],
                        chunk[v_fields[2]].d[idxs]
                    ]))
                if w_field is None:
                    photons["dx"].append(np.zeros(ncells))
                else:
                    photons["dx"].append(chunk[w_field].d[idxs])

        source_model.cleanup_model()

        photon_units = {
            "pos": ds.field_info[p_fields[0]].units,
            "vel": ds.field_info[v_fields[0]].units,
            "energy": "keV"
        }
        if w_field is None:
            photon_units["dx"] = "kpc"
        else:
            photon_units["dx"] = ds.field_info[w_field].units

        concatenate_photons(ds, photons, photon_units)

        c = parameters["center"].to("kpc")

        if sum(ds.periodicity) > 0:
            # Fix photon coordinates for regions crossing a periodic boundary
            dw = ds.domain_width.to("kpc")
            le, re = find_object_bounds(data_source)
            for i in range(3):
                if ds.periodicity[i] and photons["pos"].shape[0] > 0:
                    tfl = photons["pos"][:, i] < le[i]
                    tfr = photons["pos"][:, i] > re[i]
                    photons["pos"][tfl, i] += dw[i]
                    photons["pos"][tfr, i] -= dw[i]

        # Re-center all coordinates
        if photons["pos"].shape[0] > 0:
            photons["pos"] -= c

        mylog.info("Finished generating photons.")
        mylog.info("Number of photons generated: %d" %
                   int(np.sum(photons["num_photons"])))
        mylog.info("Number of cells with photons: %d" % photons["dx"].size)

        return cls(photons, parameters, cosmo)
Ejemplo n.º 3
0
    def from_data_source(cls,
                         data_source,
                         redshift,
                         area,
                         exp_time,
                         source_model,
                         parameters=None,
                         center=None,
                         dist=None,
                         cosmology=None,
                         velocity_fields=None):
        r"""
        Initialize a :class:`~pyxsim.photon_list.PhotonList` from a yt data source.
        The redshift, collecting area, exposure time, and cosmology are stored in the
        *parameters* dictionary which is passed to the *source_model* function.

        Parameters
        ----------
        data_source : :class:`~yt.data_objects.data_containers.YTSelectionContainer`
            The data source from which the photons will be generated.
        redshift : float
            The cosmological redshift for the photons.
        area : float, (value, unit) tuple, or :class:`~yt.units.yt_array.YTQuantity`.
            The collecting area to determine the number of photons. If units are
            not specified, it is assumed to be in cm^2.
        exp_time : float, (value, unit) tuple, or :class:`~yt.units.yt_array.YTQuantity`.
            The exposure time to determine the number of photons. If units are
            not specified, it is assumed to be in seconds.
        source_model : :class:`~pyxsim.source_models.SourceModel`
            A source model used to generate the photons.
        parameters : dict, optional
            A dictionary of parameters to be passed for the source model to use, if necessary.
        center : string or array_like, optional
            The origin of the photon spatial coordinates. Accepts "c", "max", or a coordinate.
            If not specified, pyxsim attempts to use the "center" field parameter of the data_source.
        dist : float, (value, unit) tuple, or :class:`~yt.units.yt_array.YTQuantity`, optional
            The angular diameter distance, used for nearby sources. This may be
            optionally supplied instead of it being determined from the *redshift*
            and given *cosmology*. If units are not specified, it is assumed to be
            in Mpc. To use this, the redshift must be set to zero.
        cosmology : :class:`~yt.utilities.cosmology.Cosmology`, optional
            Cosmological information. If not supplied, we try to get
            the cosmology from the dataset. Otherwise, LCDM with
            the default yt parameters is assumed.
        velocity_fields : list of fields
            The yt fields to use for the velocity. If not specified, the following will
            be assumed:
            ['velocity_x', 'velocity_y', 'velocity_z'] for grid datasets
            ['particle_velocity_x', 'particle_velocity_y', 'particle_velocity_z'] for particle datasets

        Examples
        --------
        >>> thermal_model = ThermalSourceModel(apec_model, Zmet=0.3)
        >>> redshift = 0.05
        >>> area = 6000.0 # assumed here in cm**2
        >>> time = 2.0e5 # assumed here in seconds
        >>> sp = ds.sphere("c", (500., "kpc"))
        >>> my_photons = PhotonList.from_data_source(sp, redshift, area,
        ...                                          time, thermal_model)
        """
        ds = data_source.ds

        if parameters is None:
            parameters = {}
        if cosmology is None:
            if hasattr(ds, 'cosmology'):
                cosmo = ds.cosmology
            else:
                cosmo = Cosmology()
        else:
            cosmo = cosmology
        mylog.info(
            "Cosmology: h = %g, omega_matter = %g, omega_lambda = %g" %
            (cosmo.hubble_constant, cosmo.omega_matter, cosmo.omega_lambda))
        if dist is None:
            if redshift <= 0.0:
                msg = "If redshift <= 0.0, you must specify a distance to the source using the 'dist' argument!"
                mylog.error(msg)
                raise ValueError(msg)
            D_A = cosmo.angular_diameter_distance(0.0,
                                                  redshift).in_units("Mpc")
        else:
            D_A = parse_value(dist, "Mpc")
            if redshift > 0.0:
                mylog.warning(
                    "Redshift must be zero for nearby sources. Resetting redshift to 0.0."
                )
                redshift = 0.0

        if center == "center" or center == "c":
            parameters["center"] = ds.domain_center
        elif center == "max" or center == "m":
            parameters["center"] = ds.find_max("density")[-1]
        elif iterable(center):
            if isinstance(center, YTArray):
                parameters["center"] = center.in_units("code_length")
            elif isinstance(center, tuple):
                if center[0] == "min":
                    parameters["center"] = ds.find_min(center[1])[-1]
                elif center[0] == "max":
                    parameters["center"] = ds.find_max(center[1])[-1]
                else:
                    raise RuntimeError
            else:
                parameters["center"] = ds.arr(center, "code_length")
        elif center is None:
            parameters["center"] = data_source.get_field_parameter("center")

        parameters["FiducialExposureTime"] = parse_value(exp_time, "s")
        parameters["FiducialArea"] = parse_value(area, "cm**2")
        parameters["FiducialRedshift"] = redshift
        parameters["FiducialAngularDiameterDistance"] = D_A
        parameters["HubbleConstant"] = cosmo.hubble_constant
        parameters["OmegaMatter"] = cosmo.omega_matter
        parameters["OmegaLambda"] = cosmo.omega_lambda

        D_A = parameters["FiducialAngularDiameterDistance"].in_cgs()
        dist_fac = 1.0 / (4. * np.pi * D_A.value * D_A.value *
                          (1. + redshift)**2)
        spectral_norm = parameters["FiducialArea"].v * parameters[
            "FiducialExposureTime"].v * dist_fac

        source_model.setup_model(data_source, redshift, spectral_norm)

        p_fields, v_fields, w_field = determine_fields(
            ds, source_model.source_type)

        if velocity_fields is not None:
            v_fields = velocity_fields

        if p_fields[0] == ("index", "x"):
            parameters["DataType"] = "cells"
        else:
            parameters["DataType"] = "particles"

        if hasattr(data_source, "left_edge"):
            # Region or grid
            le = data_source.left_edge
            re = data_source.right_edge
        elif hasattr(data_source,
                     "radius") and not hasattr(data_source, "height"):
            # Sphere
            le = -data_source.radius + data_source.center
            re = data_source.radius + data_source.center
        else:
            # Compute rough boundaries of the object
            # DOES NOT WORK for objects straddling periodic
            # boundaries yet
            if sum(ds.periodicity) > 0:
                mylog.warning("You are using a region that is not currently "
                              "supported for straddling periodic boundaries. "
                              "Check to make sure that this is not the case.")
            le = ds.arr(np.zeros(3), "code_length")
            re = ds.arr(np.zeros(3), "code_length")
            for i, ax in enumerate(p_fields):
                le[i], re[i] = data_source.quantities.extrema(ax)

        dds_min = get_smallest_dds(ds, parameters["DataType"])
        le = np.rint((le - ds.domain_left_edge) /
                     dds_min) * dds_min + ds.domain_left_edge
        re = ds.domain_right_edge - np.rint(
            (ds.domain_right_edge - re) / dds_min) * dds_min
        width = re - le
        parameters["Dimension"] = np.rint(width / dds_min).astype("int")
        parameters["Width"] = parameters["Dimension"] * dds_min.in_units("kpc")

        citer = data_source.chunks([], "io")

        photons = defaultdict(list)

        for chunk in parallel_objects(citer):

            chunk_data = source_model(chunk)

            if chunk_data is not None:
                number_of_photons, idxs, energies = chunk_data
                photons["NumberOfPhotons"].append(number_of_photons)
                photons["Energy"].append(ds.arr(energies, "keV"))
                photons["x"].append(chunk[p_fields[0]][idxs].in_units("kpc"))
                photons["y"].append(chunk[p_fields[1]][idxs].in_units("kpc"))
                photons["z"].append(chunk[p_fields[2]][idxs].in_units("kpc"))
                photons["vx"].append(chunk[v_fields[0]][idxs].in_units("km/s"))
                photons["vy"].append(chunk[v_fields[1]][idxs].in_units("km/s"))
                photons["vz"].append(chunk[v_fields[2]][idxs].in_units("km/s"))
                if w_field is None:
                    photons["dx"].append(ds.arr(np.zeros(len(idxs)), "kpc"))
                else:
                    photons["dx"].append(chunk[w_field][idxs].in_units("kpc"))

        source_model.cleanup_model()

        concatenate_photons(photons)

        # Translate photon coordinates to the source center
        # Fix photon coordinates for regions crossing a periodic boundary
        dw = ds.domain_width.to("kpc")
        for i, ax in enumerate("xyz"):
            if ds.periodicity[i] and len(photons[ax]) > 0:
                tfl = photons[ax] < le[i].to('kpc')
                tfr = photons[ax] > re[i].to('kpc')
                photons[ax][tfl] += dw[i]
                photons[ax][tfr] -= dw[i]
            photons[ax] -= parameters["center"][i].in_units("kpc")

        mylog.info("Finished generating photons.")
        mylog.info("Number of photons generated: %d" %
                   int(np.sum(photons["NumberOfPhotons"])))
        mylog.info("Number of cells with photons: %d" % len(photons["x"]))

        return cls(photons, parameters, cosmo)