diff --git a/CHANGES.rst b/CHANGES.rst index 71714cdef..a85ddb83c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,8 @@ - Renamed old implementation of `conditional_abunmatch` to `conditional_abunmatch_bin_based` +- Added new bin-free implementation of `conditional_abunmatch`. + 0.6 (2017-12-15) ---------------- diff --git a/docs/_static/cam_example_assembias_clf.png b/docs/_static/cam_example_assembias_clf.png new file mode 100644 index 000000000..980609e26 Binary files /dev/null and b/docs/_static/cam_example_assembias_clf.png differ diff --git a/docs/_static/cam_example_bulge_disk_ratio.png b/docs/_static/cam_example_bulge_disk_ratio.png new file mode 100644 index 000000000..dc64d8a42 Binary files /dev/null and b/docs/_static/cam_example_bulge_disk_ratio.png differ diff --git a/docs/_static/cam_example_complex_sfr.png b/docs/_static/cam_example_complex_sfr.png new file mode 100644 index 000000000..d187d3587 Binary files /dev/null and b/docs/_static/cam_example_complex_sfr.png differ diff --git a/docs/_static/cam_example_complex_sfr_dmdt_correlation.png b/docs/_static/cam_example_complex_sfr_dmdt_correlation.png new file mode 100644 index 000000000..9249af4de Binary files /dev/null and b/docs/_static/cam_example_complex_sfr_dmdt_correlation.png differ diff --git a/docs/_static/cam_example_complex_sfr_recovery.png b/docs/_static/cam_example_complex_sfr_recovery.png new file mode 100644 index 000000000..b5362171c Binary files /dev/null and b/docs/_static/cam_example_complex_sfr_recovery.png differ diff --git a/docs/function_usage/utility_functions.rst b/docs/function_usage/utility_functions.rst index a6c54025c..2aeece071 100644 --- a/docs/function_usage/utility_functions.rst +++ b/docs/function_usage/utility_functions.rst @@ -58,3 +58,10 @@ Probabilistic binning .. autosummary:: fuzzy_digitize + +Estimating two-dimensional PDFs +=============================================================== + +.. autosummary:: + + sliding_conditional_percentile diff --git a/docs/notebooks/cam_modeling/cam_complex_sfr_tutorial.ipynb b/docs/notebooks/cam_modeling/cam_complex_sfr_tutorial.ipynb new file mode 100644 index 000000000..caba366e3 --- /dev/null +++ b/docs/notebooks/cam_modeling/cam_complex_sfr_tutorial.ipynb @@ -0,0 +1,362 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np \n", + "from matplotlib import pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Generate powerlaw distributed stellar mass" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from scipy.stats import powerlaw\n", + "\n", + "ngals_tot = int(1e5)\n", + "slope = 2\n", + "log_mstar = 3*(1-powerlaw.rvs(slope, size=ngals_tot)) + 9\n", + "galaxy_mstar = 10**log_mstar\n", + "fig, ax = plt.subplots(1, 1)\n", + "\n", + "__=ax.hist(log_mstar, bins=100)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Model the quenched fraction vs. $M_{\\ast}$ with an exponential" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "log_mstar_crit = 10.5\n", + "uran = np.random.rand(ngals_tot)\n", + "is_quenched = uran < 1-np.exp(-(log_mstar/log_mstar_crit)**5)\n", + "\n", + "num_quenched = np.count_nonzero(is_quenched)\n", + "num_star_forming = ngals_tot - num_quenched" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Generate quenched sequence SFR using exponential power " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from scipy.stats import exponpow\n", + "\n", + "b = 1.5\n", + "log_ssfr_quenched = exponpow.rvs(b, loc=-12, scale=1, size=num_quenched)\n", + "ssfr_quenched = 10**log_ssfr_quenched\n", + "\n", + "fig, ax = plt.subplots(1, 1)\n", + "\n", + "__=ax.hist(log_ssfr_quenched, bins=100)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Generate star-forming sequence SFR using log-normal" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "log_ssfr_main_sequence = np.random.normal(loc=-10, scale=0.35, size=num_star_forming)\n", + "ssfr_star_forming = 10**log_ssfr_main_sequence\n", + "\n", + "fig, ax = plt.subplots(1, 1)\n", + "\n", + "__=ax.hist(log_ssfr_main_sequence, bins=100)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Build composite galaxy population" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "galaxy_ssfr = np.zeros_like(galaxy_mstar)\n", + "galaxy_ssfr[is_quenched] = ssfr_quenched\n", + "galaxy_ssfr[~is_quenched] = ssfr_star_forming\n", + "\n", + "logsm_low, logsm_high = 9.5, 10\n", + "mask1 = (log_mstar >= logsm_low) & (log_mstar < logsm_high)\n", + "\n", + "logsm_low, logsm_high = 10.75, 11.25\n", + "mask2 = (log_mstar >= logsm_low) & (log_mstar < logsm_high)\n", + "\n", + "fig, ax = plt.subplots(1, 1)\n", + "\n", + "from scipy.stats import gaussian_kde\n", + "kde1 = gaussian_kde(np.log10(galaxy_ssfr[mask1]))\n", + "kde2 = gaussian_kde(np.log10(galaxy_ssfr[mask2]))\n", + "\n", + "x = np.linspace(-12.5, -8, 1000)\n", + "pdf1 = kde1.evaluate(x)\n", + "pdf2 = kde2.evaluate(x)\n", + "\n", + "__=ax.fill(x, pdf1, alpha=0.8, label=r'$9.5 < \\log M_{\\ast} < 10$')\n", + "__=ax.fill(x, pdf2, alpha=0.8, label=r'$10.75 < \\log M_{\\ast} < 11.25$')\n", + "\n", + "xlim = ax.set_xlim(-12.5, -8.5)\n", + "ylim = ax.set_ylim(ymin=0)\n", + "legend = ax.legend()\n", + "\n", + "xlabel = ax.set_xlabel(r'$\\log{\\rm sSFR}$')\n", + "ylabel = ax.set_ylabel(r'${\\rm PDF}$')\n", + "\n", + "figname = 'cam_example_complex_sfr.png'\n", + "fig.savefig(figname, bbox_extra_artists=[xlabel, ylabel], bbox_inches='tight')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Build a baseline model of stellar mass" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['halo_vmax_firstacc', 'halo_dmvir_dt_tdyn', 'halo_macc', 'halo_scale_factor', 'halo_vmax_mpeak', 'halo_m_pe_behroozi', 'halo_xoff', 'halo_spin', 'halo_scale_factor_firstacc', 'halo_c_to_a', 'halo_mvir_firstacc', 'halo_scale_factor_last_mm', 'halo_scale_factor_mpeak', 'halo_pid', 'halo_m500c', 'halo_id', 'halo_halfmass_scale_factor', 'halo_upid', 'halo_t_by_u', 'halo_rvir', 'halo_vpeak', 'halo_dmvir_dt_100myr', 'halo_mpeak', 'halo_m_pe_diemer', 'halo_jx', 'halo_jy', 'halo_jz', 'halo_m2500c', 'halo_mvir', 'halo_voff', 'halo_axisA_z', 'halo_axisA_x', 'halo_axisA_y', 'halo_y', 'halo_b_to_a', 'halo_x', 'halo_z', 'halo_m200b', 'halo_vacc', 'halo_scale_factor_lastacc', 'halo_vmax', 'halo_m200c', 'halo_vx', 'halo_vy', 'halo_vz', 'halo_dmvir_dt_inst', 'halo_rs', 'halo_nfw_conc', 'halo_hostid', 'halo_mvir_host_halo', 'stellar_mass']\n" + ] + } + ], + "source": [ + "from halotools.sim_manager import CachedHaloCatalog\n", + "halocat = CachedHaloCatalog()\n", + "\n", + "from halotools.empirical_models import Moster13SmHm\n", + "model = Moster13SmHm()\n", + "\n", + "halocat.halo_table['stellar_mass'] = model.mc_stellar_mass(\n", + " prim_haloprop=halocat.halo_table['halo_mpeak'], redshift=0)\n", + "\n", + "print(halocat.halo_table.keys())" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "from halotools.empirical_models import conditional_abunmatch\n", + "\n", + "x = halocat.halo_table['stellar_mass']\n", + "y = halocat.halo_table['halo_dmvir_dt_100myr']\n", + "\n", + "x2 = galaxy_mstar\n", + "y2 = np.log10(galaxy_ssfr)\n", + "\n", + "nwin = 201\n", + "\n", + "halocat.halo_table['log_ssfr'] = conditional_abunmatch(x, y, x2, y2, nwin)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "logsm_low, logsm_high = 9.5, 10\n", + "mask1_gals = (log_mstar >= logsm_low) & (log_mstar < logsm_high)\n", + "mask1_halos = (np.log10(halocat.halo_table['stellar_mass']) >= logsm_low)\n", + "mask1_halos *= (np.log10(halocat.halo_table['stellar_mass']) < logsm_high)\n", + "\n", + "logsm_low, logsm_high = 10.75, 11.25\n", + "mask2_gals = (log_mstar >= logsm_low) & (log_mstar < logsm_high)\n", + "mask2_halos = (np.log10(halocat.halo_table['stellar_mass']) >= logsm_low)\n", + "mask2_halos *= (np.log10(halocat.halo_table['stellar_mass']) < logsm_high)\n", + "\n", + "\n", + "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))\n", + "\n", + "__=ax1.hist(np.log10(galaxy_ssfr[mask1_gals]), bins=75, normed=True, alpha=0.8, \n", + " label=r'${\\rm observed\\ galaxies}$')\n", + "__=ax1.hist(halocat.halo_table['log_ssfr'][mask1_halos], bins=75, normed=True, alpha=0.8,\n", + " label=r'${\\rm model\\ galaxies}$')\n", + "__=ax2.hist(np.log10(galaxy_ssfr[mask2_gals]), bins=75, normed=True, alpha=0.8, \n", + " label=r'${\\rm observed\\ galaxies}$')\n", + "__=ax2.hist(halocat.halo_table['log_ssfr'][mask2_halos], bins=75, normed=True, alpha=0.8,\n", + " label=r'${\\rm model\\ galaxies}$')\n", + "\n", + "legend1 = ax1.legend()\n", + "legend2 = ax2.legend()\n", + "\n", + "ylim1 = ax1.set_ylim(0, 1)\n", + "xlim1 = ax1.set_xlim(-12.1, -9)\n", + "ylim2 = ax2.set_ylim(0, 1)\n", + "xlim2 = ax2.set_xlim(-12.1, -9)\n", + "\n", + "xlabel1 = ax1.set_xlabel(r'$\\log{\\rm sSFR}$')\n", + "xlabel2 = ax2.set_xlabel(r'$\\log{\\rm sSFR}$')\n", + "ylabel1 = ax1.set_ylabel(r'${\\rm PDF}$')\n", + "title1 = ax1.set_title(r'$9.5 < \\log M_{\\ast} < 10$')\n", + "title2 = ax2.set_title(r'$10.75 < \\log M_{\\ast} < 11.25$')\n", + "\n", + "figname = 'cam_example_complex_sfr_recovery.png'\n", + "fig.savefig(figname, bbox_extra_artists=[xlabel1, ylabel1], bbox_inches='tight')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda root]", + "language": "python", + "name": "conda-root-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.14" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/notebooks/cam_modeling/cam_decorated_clf.ipynb b/docs/notebooks/cam_modeling/cam_decorated_clf.ipynb new file mode 100644 index 000000000..4b698aea0 --- /dev/null +++ b/docs/notebooks/cam_modeling/cam_decorated_clf.ipynb @@ -0,0 +1,223 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np \n", + "from matplotlib import pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Calculate $V_{\\rm max}$ percentile for host halos" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "from halotools.sim_manager import CachedHaloCatalog\n", + "halocat = CachedHaloCatalog(simname='bolplanck')\n", + "host_halos = halocat.halo_table[halocat.halo_table['halo_upid']==-1]\n", + "\n", + "from halotools.utils import sliding_conditional_percentile\n", + "x = host_halos['halo_mvir']\n", + "y = host_halos['halo_vmax']\n", + "nwin = 301\n", + "host_halos['vmax_percentile'] = sliding_conditional_percentile(x, y, nwin)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Calculate median luminosity for every galaxy" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "from halotools.empirical_models import Cacciato09Cens\n", + "model = Cacciato09Cens()\n", + "host_halos['median_luminosity'] = model.median_prim_galprop(\n", + " prim_haloprop=host_halos['halo_mvir'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Generate Monte Carlo log-normal luminosity realization using CAM" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "from scipy.stats import norm\n", + "\n", + "host_halos['luminosity'] = 10**norm.isf(1-host_halos['vmax_percentile'], \n", + " loc=np.log10(host_halos['median_luminosity']),\n", + " scale=0.2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot the results" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "xmin, xmax = 10**10.75, 10**13.5\n", + "\n", + "fig, ax = plt.subplots(1, 1)\n", + "__=ax.loglog()\n", + "\n", + "__=ax.scatter(host_halos['halo_mvir'][::100], \n", + " host_halos['luminosity'][::100], s=0.1, color='gray', label='')\n", + "\n", + "from scipy.stats import binned_statistic\n", + "log_mass_bins = np.linspace(np.log10(xmin), np.log10(xmax), 25)\n", + "mass_mids = 10**(0.5*(log_mass_bins[:-1] + log_mass_bins[1:]))\n", + "\n", + "median_lum, __, __ = binned_statistic(\n", + " host_halos['halo_mvir'], host_halos['luminosity'], bins=10**log_mass_bins, \n", + " statistic='median')\n", + "\n", + "high_vmax_mask = host_halos['vmax_percentile'] > 0.8\n", + "median_lum_high_vmax, __, __ = binned_statistic(\n", + " host_halos['halo_mvir'][high_vmax_mask], host_halos['luminosity'][high_vmax_mask], \n", + " bins=10**log_mass_bins, statistic='median')\n", + "\n", + "mid_high_vmax_mask = host_halos['vmax_percentile'] < 0.8\n", + "mid_high_vmax_mask *= host_halos['vmax_percentile'] > 0.6\n", + "median_lum_mid_high_vmax, __, __ = binned_statistic(\n", + " host_halos['halo_mvir'][mid_high_vmax_mask], host_halos['luminosity'][mid_high_vmax_mask], \n", + " bins=10**log_mass_bins, statistic='median')\n", + "\n", + "mid_low_vmax_mask = host_halos['vmax_percentile'] < 0.4\n", + "mid_low_vmax_mask *= host_halos['vmax_percentile'] > 0.2\n", + "median_lum_mid_low_vmax, __, __ = binned_statistic(\n", + " host_halos['halo_mvir'][mid_low_vmax_mask], host_halos['luminosity'][mid_low_vmax_mask], \n", + " bins=10**log_mass_bins, statistic='median')\n", + "\n", + "\n", + "low_vmax_mask = host_halos['vmax_percentile'] < 0.2\n", + "median_lum_low_vmax, __, __ = binned_statistic(\n", + " host_halos['halo_mvir'][low_vmax_mask], host_halos['luminosity'][low_vmax_mask], \n", + " bins=10**log_mass_bins, statistic='median')\n", + "\n", + "__=ax.plot(mass_mids, median_lum_high_vmax, color='red', \n", + " label=r'$V_{\\rm max}\\ {\\rm percentile} > 0.8$')\n", + "__=ax.plot(mass_mids, median_lum_mid_high_vmax, color='orange',\n", + " label=r'$V_{\\rm max}\\ {\\rm percentile} \\approx 0.7$')\n", + "__=ax.plot(mass_mids, median_lum, color='k', \n", + " label=r'$V_{\\rm max}\\ {\\rm percentile} \\approx 0.5$')\n", + "__=ax.plot(mass_mids, median_lum_mid_low_vmax, color='blue', \n", + " label=r'$V_{\\rm max}\\ {\\rm percentile} \\approx 0.3$')\n", + "__=ax.plot(mass_mids, median_lum_low_vmax, color='purple',\n", + " label=r'$V_{\\rm max}\\ {\\rm percentile} < 0.2$')\n", + "\n", + "xlim = ax.set_xlim(xmin, xmax/1.2)\n", + "ylim = ax.set_ylim(10**7.5, 10**11)\n", + "legend = ax.legend()\n", + "\n", + "xlabel = ax.set_xlabel(r'${\\rm M_{vir}/M_{\\odot}}$')\n", + "ylabel = ax.set_ylabel(r'${\\rm L/L_{\\odot}}$')\n", + "title = ax.set_title(r'${\\rm CLF\\ with\\ assembly\\ bias}$')\n", + "\n", + "figname = 'cam_example_assembias_clf.png'\n", + "fig.savefig(figname, bbox_extra_artists=[xlabel, ylabel], bbox_inches='tight')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda root]", + "language": "python", + "name": "conda-root-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.14" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/notebooks/cam_modeling/cam_disk_bulge_ratios_demo.ipynb b/docs/notebooks/cam_modeling/cam_disk_bulge_ratios_demo.ipynb new file mode 100644 index 000000000..818e598e9 --- /dev/null +++ b/docs/notebooks/cam_modeling/cam_disk_bulge_ratios_demo.ipynb @@ -0,0 +1,252 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np \n", + "from matplotlib import pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Build a baseline model of stellar mass" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "from halotools.sim_manager import CachedHaloCatalog\n", + "halocat = CachedHaloCatalog()\n", + "\n", + "from halotools.empirical_models import Moster13SmHm\n", + "model = Moster13SmHm()\n", + "\n", + "halocat.halo_table['stellar_mass'] = model.mc_stellar_mass(\n", + " prim_haloprop=halocat.halo_table['halo_mpeak'], redshift=0)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define a simple model for $M_{\\ast}-$dependence of ${\\rm B/T}$ power law index" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [], + "source": [ + "def powerlaw_index(log_mstar):\n", + " abscissa = [9, 10, 11.5]\n", + " ordinates = [3, 2, 1]\n", + " return np.interp(log_mstar, abscissa, ordinates)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Calculate the spin-percentile" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [], + "source": [ + "from halotools.utils import sliding_conditional_percentile\n", + "\n", + "x = halocat.halo_table['stellar_mass']\n", + "y = halocat.halo_table['halo_spin']\n", + "nwin = 201\n", + "halocat.halo_table['spin_percentile'] = sliding_conditional_percentile(x, y, nwin)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Use CAM to generate a Monte Carlo realization of ${\\rm B/T}$" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [], + "source": [ + "a = powerlaw_index(np.log10(halocat.halo_table['stellar_mass']))\n", + "u = halocat.halo_table['spin_percentile']\n", + "halocat.halo_table['bulge_to_total_ratio'] = 1 - powerlaw.isf(1 - u, a)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot the results" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(1, 1)\n", + "\n", + "mask1 = halocat.halo_table['stellar_mass'] < 10**9.5\n", + "mask2 = halocat.halo_table['stellar_mass'] > 10**10.5\n", + "\n", + "__=ax.hist(halocat.halo_table['bulge_to_total_ratio'][mask1], \n", + " bins=100, alpha=0.8, normed=True, color='blue',\n", + " label=r'$\\log M_{\\ast} < 9.5$')\n", + "__=ax.hist(halocat.halo_table['bulge_to_total_ratio'][mask2], \n", + " bins=100, alpha=0.8, normed=True, color='red',\n", + " label=r'$\\log M_{\\ast} > 10.5$')\n", + "\n", + "legend = ax.legend()\n", + "\n", + "xlabel = ax.set_xlabel(r'${\\rm B/T}$')\n", + "ylabel = ax.set_ylabel(r'${\\rm PDF}$')\n", + "title = ax.set_title(r'${\\rm Bulge}$-${\\rm to}$-${\\rm Total\\ M_{\\ast}\\ Ratio}$')\n", + "\n", + "figname = 'cam_example_bt_distributions.png'\n", + "fig.savefig(figname, bbox_extra_artists=[xlabel, ylabel], bbox_inches='tight')" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "xmin, xmax = 9, 11.25\n", + "\n", + "fig, ax = plt.subplots(1, 1)\n", + "xscale = ax.set_xscale('log')\n", + "\n", + "from scipy.stats import binned_statistic\n", + "log_mass_bins = np.linspace(xmin, xmax, 25)\n", + "mass_mids = 10**(0.5*(log_mass_bins[:-1] + log_mass_bins[1:]))\n", + "\n", + "median_bt, __, __ = binned_statistic(\n", + " halocat.halo_table['stellar_mass'], halocat.halo_table['bulge_to_total_ratio'], \n", + " bins=10**log_mass_bins, statistic='median')\n", + "std_bt, __, __ = binned_statistic(\n", + " halocat.halo_table['stellar_mass'], halocat.halo_table['bulge_to_total_ratio'], \n", + " bins=10**log_mass_bins, statistic=np.std)\n", + "\n", + "low_spin_mask = halocat.halo_table['spin_percentile'] < 0.5\n", + "median_bt_low_spin, __, __ = binned_statistic(\n", + " halocat.halo_table['stellar_mass'][low_spin_mask], \n", + " halocat.halo_table['bulge_to_total_ratio'][low_spin_mask], \n", + " bins=10**log_mass_bins, statistic='median')\n", + "std_bt_low_spin, __, __ = binned_statistic(\n", + " halocat.halo_table['stellar_mass'][low_spin_mask], \n", + " halocat.halo_table['bulge_to_total_ratio'][low_spin_mask], \n", + " bins=10**log_mass_bins, statistic=np.std)\n", + "\n", + "high_spin_mask = halocat.halo_table['spin_percentile'] > 0.5\n", + "median_bt_high_spin, __, __ = binned_statistic(\n", + " halocat.halo_table['stellar_mass'][high_spin_mask], \n", + " halocat.halo_table['bulge_to_total_ratio'][high_spin_mask], \n", + " bins=10**log_mass_bins, statistic='median')\n", + "std_bt_high_spin, __, __ = binned_statistic(\n", + " halocat.halo_table['stellar_mass'][high_spin_mask], \n", + " halocat.halo_table['bulge_to_total_ratio'][high_spin_mask], \n", + " bins=10**log_mass_bins, statistic=np.std)\n", + "\n", + "y1 = median_bt_low_spin - std_bt_low_spin\n", + "y2 = median_bt_low_spin + std_bt_low_spin\n", + "__=ax.fill_between(mass_mids, y1, y2, alpha=0.8, color='red', \n", + " label=r'${\\rm low\\ spin\\ halos}$')\n", + "\n", + "y1 = median_bt_high_spin - std_bt_high_spin\n", + "y2 = median_bt_high_spin + std_bt_high_spin\n", + "__=ax.fill_between(mass_mids, y1, y2, alpha=0.8, color='blue',\n", + " label=r'${\\rm high\\ spin\\ halos}$')\n", + "\n", + "ylim = ax.set_ylim(0, 1)\n", + "\n", + "legend = ax.legend(loc='upper left')\n", + "\n", + "xlabel = ax.set_xlabel(r'${\\rm M_{\\ast}/M_{\\odot}}$')\n", + "ylabel = ax.set_ylabel(r'$\\langle{\\rm B/T}\\rangle$')\n", + "\n", + "figname = 'cam_example_bulge_disk_ratio.png'\n", + "fig.savefig(figname, bbox_extra_artists=[xlabel, ylabel], bbox_inches='tight')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda root]", + "language": "python", + "name": "conda-root-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.14" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/quickstart_and_tutorials/index.rst b/docs/quickstart_and_tutorials/index.rst index 19ebdc66d..a36d39545 100644 --- a/docs/quickstart_and_tutorials/index.rst +++ b/docs/quickstart_and_tutorials/index.rst @@ -49,7 +49,7 @@ Galaxy-Halo Modeling tutorials/model_building/preloaded_models/index tutorials/model_building/index ../source_notes/empirical_models/factories/index - ../quickstart_and_tutorials/tutorials/model_building/cam_tutorial + ../quickstart_and_tutorials/tutorials/model_building/cam_tutorial_pages/cam_tutorial Galaxy/Halo Catalog Analysis --------------------------------------------------------- diff --git a/docs/quickstart_and_tutorials/tutorials/model_building/cam_tutorial.rst b/docs/quickstart_and_tutorials/tutorials/model_building/cam_tutorial.rst deleted file mode 100644 index b1fb41bfa..000000000 --- a/docs/quickstart_and_tutorials/tutorials/model_building/cam_tutorial.rst +++ /dev/null @@ -1,102 +0,0 @@ -:orphan: - -.. _cam_tutorial: - -********************************************************************** -Tutorial on Conditional Abundance Matching -********************************************************************** - -Conditional Abundance Matching (CAM) is a technique that you can use to -model a variety of correlations between galaxy and halo properties, -such as the dependence of galaxy quenching upon both halo mass and -halo formation time. This tutorial explains CAM by applying -the technique to a few different problems. -Each of the following worked examples are independent from one another, -and illustrate the range of applications of the technique. - - -Basic Idea -================= - -Forward-modeling the galaxy-halo connection requires specifying -some statistical distribution of the galaxy property being modeled, -so that Monte Carlo realizations can be drawn from the distribution. -The most convenient distribution to use for this purpose is the cumulative -distribution function (CDF), :math:`{\rm CDF}(x) = {\rm Prob}(< x).` -Once the CDF is specified, you only need to generate -a realization of a random uniform distribution and pass those draws to the -CDF inverse, :math:`{\rm CDF}^{-1}(p),` which evaluates to the variable -:math:`x` being painted on the model galaxies. - -CAM introduces correlations between the -galaxy property :math:`x` and some halo property :math:`h,` -without changing :math:`{\rm CDF}(x)`. Rather than evaluating :math:`{\rm CDF}^{-1}(p)` -with random uniform variables, -instead you evaluate with :math:`p = {\rm CDF}(h) = {\rm Prob}(< h),` -introducing a monotonic correlation between :math:`x` and :math:`h`. - -The function `~halotools.empirical_models.noisy_percentile` can be used to -add controllable levels of noise to :math:`p = {\rm CDF}(h).` -This allows you to control the correlation coefficient -between :math:`x` and :math:`h,` -always exactly preserving the 1-point statistics of the output distribution. - - -The "Conditional" part of CAM is that this technique naturally generalizes to -introduce a galaxy property correlation while holding some other property fixed. -Age Matching in `Hearin and Watson 2013 `_ -is an example of this: the distribution :math:`{\rm Prob}(`_ model satellite -quenching with a simple analytical function :math:`{\rm Prob(\ quenched}\ \vert\ M_{\rm host})`, -where :math:`M_{\rm host}` is the dark matter mass of the satellite's parent halo. -For a standard implementation of this model, you can draw from a random uniform number generator -of the unit interval, and evaluate whether those draws are above or below :math:`{\rm Prob(\ quenched)}`. - -Alternatively, to implement CAM you would compute -:math:`p={\rm Prob(< r/R_{vir}}\ \vert\ M_{\rm host})` for each simulated subhalo, -and then evaluate whether each :math:`p` -is above or below :math:`{\rm Prob(\ quenched}\ \vert\ M_{\rm host})`. -This technique lets you generate a series of mocks with exactly the same -:math:`{\rm Prob(\ quenched}\ \vert\ M_{\rm host})`, -but with tunable levels of quenching gradient, ranging from zero gradient -to the statistical extrema. -The `~halotools.utils.sliding_conditional_percentile` function can be used to -calculate :math:`p={\rm Prob(< r/R_{vir}}\ \vert\ M_{\rm host}).` - - -The plot below demonstrates three different mock catalogs made with CAM in this way. -The left hand plot shows how the quenched fraction of satellites varies -with intra-halo position. The right hand plot confirms that all three mocks have -statistically indistinguishable "halo mass quenching", even though their gradients -are very different. - -.. image:: /_static/quenching_gradient_models.png - -The next plot compares the 3d clustering between these models. - -.. image:: /_static/quenching_gradient_model_clustering.png - -For implementation details, the code producing these plots -can be found in the following Jupyter notebook: - - **halotools/docs/notebooks/galcat_analysis/intermediate_examples/quenching_gradient_tutorial.ipynb** - - - - - diff --git a/docs/quickstart_and_tutorials/tutorials/model_building/cam_tutorial_pages/cam_complex_sfr.rst b/docs/quickstart_and_tutorials/tutorials/model_building/cam_tutorial_pages/cam_complex_sfr.rst new file mode 100644 index 000000000..eee8c1f95 --- /dev/null +++ b/docs/quickstart_and_tutorials/tutorials/model_building/cam_tutorial_pages/cam_complex_sfr.rst @@ -0,0 +1,95 @@ +.. _cam_complex_sfr: + +Modeling Complex Star-Formation Rates +============================================== + +In this example, we will show how to use Conditional Abundance Matching to model +a correlation between the mass accretion rate of a halo and the specific +star-formation rate of the galaxy living in the halo. +The code used to generate these results can be found here: + + **halotools/docs/notebooks/cam_modeling/cam_complex_sfr_tutorial.ipynb** + +Observed star-formation rate distribution +------------------------------------------ + +We will work with a distribution of star-formation +rates that would be difficult to model analytically, but that is well-sampled +by some observed galaxy population. The particular form of this distribution +is not important for this tutorial, since our CAM application will directly +use the "observed" population to define the distribution that we recover. + +.. image:: /_static/cam_example_complex_sfr.png + +The plot above shows the specific star-formation rates of the +toy galaxy distribution we have created for demonstration purposes. +Briefly, there are separate distributions for quenched and star-forming galaxies. +For the quenched galaxies, we model sSFR using an exponential power law; +for star-forming galaxies, we use a log-normal; +implementation details can be found in the notebook. + + +Modeling sSFR with CAM +------------------------------------------ + +We will start out by painting stellar mass onto subhalos +in the Bolshoi simulation, which we do using +the stellar-to-halo mass relation from Moster et al 2013. + +.. code:: python + + from halotools.sim_manager import CachedHaloCatalog + halocat = CachedHaloCatalog() + + from halotools.empirical_models import Moster13SmHm + model = Moster13SmHm() + + halocat.halo_table['stellar_mass'] = model.mc_stellar_mass( + prim_haloprop=halocat.halo_table['halo_mpeak'], redshift=0) + + +Algorithm description +~~~~~~~~~~~~~~~~~~~~~~ + +We will now use CAM to paint star-formation rates onto these model galaxies. +The way the algorithm works is as follows. For every model galaxy, +we find the observed galaxy with the closest stellar mass. +We set up a window of ~200 observed galaxies bracketing this matching galaxy; +this window defines :math:`{\rm Prob(< sSFR | M_{\ast})}`, which allows us to +calculate the rank-order sSFR-percentile for each galaxy in the window. +Similarly, we set up a window of ~200 model galaxies; this window +defines :math:`{\rm Prob(< dM_{vir}/dt | M_{\ast})}`, which allows us to +calculate the rank-order accretion-rate-percentile of our model galaxy, +:math:`r_1`. Then we simply search the observed window for the +observed galaxy whose rank-order sSFR-percentile equals +:math:`r_1`, and map its sSFR value onto our model galaxy. +We perform that calculation for every model galaxy with the following syntax: + +.. code:: python + + from halotools.empirical_models import conditional_abunmatch + x = halocat.halo_table['stellar_mass'] + y = halocat.halo_table['halo_dmvir_dt_100myr'] + x2 = galaxy_mstar + y2 = np.log10(galaxy_ssfr) + nwin = 201 + halocat.halo_table['log_ssfr'] = conditional_abunmatch(x, y, x2, y2, nwin) + + +Results +~~~~~~~~~~~~~~~~~~~~~~ + +Now let's inspect the results of our calculation. First we show that the +distribution specific star-formation rates of our model galaxies +matches the observed distribution across the range of stellar mass: + + +.. image:: /_static/cam_example_complex_sfr_recovery.png + +Next we can see that these sSFR values are tightly correlated +with halo accretion rate at fixed stellar mass: + +.. image:: /_static/cam_example_complex_sfr_dmdt_correlation.png + + + diff --git a/docs/quickstart_and_tutorials/tutorials/model_building/cam_tutorial_pages/cam_decorated_clf.rst b/docs/quickstart_and_tutorials/tutorials/model_building/cam_tutorial_pages/cam_decorated_clf.rst new file mode 100644 index 000000000..d6b2e9266 --- /dev/null +++ b/docs/quickstart_and_tutorials/tutorials/model_building/cam_tutorial_pages/cam_decorated_clf.rst @@ -0,0 +1,92 @@ +.. _cam_decorated_clf: + + +Modeling Central Galaxy Luminosity with Assembly Bias +========================================================================== + +In this example, we will show how to use Conditional Abundance Matching to +map central galaxy luminosity onto halos in a way that simultaneously correlates +with halo :math:`M_{\rm vir}` and halo :math:`V_{\rm max}`. +The code used to generate these results can be found here: + + **halotools/docs/notebooks/cam_modeling/cam_decorated_clf.ipynb** + + +Baseline mass-to-light model +------------------------------------------ + +The approach we will demonstrate in this tutorial is very similar to the ordinary +Conditional Luminosity Function model (CLF) of central galaxy luminosity. +In the CLF, a parameterized form is chosen for the median luminosity +of central galaxies as a function of host halo mass. The code below +shows how to calculate this median luminosity for every host halo in the Bolshoi simulation: + +.. code:: python + + from halotools.sim_manager import CachedHaloCatalog + halocat = CachedHaloCatalog(simname='bolplanck') + host_halos = halocat.halo_table[halocat.halo_table['halo_upid']==-1] + from halotools.empirical_models import Cacciato09Cens + model = Cacciato09Cens() + host_halos['median_luminosity'] = model.median_prim_galprop( + prim_haloprop=host_halos['halo_mvir']) + +To generate a Monte Carlo realization of the model, +one typically assumes that luminosities are distributed +as a log-normal distribution centered about this median relation. +While there is already a convenience function +`~halotools.empirical_models.Cacciato09Cens.mc_prim_galprop` for the +`~halotools.empirical_models.Cacciato09Cens` class that handles this, +it is straightforward to do this yourself +using the `~scipy.stats.norm` function in `scipy.stats`. +You just need to generate uniform random numbers and pass the result to the +`scipy.stats.norm.isf` function: + +.. code:: python + + from scipy.stats import norm + loc = np.log10(host_halos['median_luminosity']) + uran = np.random.rand(len(host_halos)) + host_halos['luminosity'] = 10**norm.isf(1-uran, loc=loc, scale=0.2) + +The *isf* function analytically evaluates the inverse CDF of the normal distribution, +and so this Monte Carlo method is based on inverse transformation sampling. +It is probably more common to use `numpy.random.normal` for this purpose, +but doing things with `scipy.stats.norm` will make it easier +to see how CAM works in the next section. + + +Correlating scatter in luminosity with halo :math:`V_{\rm max}` +---------------------------------------------------------------- + +As described in :ref:`cam_basic_idea`, we can generalize the inverse transformation sampling +technique so that the modeled variable is not purely stochastic, but is instead +correlated with some other variable. In this example, we will choose to +correlate the scatter with :math:`V_{\rm max}`. To do so, we need to calculate +:math:`{\rm Prob}(`_ model satellite +quenching with a simple analytical function :math:`{\rm Prob(\ quenched}\ \vert\ M_{\rm host})`, +where :math:`M_{\rm host}` is the dark matter mass of the satellite's parent halo. +For a standard implementation of this model, you can draw from a random uniform number generator +of the unit interval, and evaluate whether those draws are above or below :math:`{\rm Prob(\ quenched)}`. + +Alternatively, to implement CAM you would compute +:math:`p={\rm Prob(< r/R_{vir}}\ \vert\ M_{\rm host})` for each simulated subhalo, +and then evaluate whether each :math:`p` +is above or below :math:`{\rm Prob(\ quenched}\ \vert\ M_{\rm host})`. +This technique lets you generate a series of mocks with exactly the same +:math:`{\rm Prob(\ quenched}\ \vert\ M_{\rm host})`, +but with tunable levels of quenching gradient, ranging from zero gradient +to the statistical extrema. +The `~halotools.utils.sliding_conditional_percentile` function can be used to +calculate :math:`p={\rm Prob(< r/R_{vir}}\ \vert\ M_{\rm host}).` + + +The plot below demonstrates three different mock catalogs made with CAM in this way. +The left hand plot shows how the quenched fraction of satellites varies +with intra-halo position. The right hand plot confirms that all three mocks have +statistically indistinguishable "halo mass quenching", even though their gradients +are very different. + +.. image:: /_static/quenching_gradient_models.png + +The next plot compares the 3d clustering between these models. + +.. image:: /_static/quenching_gradient_model_clustering.png + +For implementation details, the code producing these plots +can be found in the following Jupyter notebook: + + **halotools/docs/notebooks/galcat_analysis/intermediate_examples/quenching_gradient_tutorial.ipynb** + + + + + diff --git a/docs/quickstart_and_tutorials/tutorials/model_building/cam_tutorial_pages/cam_tutorial.rst b/docs/quickstart_and_tutorials/tutorials/model_building/cam_tutorial_pages/cam_tutorial.rst new file mode 100644 index 000000000..b0303794f --- /dev/null +++ b/docs/quickstart_and_tutorials/tutorials/model_building/cam_tutorial_pages/cam_tutorial.rst @@ -0,0 +1,151 @@ +.. _cam_tutorial: + +********************************************************************** +Tutorial on Conditional Abundance Matching +********************************************************************** + +Conditional Abundance Matching (CAM) is a technique that you can use to +model a variety of correlations between galaxy and halo properties, +such as the dependence of galaxy quenching upon both halo mass and +halo formation time, or the dependence of galaxy disk size upon halo spin. +This tutorial explains CAM by applying the technique to a few different problems. + + +.. _cam_basic_idea: + +Basic Idea Behind CAM +====================== + +CAM is designed to answer questions of the following form: +*does halo property A correlate with galaxy property B?* +The Halotools approach to answering such questions is via forward modeling: +a mock universe is created in which the A--B correlation exists; +comparing the mock universe to the real one allows you to evaluate the +success of the A--B correlation hypothesis. + +Forward-modeling the galaxy-halo connection requires specifying +some statistical distribution of the galaxy property being modeled, +so that Monte Carlo realizations can be drawn from the distribution. +CAM uses the most ubiquitous approach to generating Monte Carlo realizations, +*inverse transformation sampling,* in which the statistical distribution +is specified in terms of the cumulative distribution function (CDF), +:math:`{\rm CDF}(z) \equiv {\rm Prob}(< z).` +Briefly, the way this work is that once you specify the CDF, +you only need to generate a realization of a random uniform distribution, +and pass the values of that realization to the CDF inverse, :math:`{\rm CDF}^{-1}(p),` +which evaluates to the variable :math:`z` being painted on the model galaxies. +See the `Transformation of Probability tutorial `_ +for pedagogical derivations associated with inverse transformation sampling, +and the `~halotools.utils.monte_carlo_from_cdf_lookup` function +for a convenient one-liner syntax. + +In ordinary applications of inverse transformation sampling, +the use of a random uniform variable guarantees +that the output variables :math:`z` will be distributed according to +:math:`{\rm Prob}(z),` and that each individual :math:`z` will be purely stochastic. +CAM generalizes this common technique so that :math:`{\rm Prob}(z)` +is still recovered exactly, and moreover :math:`z` exhibits residual correlations +with some other variable, :math:`h`. Operationally, the way this works is that +rather than evaluating :math:`{\rm CDF}^{-1}(p)` with random uniform variables, +instead you evaluate with :math:`p = {\rm CDF}(h) = {\rm Prob}(< h),` +introducing a monotonic correlation between :math:`z` and :math:`h`. +In most applications, :math:`h` is some halo property like mass accretion rate, +and :math:`z` is some galaxy property like star-formation rate. +In this way, the galaxy property you paint on to your halos will +trace the distribution :math:`{\rm Prob}(z)`, such that above-average +values of :math:`z` will be painted onto halos with above average values of +:math:`h`, and conversely. + +Finally, the "Conditional" part of CAM is that this technique naturally generalizes to +introduce a galaxy property correlation while holding some other property fixed. +For example, at fixed stellar mass, it is natural to hypothesize that +star-forming galaxies live in halos that are rapidly accreting mass, +and that quiescent galaxies live in halos that have already built up most of their mass. +In this kind of CAM application, we have: +:math:`{\rm Prob}(z)\rightarrow{\rm Prob}(`_ +for a fast and well-written code written by Yao-Yuan Mao +that provides a python wrapper around the deconvolution kernel written by Peter Behroozi. diff --git a/docs/whats_new_history/whats_new_0.7.rst b/docs/whats_new_history/whats_new_0.7.rst index 6a2430b66..21c862b97 100644 --- a/docs/whats_new_history/whats_new_0.7.rst +++ b/docs/whats_new_history/whats_new_0.7.rst @@ -38,3 +38,10 @@ New Mock Observables Inertia Tensor calculation ------------------------------- The pairwise calculation `~halotools.mock_observables.inertia_tensor_per_object` computes the inertia tensor of a mass distribution surrounding each point in a sample of galaxies or halos. + +API Changes +=========== + +* The old implementation of the `~halotools.empirical_models.conditional_abunmatch` function has been renamed to be `~halotools.empirical_models.conditional_abunmatch_bin_based`. + +* There is an entirely distinct, bin-free implementation of Conditional Abundance Matching that now bears the name `~halotools.empirical_models.conditional_abunmatch`. diff --git a/halotools/empirical_models/abunmatch/__init__.py b/halotools/empirical_models/abunmatch/__init__.py index 248ecf7c3..deb1bac75 100644 --- a/halotools/empirical_models/abunmatch/__init__.py +++ b/halotools/empirical_models/abunmatch/__init__.py @@ -1,2 +1,4 @@ from .conditional_abunmatch_bin_based import * from .noisy_percentile import * +from .engines import cython_bin_free_cam_kernel +from .bin_free_cam import conditional_abunmatch diff --git a/halotools/empirical_models/abunmatch/bin_free_cam.py b/halotools/empirical_models/abunmatch/bin_free_cam.py new file mode 100644 index 000000000..3449675c7 --- /dev/null +++ b/halotools/empirical_models/abunmatch/bin_free_cam.py @@ -0,0 +1,128 @@ +""" +""" +import numpy as np +from ...utils import unsorting_indices +from ...utils.conditional_percentile import _check_xyn_bounds, rank_order_function +from .engines import cython_bin_free_cam_kernel +from .tests.naive_python_cam import sample2_window_indices + + +def conditional_abunmatch(x, y, x2, y2, nwin, add_subgrid_noise=True, + assume_x_is_sorted=False, assume_x2_is_sorted=False): + r""" + Given a set of input points with primary property `x` and secondary property `y`, + use conditional abundance matching to map new values `ynew` onto the input points + such that :math:`P(>> npts1, npts2 = 5000, 3000 + >>> x = np.linspace(0, 1, npts1) + >>> y = np.random.uniform(-1, 1, npts1) + >>> x2 = np.linspace(0.5, 0.6, npts2) + >>> y2 = np.random.uniform(-5, 3, npts2) + >>> nwin = 51 + >>> new_y = conditional_abunmatch(x, y, x2, y2, nwin) + + Notes + ----- + The ``nwin`` argument controls the precision of the calculation, + and also the performance. For estimations of Prob(< y | x) with sub-percent accuracy, + values of ``window_length`` must exceed 100. Values more tha a few hundred are + likely overkill when using the (recommended) sub-grid noise option. + + See :ref:`cam_tutorial` demonstrating how to use this + function in galaxy-halo modeling with several worked examples. + + With the release of Halotools v0.7, this function replaced a previous function + of the same name. The old function is now called + `~halotools.empirical_models.conditional_abunmatch_bin_based`. + + """ + x, y, nwin = _check_xyn_bounds(x, y, nwin) + x2, y2, nwin = _check_xyn_bounds(x2, y2, nwin) + nhalfwin = int(nwin/2) + npts1 = len(x) + + if assume_x_is_sorted: + x_sorted = x + y_sorted = y + else: + idx_x_sorted = np.argsort(x) + x_sorted = x[idx_x_sorted] + y_sorted = y[idx_x_sorted] + + if assume_x2_is_sorted: + x2_sorted = x2 + y2_sorted = y2 + else: + idx_x2_sorted = np.argsort(x2) + x2_sorted = x2[idx_x2_sorted] + y2_sorted = y2[idx_x2_sorted] + + i2_matched = np.searchsorted(x2_sorted, x_sorted).astype('i4') + + result = np.array(cython_bin_free_cam_kernel( + y_sorted, y2_sorted, i2_matched, nwin, int(add_subgrid_noise))) + + # Finish the leftmost points in pure python + iw = 0 + for ix1 in range(0, nhalfwin): + iy2_low, iy2_high = sample2_window_indices(ix1, x_sorted, x2_sorted, nwin) + leftmost_sorted_window_y2 = np.sort(y2_sorted[iy2_low:iy2_high]) + leftmost_window_ranks = rank_order_function(y_sorted[:nwin]) + result[ix1] = leftmost_sorted_window_y2[leftmost_window_ranks[iw]] + iw += 1 + + # Finish the rightmost points in pure python + iw = nhalfwin + 1 + for ix1 in range(npts1-nhalfwin, npts1): + iy2_low, iy2_high = sample2_window_indices(ix1, x_sorted, x2_sorted, nwin) + rightmost_sorted_window_y2 = np.sort(y2_sorted[iy2_low:iy2_high]) + rightmost_window_ranks = rank_order_function(y_sorted[-nwin:]) + result[ix1] = rightmost_sorted_window_y2[rightmost_window_ranks[iw]] + iw += 1 + + if assume_x_is_sorted: + return result + else: + return result[unsorting_indices(idx_x_sorted)] diff --git a/halotools/empirical_models/abunmatch/conditional_abunmatch_bin_based.py b/halotools/empirical_models/abunmatch/conditional_abunmatch_bin_based.py index 52fdfe0d1..cd33f3f4e 100644 --- a/halotools/empirical_models/abunmatch/conditional_abunmatch_bin_based.py +++ b/halotools/empirical_models/abunmatch/conditional_abunmatch_bin_based.py @@ -93,6 +93,14 @@ def conditional_abunmatch_bin_based(haloprop, galprop, sigma=0., npts_lookup_tab see the `deconvolution abundance matching code `_ written by Yao-Yuan Mao. + With the release of Halotools v0.7, this function had its name changed. + The previous name given to this function was "conditional_abunmatch". + Halotools v0.7 has a new function `~halotools.empirical_models.conditional_abunmatch` + with this name that largely replaces the functionality here. + See :ref:`cam_tutorial` demonstrating how to use the new + function in galaxy-halo modeling with several worked examples. + + """ haloprop_table, galprop_table = its.build_cdf_lookup(galprop, npts_lookup_table) haloprop_percentiles = its.rank_order_percentile(haloprop) diff --git a/halotools/empirical_models/abunmatch/engines/__init__.py b/halotools/empirical_models/abunmatch/engines/__init__.py new file mode 100644 index 000000000..1d58f11e1 --- /dev/null +++ b/halotools/empirical_models/abunmatch/engines/__init__.py @@ -0,0 +1 @@ +from .bin_free_cam_kernel import cython_bin_free_cam_kernel diff --git a/halotools/empirical_models/abunmatch/engines/bin_free_cam_kernel.pyx b/halotools/empirical_models/abunmatch/engines/bin_free_cam_kernel.pyx new file mode 100644 index 000000000..1411b6ffd --- /dev/null +++ b/halotools/empirical_models/abunmatch/engines/bin_free_cam_kernel.pyx @@ -0,0 +1,277 @@ +""" +""" +from libc.stdlib cimport rand, RAND_MAX +from libc.math cimport floor +import numpy as np +cimport cython +from ....utils import unsorting_indices + +__all__ = ('cython_bin_free_cam_kernel', ) + + +cdef double random_uniform(): + cdef double r = rand() + return r / RAND_MAX + + +@cython.boundscheck(False) +@cython.nonecheck(False) +@cython.wraparound(False) +cdef int _bisect_left_kernel(double[:] arr, double value): + """ Return the index where to insert ``value`` in list ``arr`` of length ``n``, + assuming ``arr`` is sorted. + + This function is equivalent to the bisect_left function implemented in the + python standard libary bisect. + """ + cdef int n = arr.shape[0] + cdef int ifirst_subarr = 0 + cdef int ilast_subarr = n + cdef int imid_subarr + + while ilast_subarr-ifirst_subarr >= 2: + imid_subarr = (ifirst_subarr + ilast_subarr)/2 + if value > arr[imid_subarr]: + ifirst_subarr = imid_subarr + else: + ilast_subarr = imid_subarr + if value > arr[ifirst_subarr]: + return ilast_subarr + else: + return ifirst_subarr + + +@cython.boundscheck(False) +@cython.nonecheck(False) +@cython.wraparound(False) +cdef void _insert_first_pop_last_kernel(int* arr, int value_in1, int n): + """ Insert the element ``value_in1`` into the input array and pop out the last element + """ + cdef int i + for i in range(n-2, -1, -1): + arr[i+1] = arr[i] + arr[0] = value_in1 + + +@cython.boundscheck(False) +@cython.nonecheck(False) +@cython.wraparound(False) +cdef int _correspondence_indices_shift(int idx_in1, int idx_out1, int idx): + """ Update the correspondence indices array + """ + cdef int shift = 0 + if idx_in1 < idx_out1: + if idx_in1 <= idx < idx_out1: + shift = 1 + elif idx_in1 > idx_out1: + if idx_out1 < idx <= idx_in1: + shift = -1 + return shift + + +@cython.boundscheck(False) +@cython.nonecheck(False) +@cython.wraparound(False) +cdef void _insert_pop_kernel(double* arr, int idx_in1, int idx_out1, double value_in1): + """ Pop out the value stored in index ``idx_out1`` of array ``arr``, + and insert ``value_in1`` at index ``idx_in1`` of the final array. + """ + cdef int i + + if idx_in1 <= idx_out1: + for i in range(idx_out1-1, idx_in1-1, -1): + arr[i+1] = arr[i] + else: + for i in range(idx_out1, idx_in1): + arr[i] = arr[i+1] + arr[idx_in1] = value_in1 + + +def setup_initial_indices(iy2, nwin, npts2): + """ Search an array of length npts2 to identify + the unique window of width nwin centered iy2. Care is taken to deal with + the left and right edges. For elements iy2 < nwin/2, the first nwin elements + of the array are used; for elements iy2 > npts2-nwin/2, + the last nwin elements are used. + """ + nhalfwin = int(nwin/2) + init_iy2_low = iy2 - nhalfwin + init_iy2_high = init_iy2_low+nwin + + if init_iy2_low < 0: + init_iy2_low = 0 + init_iy2_high = init_iy2_low + nwin + iy2 = init_iy2_low + nhalfwin + elif init_iy2_high > npts2 - nhalfwin: + init_iy2_high = npts2 + init_iy2_low = init_iy2_high - nwin + iy2 = init_iy2_low + nhalfwin + + return iy2, init_iy2_low, init_iy2_high + + +@cython.boundscheck(False) +@cython.nonecheck(False) +@cython.wraparound(False) +def cython_bin_free_cam_kernel(double[:] y1, double[:] y2, int[:] i2_match, int nwin, + int add_subgrid_noise=0): + """ Kernel underlying the bin-free implementation of conditional abundance matching. + For the i^th element of y1, we define a window of length `nwin` surrounding the + point y1[i], and another window surrounding y2[i2_match[i]]. We calculate the + rank-order of y1[i] within the first window. Then we find the point in the second + window with a matching rank-order and use this value as ynew[i]. + The algorithm has been implemented so that the windows are only sorted once + at the beginning, and as the windows slide along the arrays with increasing i, + elements are popped in and popped out so preserve the sorted order. + + When using add_subgrid_noise, the algorithm differs slightly. Rather than setting + ynew[i] to the value in the second window with the matching rank-order, + instead we assign a random uniform number from the range spanned by + (y2_window[rank-1],y2_window[rank+1]). This eliminates discreteness effects + and comes at no loss of precision since the PDF is not known to an accuracy + better than 1/nwin. + + The arrays named sorted_cdf_values store the two windows. + The arrays correspondence_indx are responsible for the bookkeeping involved in + maintaining a sorted order as elements are popped in and popped out. + The way this works is that as the window slides along from left to right, + the leftmost value is the one that should be popped out + (that is, the y value corresponding to the smallest x in the window). + However, the position of this element within sorted_cdf_values can be anywhere. + So the correspondence_indx array is used to keep track of the x-ordering + of the values within the windows. + In particular, element 0 in correspondence_indx stores the position of the + most-recently added element to sorted_cdf_values; + element nwin-1 in correspondence_indx stores the position of the + element of sorted_cdf_values that will be the next one popped out; + element nwin/2 stores the position of the middle of the window within + sorted_cdf_values. Since the position within sorted_cdf_values is the rank, + then sorted_cdf_values[correspondence_indx[nwin/2]] stores the value of ynew. + + """ + cdef int nhalfwin = int(nwin/2) + cdef int npts1 = y1.shape[0] + cdef int npts2 = y2.shape[0] + + cdef int iy1, i, j, idx, idx2, iy2_match + cdef int idx_in1, idx_out1, idx_in2, idx_out2 + cdef double value_in1, value_out1, value_in2, value_out2 + + cdef double[:] y1_new = np.zeros(npts1, dtype='f8') - 1 + cdef int rank1, rank2 + + # Set up initial window arrays for y1 + cdf_values1 = np.copy(y1[:nwin]) + idx_sorted_cdf_values1 = np.argsort(cdf_values1) + cdef double[:] sorted_cdf_values1 = np.ascontiguousarray( + cdf_values1[idx_sorted_cdf_values1], dtype='f8') + cdef int[:] correspondence_indx1 = np.ascontiguousarray( + unsorting_indices(idx_sorted_cdf_values1)[::-1], dtype='i4') + + # Set up initial window arrays for y2 + cdef int iy2_init = i2_match[nhalfwin] + _iy2, init_iy2_low, init_iy2_high = setup_initial_indices( + iy2_init, nwin, npts2) + cdef int iy2 = _iy2 + cdef int iy2_max = npts2 - nhalfwin - 1 + + cdef int low_rank, high_rank + cdef double low_cdf, high_cdf + + # Ensure that any bookkeeping error in setting up the window + # is caught by an exception rather than a bus error + msg = ("Bookkeeping error internal to cython_bin_free_cam_kernel\n" + "init_iy2_low = {0}, init_iy2_high = {1}, nwin = {2}") + assert init_iy2_high - init_iy2_low == nwin, msg.format( + init_iy2_low, init_iy2_high, nwin) + + cdf_values2 = np.copy(y2[init_iy2_low:init_iy2_high]) + + idx_sorted_cdf_values2 = np.argsort(cdf_values2) + + cdef double[:] sorted_cdf_values2 = np.ascontiguousarray( + cdf_values2[idx_sorted_cdf_values2], dtype='f8') + cdef int[:] correspondence_indx2 = np.ascontiguousarray( + unsorting_indices(idx_sorted_cdf_values2)[::-1], dtype='i4') + + # Loop over elements of the first array, ignoring the first and last nwin/2 points, + # which will be treated separately by the python wrapper. + for iy1 in range(nhalfwin, npts1-nhalfwin): + + rank1 = correspondence_indx1[nhalfwin] + iy2_match = i2_match[iy1] + + # Stop updating the second window once we reach npts2-nwin/2 + if iy2_match > iy2_max: + iy2_match = iy2_max + + if iy2 > iy2_max: + iy2 = iy2_max + else: + # Continue to slide the window along the second array + # until we find the matching point, updating the window with each iteration + while iy2 < iy2_match: + + # Find the value coming in and the value coming out + value_in2 = y2[iy2 + nhalfwin + 1] + idx_out2 = correspondence_indx2[nwin-1] + value_out2 = sorted_cdf_values2[idx_out2] + + # Find the position where we will insert the new point into the second window + idx_in2 = _bisect_left_kernel(sorted_cdf_values2, value_in2) + if value_in2 > value_out2: + idx_in2 -= 1 + + # Update the correspondence array + _insert_first_pop_last_kernel(&correspondence_indx2[0], idx_in2, nwin) + for j in range(1, nwin): + idx2 = correspondence_indx2[j] + correspondence_indx2[j] += _correspondence_indices_shift( + idx_in2, idx_out2, idx2) + + # Update the CDF window + _insert_pop_kernel(&sorted_cdf_values2[0], idx_in2, idx_out2, value_in2) + + iy2 += 1 + + # The array sorted_cdf_values2 is now centered on the correct point of y2 + # We have already calculated the rank-order of the point iy1, rank1 + # So we either directly map sorted_cdf_values2[rank1] to ynew, + # or alternatively we randomly draw a value between + # sorted_cdf_values2[rank1-1] and sorted_cdf_values2[rank1+1] + if add_subgrid_noise == 0: + y1_new[iy1] = sorted_cdf_values2[rank1] + else: + low_rank = rank1 - 1 + high_rank = rank1 + 1 + if low_rank < 0: + low_rank = 0 + elif high_rank >= nwin: + high_rank = nwin - 1 + low_cdf = sorted_cdf_values2[low_rank] + high_cdf = sorted_cdf_values2[high_rank] + y1_new[iy1] = low_cdf + (high_cdf-low_cdf)*random_uniform() + + # Move on to the next value in y1 + + # Find the value coming in and the value coming out + value_in1 = y1[iy1 + nhalfwin + 1] + idx_out1 = correspondence_indx1[nwin-1] + value_out1 = sorted_cdf_values1[idx_out1] + + # Find the position where we will insert the new point into the first window + idx_in1 = _bisect_left_kernel(sorted_cdf_values1, value_in1) + if value_in1 > value_out1: + idx_in1 -= 1 + + # Update the correspondence array + _insert_first_pop_last_kernel(&correspondence_indx1[0], idx_in1, nwin) + for i in range(1, nwin): + idx = correspondence_indx1[i] + correspondence_indx1[i] += _correspondence_indices_shift( + idx_in1, idx_out1, idx) + + # Update the CDF window + _insert_pop_kernel(&sorted_cdf_values1[0], idx_in1, idx_out1, value_in1) + + return y1_new diff --git a/halotools/empirical_models/abunmatch/engines/setup_package.py b/halotools/empirical_models/abunmatch/engines/setup_package.py new file mode 100644 index 000000000..029128f41 --- /dev/null +++ b/halotools/empirical_models/abunmatch/engines/setup_package.py @@ -0,0 +1,27 @@ +from distutils.extension import Extension +import os + +PATH_TO_PKG = os.path.relpath(os.path.dirname(__file__)) +SOURCES = ("bin_free_cam_kernel.pyx", ) +THIS_PKG_NAME = '.'.join(__name__.split('.')[:-1]) + + +def get_extensions(): + + names = [THIS_PKG_NAME + "." + src.replace('.pyx', '') for src in SOURCES] + sources = [os.path.join(PATH_TO_PKG, srcfn) for srcfn in SOURCES] + include_dirs = ['numpy'] + libraries = [] + language = 'c++' + extra_compile_args = ['-Ofast'] + + extensions = [] + for name, source in zip(names, sources): + extensions.append(Extension(name=name, + sources=[source], + include_dirs=include_dirs, + libraries=libraries, + language=language, + extra_compile_args=extra_compile_args)) + + return extensions diff --git a/halotools/empirical_models/abunmatch/tests/naive_python_cam.py b/halotools/empirical_models/abunmatch/tests/naive_python_cam.py new file mode 100644 index 000000000..4759e40b8 --- /dev/null +++ b/halotools/empirical_models/abunmatch/tests/naive_python_cam.py @@ -0,0 +1,43 @@ +""" Naive python implementation of bin-free conditional abundance matching +""" +import numpy as np + + +def sample2_window_indices(ix1, x_sample1, x_sample2, nwin): + """ For the point x1 = x_sample1[ix1], determine the indices of + the window surrounding each point in sample 2 that defines the + conditional probability distribution for `ynew`. + """ + nhalfwin = int(nwin/2) + npts2 = len(x_sample2) + + x1 = x_sample1[ix1] + iy2 = min(np.searchsorted(x_sample2, x1), npts2-1) + + if iy2 <= nhalfwin: + init_iy2_low, init_iy2_high = 0, nwin + elif iy2 >= npts2 - nhalfwin - 1: + init_iy2_low, init_iy2_high = npts2-nwin, npts2 + else: + init_iy2_low = iy2 - nhalfwin + init_iy2_high = init_iy2_low+nwin + + return init_iy2_low, init_iy2_high + + +def pure_python_rank_matching(x_sample1, ranks_sample1, + x_sample2, ranks_sample2, y_sample2, nwin): + """ Naive algorithm for implementing bin-free conditional abundance matching + for use in unit-testing. + """ + result = np.zeros_like(x_sample1) + + n1 = len(x_sample1) + + for i in range(n1): + low, high = sample2_window_indices(i, x_sample1, x_sample2, nwin) + sorted_window = np.sort(y_sample2[low:high]) + rank1 = ranks_sample1[i] + result[i] = sorted_window[rank1] + + return result diff --git a/halotools/empirical_models/abunmatch/tests/test_bin_free_cam.py b/halotools/empirical_models/abunmatch/tests/test_bin_free_cam.py new file mode 100644 index 000000000..76ec2f637 --- /dev/null +++ b/halotools/empirical_models/abunmatch/tests/test_bin_free_cam.py @@ -0,0 +1,505 @@ +""" +""" +import numpy as np +from astropy.utils.misc import NumpyRNGContext +from ..bin_free_cam import conditional_abunmatch +from ....utils.conditional_percentile import cython_sliding_rank, rank_order_function +from .naive_python_cam import pure_python_rank_matching +from ....utils import unsorting_indices + + +fixed_seed = 43 + + +def test1(): + """ Test case where x and x2 are sorted, y and y2 are sorted, + and the nearest x2 value is lined up with x + """ + nwin = 3 + + x = [1, 2, 3, 4, 5, 6, 7, 8, 9] + x2 = x + + y = np.arange(1, len(x)+1) + y2 = y*10. + + i2_matched = np.searchsorted(x2, x) + i2_matched = np.where(i2_matched >= len(y2), len(y2)-1, i2_matched) + + print("y = {0}".format(y)) + print("y2 = {0}\n".format(y2)) + + result = conditional_abunmatch(x, y, x2, y2, nwin, add_subgrid_noise=False) + print("ynew = {0}".format(result.astype('i4'))) + + assert np.all(result == y2) + + +def test2(): + """ Test case where x and x2 are sorted, y and y2 are not sorted, + and the nearest x2 value is lined up with x + """ + nwin = 3 + nhalfwin = int(nwin/2) + + x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9]) + x2 = x+0.01 + + with NumpyRNGContext(fixed_seed): + y = np.round(np.random.rand(len(x)), 2) + y2 = np.round(np.random.rand(len(x2)), 2) + + i2_matched = np.searchsorted(x2, x) + i2_matched = np.where(i2_matched >= len(y2), len(y2)-1, i2_matched) + + print("y = {0}".format(y)) + print("y2 = {0}\n".format(y2)) + + print("ranks1 = {0}".format(cython_sliding_rank(x, y, nwin))) + print("ranks2 = {0}".format(cython_sliding_rank(x2, y2, nwin))) + + result = conditional_abunmatch(x, y, x2, y2, nwin, add_subgrid_noise=False) + + print("\n\nynew = {0}".format(np.abs(result))) + print("y2 = {0}".format(y2)) + print("y = {0}\n".format(y)) + + # Test all points except edges + for itest in range(nhalfwin, len(x)-nhalfwin): + low = itest-nhalfwin + high = itest+nhalfwin+1 + window = y[low:high] + window2 = y2[low:high] + sorted_window2 = np.sort(window2) + window_ranks = rank_order_function(window) + itest_rank = window_ranks[nhalfwin] + itest_correct_result = sorted_window2[itest_rank] + itest_result = result[itest] + assert itest_result == itest_correct_result + + # Test left edge + for itest in range(nhalfwin): + low, high = 0, nwin + window = y[low:high] + window2 = y2[low:high] + sorted_window2 = np.sort(window2) + window_ranks = rank_order_function(window) + itest_rank = window_ranks[itest] + itest_correct_result = sorted_window2[itest_rank] + itest_result = result[itest] + msg = "itest_result = {0}, correct result = {1}" + assert itest_result == itest_correct_result, msg.format( + itest_result, itest_correct_result) + + # Test right edge + for iwin in range(nhalfwin+1, nwin): + itest = iwin + len(x) - nwin + low, high = len(x)-nwin, len(x) + window = y[low:high] + window2 = y2[low:high] + sorted_window2 = np.sort(window2) + window_ranks = rank_order_function(window) + itest_rank = window_ranks[iwin] + itest_correct_result = sorted_window2[itest_rank] + itest_result = result[itest] + msg = "itest_result = {0}, correct result = {1}" + assert itest_result == itest_correct_result, msg.format( + itest_result, itest_correct_result) + + +def test3(): + """ Test hard-coded case where x and x2 are sorted, y and y2 are sorted, + but the nearest x--x2 correspondence is no longer simple + """ + nwin = 3 + + x = np.array([0.1, 0.36, 0.36, 0.74, 0.83]) + x2 = np.array([0.54, 0.54, 0.55, 0.56, 0.57]) + + y = np.array([0.12, 0.13, 0.24, 0.33, 0.61]) + y2 = np.array([0.03, 0.54, 0.67, 0.73, 0.86]) + + i2_matched = np.searchsorted(x2, x) + i2_matched = np.where(i2_matched >= len(y2), len(y2)-1, i2_matched) + i2_matched = np.array([0, 0, 0, 4, 4]) + + result = conditional_abunmatch(x, y, x2, y2, nwin, add_subgrid_noise=False) + correct_result = [0.03, 0.54, 0.54, 0.73, 0.86] + + assert np.allclose(result, correct_result) + + +def test4(): + """ Regression test for buggy treatment of rightmost endpoint behavior + """ + + n1, n2, nwin = 8, 8, 3 + x = np.round(np.linspace(0.15, 1.3, n1), 2) + with NumpyRNGContext(fixed_seed): + y = np.round(np.random.uniform(0, 1, n1), 2) + ranks_sample1 = cython_sliding_rank(x, y, nwin) + + x2 = np.round(np.linspace(0.15, 1.3, n2), 2) + with NumpyRNGContext(fixed_seed): + y2 = np.round(np.random.uniform(-4, -3, n2), 2) + ranks_sample2 = cython_sliding_rank(x2, y2, nwin) + + pure_python_result = pure_python_rank_matching(x, ranks_sample1, + x2, ranks_sample2, y2, nwin) + + result = conditional_abunmatch(x, y, x2, y2, nwin, add_subgrid_noise=False) + + assert np.allclose(result, pure_python_result) + + +def test_brute_force_interior_points(): + """ + """ + num_tests = 50 + + nwin = 11 + nhalfwin = int(nwin/2) + + for i in range(num_tests): + seed = fixed_seed + i + with NumpyRNGContext(seed): + x1_low, x2_low = np.random.uniform(-10, 10, 2) + x1_high, x2_high = np.random.uniform(100, 200, 2) + n1, n2 = np.random.randint(30, 100, 2) + x = np.sort(np.random.uniform(x1_low, x1_high, n1)) + x2 = np.sort(np.random.uniform(x2_low, x2_high, n2)) + + y1_low, y2_low = np.random.uniform(-10, 10, 2) + y1_high, y2_high = np.random.uniform(100, 200, 2) + y = np.random.uniform(y1_low, y1_high, n1) + y2 = np.random.uniform(y2_low, y2_high, n2) + + ranks_sample1 = cython_sliding_rank(x, y, nwin) + ranks_sample2 = cython_sliding_rank(x2, y2, nwin) + + pure_python_result = pure_python_rank_matching(x, ranks_sample1, + x2, ranks_sample2, y2, nwin) + + cython_result = conditional_abunmatch(x, y, x2, y2, nwin, add_subgrid_noise=False) + + assert np.allclose(pure_python_result[nhalfwin:-nhalfwin], + cython_result[nhalfwin:-nhalfwin]) + + +def test_brute_force_left_endpoints(): + """ + """ + + num_tests = 50 + + nwin = 11 + nhalfwin = int(nwin/2) + + for i in range(num_tests): + seed = fixed_seed + i + with NumpyRNGContext(seed): + x1_low, x2_low = np.random.uniform(-10, 10, 2) + x1_high, x2_high = np.random.uniform(100, 200, 2) + n1, n2 = np.random.randint(30, 100, 2) + x = np.sort(np.random.uniform(x1_low, x1_high, n1)) + x2 = np.sort(np.random.uniform(x2_low, x2_high, n2)) + + y1_low, y2_low = np.random.uniform(-10, 10, 2) + y1_high, y2_high = np.random.uniform(100, 200, 2) + y = np.random.uniform(y1_low, y1_high, n1) + y2 = np.random.uniform(y2_low, y2_high, n2) + + ranks_sample1 = cython_sliding_rank(x, y, nwin) + ranks_sample2 = cython_sliding_rank(x2, y2, nwin) + + pure_python_result = pure_python_rank_matching(x, ranks_sample1, + x2, ranks_sample2, y2, nwin) + + cython_result = conditional_abunmatch(x, y, x2, y2, nwin, add_subgrid_noise=False) + + # Test left edge + assert np.allclose(pure_python_result[:nhalfwin], cython_result[:nhalfwin]) + + +def test_brute_force_right_points(): + """ + """ + + num_tests = 50 + + nwin = 11 + nhalfwin = int(nwin/2) + + for i in range(num_tests): + seed = fixed_seed + i + with NumpyRNGContext(seed): + x1_low, x2_low = np.random.uniform(-10, 10, 2) + x1_high, x2_high = np.random.uniform(100, 200, 2) + n1, n2 = np.random.randint(30, 100, 2) + x = np.sort(np.random.uniform(x1_low, x1_high, n1)) + x2 = np.sort(np.random.uniform(x2_low, x2_high, n2)) + + y1_low, y2_low = np.random.uniform(-10, 10, 2) + y1_high, y2_high = np.random.uniform(100, 200, 2) + y = np.random.uniform(y1_low, y1_high, n1) + y2 = np.random.uniform(y2_low, y2_high, n2) + + ranks_sample1 = cython_sliding_rank(x, y, nwin) + ranks_sample2 = cython_sliding_rank(x2, y2, nwin) + + pure_python_result = pure_python_rank_matching(x, ranks_sample1, + x2, ranks_sample2, y2, nwin) + + cython_result = conditional_abunmatch(x, y, x2, y2, nwin, add_subgrid_noise=False) + + # Test right edge + assert np.allclose(pure_python_result[-nhalfwin:], cython_result[-nhalfwin:]) + + +def test_hard_coded_case1(): + nwin = 3 + + x = np.array([0.1, 0.36, 0.5, 0.74, 0.83]) + x2 = np.copy(x) + + y = np.array([0.12, 0.13, 0.24, 0.33, 0.61]) + y2 = np.array([0.03, 0.54, 0.67, 0.73, 0.86]) + + correct_result = [0.03, 0.54, 0.67, 0.73, 0.86] + result = conditional_abunmatch(x, y, x2, y2, nwin, add_subgrid_noise=False) + + assert np.allclose(result, correct_result) + +def test_hard_coded_case2(): + nwin = 3 + + x = np.array([0.1, 0.36, 0.36, 0.74, 0.83]) + x2 = np.array([0.54, 0.54, 0.55, 0.56, 0.57]) + + y = np.array([0.12, 0.13, 0.24, 0.33, 0.61]) + y2 = np.array([0.03, 0.54, 0.67, 0.73, 0.86]) + + correct_result = [0.03, 0.54, 0.54, 0.73, 0.86] + result = conditional_abunmatch(x, y, x2, y2, nwin, add_subgrid_noise=False) + + assert np.allclose(result, correct_result) + + +def test_hard_coded_case3(): + """ x==x2. + + So the CAM windows are always the same. + So the first two windows are the leftmost edge, + the middle entry uses the middle window, + and the last two entries use the rightmost edge window. + """ + nwin = 3 + + x = np.array([0.1, 0.36, 0.5, 0.74, 0.83]) + x2 = np.copy(x) + + y = np.array([0.12, 0.13, 0.24, 0.33, 0.61]) + y2 = np.array([0.3, 0.04, 0.6, 10., 5.]) + + correct_result = [0.04, 0.3, 0.6, 5., 10.] + result = conditional_abunmatch(x, y, x2, y2, nwin, add_subgrid_noise=False) + + assert np.allclose(result, correct_result) + + +def test_hard_coded_case5(): + nwin = 3 + + x = np.array((1., 1., 1, 1, 1)) + x2 = np.array([0.1, 0.36, 0.5, 0.74, 0.83]) + + y = np.array([0.12, 0.13, 0.24, 0.33, 0.61]) + y2 = np.array([0.3, 0.04, 0.6, 10., 5.]) + + correct_result = [0.6, 5., 5., 5., 10.] + result = conditional_abunmatch(x, y, x2, y2, nwin, add_subgrid_noise=False) + + print("\n\ncorrect result = {0}".format(correct_result)) + print("cython result = {0}\n".format(result)) + + msg = "Cython implementation incorrectly ignores searchsorted result for edges" + assert np.allclose(result, correct_result), msg + + +def test_hard_coded_case4(): + """ Every x2 is larger than the largest x. + + So the only CAM window ever used is the first 3 elements of y2. + """ + nwin = 3 + + x = np.array((0., 0., 0., 0., 0.)) + x2 = np.array([0.1, 0.36, 0.5, 0.74, 0.83]) + + y = np.array([0.12, 0.13, 0.24, 0.33, 0.61]) + y2 = np.array([0.3, 0.04, 0.6, 10., 5.]) + + correct_result = [0.04, 0.3, 0.3, 0.3, 0.6] + result = conditional_abunmatch(x, y, x2, y2, nwin, add_subgrid_noise=False) + + assert np.allclose(result, correct_result) + + +def test_hard_coded_case6(): + """ + """ + + x = [0.15, 0.31, 0.48, 0.64, 0.81, 0.97, 1.14, 1.3] + x2 = [0.15, 0.38, 0.61, 0.84, 1.07, 1.3] + + y = [0.22, 0.87, 0.21, 0.92, 0.49, 0.61, 0.77, 0.52] + y2 = [-3.78, -3.13, -3.79, -3.08, -3.51, -3.39] + + nwin = 5 + + ranks_sample1 = cython_sliding_rank(x, y, nwin) + ranks_sample2 = cython_sliding_rank(x2, y2, nwin) + + pure_python_result = pure_python_rank_matching(x, ranks_sample1, + x2, ranks_sample2, y2, nwin) + + result = conditional_abunmatch(x, y, x2, y2, nwin, add_subgrid_noise=False) + + assert np.allclose(result, pure_python_result) + + +def test_subgrid_noise1(): + n1, n2 = int(5e4), int(5e3) + + with NumpyRNGContext(fixed_seed): + x = np.sort(np.random.uniform(0, 10, n1)) + y = np.random.uniform(0, 1, n1) + + with NumpyRNGContext(fixed_seed): + x2 = np.sort(np.random.uniform(0, 10, n2)) + y2 = np.random.uniform(-4, -3, n2) + + nwin1 = 201 + result = conditional_abunmatch(x, y, x2, y2, nwin1, add_subgrid_noise=False) + result2 = conditional_abunmatch(x, y, x2, y2, nwin1, add_subgrid_noise=True) + assert np.allclose(result, result2, atol=0.1) + assert not np.allclose(result, result2, atol=0.02) + + nwin2 = 1001 + result = conditional_abunmatch(x, y, x2, y2, nwin2, add_subgrid_noise=False) + result2 = conditional_abunmatch(x, y, x2, y2, nwin2, add_subgrid_noise=True) + assert np.allclose(result, result2, atol=0.02) + + +def test_initial_sorting1(): + """ + """ + n1, n2 = int(2e3), int(1e3) + + with NumpyRNGContext(fixed_seed): + x = np.sort(np.random.uniform(0, 10, n1)) + y = np.random.uniform(0, 1, n1) + + with NumpyRNGContext(fixed_seed): + x2 = np.sort(np.random.uniform(0, 10, n2)) + y2 = np.random.uniform(-4, -3, n2) + + nwin1 = 101 + result = conditional_abunmatch( + x, y, x2, y2, nwin1, assume_x_is_sorted=False, assume_x2_is_sorted=False, + add_subgrid_noise=False) + result2 = conditional_abunmatch( + x, y, x2, y2, nwin1, assume_x_is_sorted=True, assume_x2_is_sorted=True, + add_subgrid_noise=False) + assert np.allclose(result, result2) + + + +def test_initial_sorting2(): + """ + """ + n1, n2 = int(2e3), int(1e3) + + with NumpyRNGContext(fixed_seed): + x = np.sort(np.random.uniform(0, 10, n1)) + y = np.random.uniform(0, 1, n1) + + with NumpyRNGContext(fixed_seed): + x2 = np.random.uniform(0, 10, n2) + y2 = np.random.uniform(-4, -3, n2) + + nwin1 = 101 + result = conditional_abunmatch( + x, y, x2, y2, nwin1, assume_x_is_sorted=False, assume_x2_is_sorted=False, + add_subgrid_noise=False) + result2 = conditional_abunmatch( + x, y, x2, y2, nwin1, assume_x_is_sorted=True, assume_x2_is_sorted=False, + add_subgrid_noise=False) + assert np.allclose(result, result2) + + +def test_initial_sorting3(): + """ + """ + n1, n2 = int(2e3), int(1e3) + + with NumpyRNGContext(fixed_seed): + x = np.random.uniform(0, 10, n1) + y = np.random.uniform(0, 1, n1) + + with NumpyRNGContext(fixed_seed): + x2 = np.sort(np.random.uniform(0, 10, n2)) + y2 = np.random.uniform(-4, -3, n2) + + nwin1 = 101 + result = conditional_abunmatch( + x, y, x2, y2, nwin1, assume_x_is_sorted=False, assume_x2_is_sorted=True, + add_subgrid_noise=False) + result2 = conditional_abunmatch( + x, y, x2, y2, nwin1, assume_x_is_sorted=False, assume_x2_is_sorted=False, + add_subgrid_noise=False) + assert np.allclose(result, result2) + + +def test_initial_sorting4(): + """ + """ + n1, n2 = int(2e3), int(1e3) + + with NumpyRNGContext(fixed_seed): + x = np.random.uniform(0, 10, n1) + y = np.random.uniform(0, 1, n1) + + with NumpyRNGContext(fixed_seed): + x2 = np.random.uniform(0, 10, n2) + y2 = np.random.uniform(-4, -3, n2) + + nwin1 = 101 + result = conditional_abunmatch( + x, y, x2, y2, nwin1, + assume_x_is_sorted=False, assume_x2_is_sorted=False, + add_subgrid_noise=False) + + idx_x_sorted = np.argsort(x) + x_sorted = x[idx_x_sorted] + y_sorted = y[idx_x_sorted] + result2 = conditional_abunmatch( + x_sorted, y_sorted, x2, y2, nwin1, + assume_x_is_sorted=True, assume_x2_is_sorted=False, + add_subgrid_noise=False) + assert np.allclose(result, result2[unsorting_indices(idx_x_sorted)]) + + idx_x2_sorted = np.argsort(x2) + x2_sorted = x2[idx_x2_sorted] + y2_sorted = y2[idx_x2_sorted] + result3 = conditional_abunmatch( + x, y, x2_sorted, y2_sorted, nwin1, + assume_x_is_sorted=False, assume_x2_is_sorted=True, + add_subgrid_noise=False) + assert np.allclose(result, result3) + + result4 = conditional_abunmatch( + x_sorted, y_sorted, x2_sorted, y2_sorted, nwin1, + assume_x_is_sorted=True, assume_x2_is_sorted=True, + add_subgrid_noise=False) + assert np.allclose(result, result4[unsorting_indices(idx_x_sorted)]) diff --git a/halotools/empirical_models/abunmatch/tests/test_conditional_abunmatch.py b/halotools/empirical_models/abunmatch/tests/test_conditional_abunmatch.py index 994870274..9db239be3 100644 --- a/halotools/empirical_models/abunmatch/tests/test_conditional_abunmatch.py +++ b/halotools/empirical_models/abunmatch/tests/test_conditional_abunmatch.py @@ -10,7 +10,7 @@ def test_conditional_abunmatch_bin_based1(): with NumpyRNGContext(43): x = np.random.normal(loc=0, scale=0.1, size=100) y = np.linspace(10, 20, 100) - model_y = conditional_abunmatch_bin_based(x, y, seed=43) + model_y = conditional_abunmatch_bin_based(x, y, seed=43, npts_lookup_table=len(y)) msg = "monotonic cam does not preserve mean" assert np.allclose(model_y.mean(), y.mean(), rtol=0.1), msg @@ -19,7 +19,7 @@ def test_conditional_abunmatch_bin_based2(): with NumpyRNGContext(43): x = np.random.normal(loc=0, scale=0.1, size=100) y = np.linspace(10, 20, 100) - model_y = conditional_abunmatch_bin_based(x, y, seed=43) + model_y = conditional_abunmatch_bin_based(x, y, seed=43, npts_lookup_table=len(y)) idx_x_sorted = np.argsort(x) msg = "monotonic cam does not preserve correlation" high = model_y[idx_x_sorted][-50:].mean() @@ -33,7 +33,7 @@ def test_conditional_abunmatch_bin_based3(): with NumpyRNGContext(43): x = np.random.normal(loc=0, scale=0.1, size=100) y = np.linspace(10, 20, 100) - model_y = conditional_abunmatch_bin_based(x, y, sigma=0.01, seed=43) + model_y = conditional_abunmatch_bin_based(x, y, sigma=0.01, seed=43, npts_lookup_table=len(y)) idx_x_sorted = np.argsort(x) msg = "low-noise cam does not preserve correlation" high = model_y[idx_x_sorted][-50:].mean() diff --git a/halotools/empirical_models/abunmatch/tests/test_pure_python.py b/halotools/empirical_models/abunmatch/tests/test_pure_python.py new file mode 100644 index 000000000..34e43a8e5 --- /dev/null +++ b/halotools/empirical_models/abunmatch/tests/test_pure_python.py @@ -0,0 +1,156 @@ +""" +""" +import numpy as np +from astropy.utils.misc import NumpyRNGContext +from ....utils.conditional_percentile import cython_sliding_rank +from .naive_python_cam import sample2_window_indices, pure_python_rank_matching + +fixed_seed = 43 + + +def test_pure_python1(): + """ + """ + n1, n2, nwin = 5001, 1001, 11 + nhalfwin = nwin/2 + x_sample1 = np.linspace(0, 1, n1) + with NumpyRNGContext(fixed_seed): + y_sample1 = np.random.uniform(0, 1, n1) + ranks_sample1 = cython_sliding_rank(x_sample1, y_sample1, nwin) + + x_sample2 = np.linspace(0, 1, n2) + with NumpyRNGContext(fixed_seed): + y_sample2 = np.random.uniform(-4, -3, n2) + ranks_sample2 = cython_sliding_rank(x_sample2, y_sample2, nwin) + + result = pure_python_rank_matching(x_sample1, ranks_sample1, + x_sample2, ranks_sample2, y_sample2, nwin) + + for ix1 in range(2*nwin, n1-2*nwin): + + rank1 = ranks_sample1[ix1] + low, high = sample2_window_indices(ix1, x_sample1, x_sample2, nwin) + + sorted_window2 = np.sort(y_sample2[low:high]) + assert len(sorted_window2) == nwin + + correct_result_ix1 = sorted_window2[rank1] + + assert correct_result_ix1 == result[ix1] + + +def test_hard_coded_case1(): + nwin = 3 + + x = np.array([0.1, 0.36, 0.5, 0.74, 0.83]) + x2 = np.copy(x) + + y = np.array([0.12, 0.13, 0.24, 0.33, 0.61]) + y2 = np.array([0.03, 0.54, 0.67, 0.73, 0.86]) + + ranks_sample1 = cython_sliding_rank(x, y, nwin) + ranks_sample2 = cython_sliding_rank(x2, y2, nwin) + + pure_python_result = pure_python_rank_matching(x, ranks_sample1, + x2, ranks_sample2, y2, nwin) + + correct_result = [0.03, 0.54, 0.67, 0.73, 0.86] + + assert np.allclose(pure_python_result, correct_result) + + +def test_hard_coded_case2(): + """ + """ + nwin = 3 + + x = np.array([0.1, 0.36, 0.36, 0.74, 0.83]) + x2 = np.array([0.54, 0.54, 0.55, 0.56, 0.57]) + + y = np.array([0.12, 0.13, 0.24, 0.33, 0.61]) + y2 = np.array([0.03, 0.54, 0.67, 0.73, 0.86]) + + ranks_sample1 = cython_sliding_rank(x, y, nwin) + ranks_sample2 = cython_sliding_rank(x2, y2, nwin) + + pure_python_result = pure_python_rank_matching(x, ranks_sample1, + x2, ranks_sample2, y2, nwin) + + correct_result = [0.03, 0.54, 0.54, 0.73, 0.86] + + assert np.allclose(pure_python_result, correct_result) + + +def test_hard_coded_case3(): + """ x==x2. + + So the CAM windows are always the same. + So the first two windows are the leftmost edge, + the middle entry uses the middle window, + and the last two entries use the rightmost edge window. + """ + nwin = 3 + + x = np.array([0.1, 0.36, 0.5, 0.74, 0.83]) + x2 = np.copy(x) + + y = np.array([0.12, 0.13, 0.24, 0.33, 0.61]) + y2 = np.array([0.3, 0.04, 0.6, 10., 5.]) + + ranks_sample1 = cython_sliding_rank(x, y, nwin) + ranks_sample2 = cython_sliding_rank(x2, y2, nwin) + + pure_python_result = pure_python_rank_matching(x, ranks_sample1, + x2, ranks_sample2, y2, nwin) + + correct_result = [0.04, 0.3, 0.6, 5., 10.] + + assert np.allclose(pure_python_result, correct_result) + + +def test_hard_coded_case4(): + """ Every x2 is larger than the largest x. + + So the only CAM window ever used is the first 3 elements of y2. + """ + nwin = 3 + + x = np.array((0., 0., 0., 0., 0.)) + x2 = np.array([0.1, 0.36, 0.5, 0.74, 0.83]) + + y = np.array([0.12, 0.13, 0.24, 0.33, 0.61]) + y2 = np.array([0.3, 0.04, 0.6, 10., 5.]) + + ranks_sample1 = cython_sliding_rank(x, y, nwin) + ranks_sample2 = cython_sliding_rank(x2, y2, nwin) + + pure_python_result = pure_python_rank_matching(x, ranks_sample1, + x2, ranks_sample2, y2, nwin) + + correct_result = [0.04, 0.3, 0.3, 0.3, 0.6] + + assert np.allclose(pure_python_result, correct_result) + + +def test_hard_coded_case5(): + """ Every x2 is smaller than the smallest x. + + So the only CAM window ever used is the final 3 elements of y2. + """ + nwin = 3 + + x = np.array((1., 1., 1, 1, 1)) + x2 = np.array([0.1, 0.36, 0.5, 0.74, 0.83]) + + y = np.array([0.12, 0.13, 0.24, 0.33, 0.61]) + y2 = np.array([0.3, 0.04, 0.6, 10., 5.]) + + ranks_sample1 = cython_sliding_rank(x, y, nwin) + ranks_sample2 = cython_sliding_rank(x2, y2, nwin) + + pure_python_result = pure_python_rank_matching(x, ranks_sample1, + x2, ranks_sample2, y2, nwin) + + correct_result = [0.6, 5, 5, 5, 10] + + assert np.allclose(pure_python_result, correct_result) diff --git a/halotools/empirical_models/abunmatch/tests/test_sample2_window_function.py b/halotools/empirical_models/abunmatch/tests/test_sample2_window_function.py new file mode 100644 index 000000000..b9a23bb7e --- /dev/null +++ b/halotools/empirical_models/abunmatch/tests/test_sample2_window_function.py @@ -0,0 +1,121 @@ +""" Module testing the sample2_window_indices function that returns the +relevant CAM window to the naive python implementation. +""" +import numpy as np +from .naive_python_cam import sample2_window_indices + + +def test_left_edge_window(): + """ Setup: x1 == x2. Enforce proper behavior at the leftmost edge. + """ + n1, n2 = 20, 20 + x_sample1 = np.arange(n1) + x_sample2 = np.arange(n2) + + nwin = 5 + + ix1 = 0 + init_iy2_low, init_iy2_high = sample2_window_indices( + ix1, x_sample1, x_sample2, nwin) + assert (init_iy2_low, init_iy2_high) == (0, nwin) + assert len(x_sample2[init_iy2_low:init_iy2_high]) == nwin + + ix1 = 1 + init_iy2_low, init_iy2_high = sample2_window_indices( + ix1, x_sample1, x_sample2, nwin) + assert (init_iy2_low, init_iy2_high) == (0, nwin) + assert len(x_sample2[init_iy2_low:init_iy2_high]) == nwin + + ix1 = 2 + init_iy2_low, init_iy2_high = sample2_window_indices( + ix1, x_sample1, x_sample2, nwin) + assert (init_iy2_low, init_iy2_high) == (0, nwin) + assert len(x_sample2[init_iy2_low:init_iy2_high]) == nwin + + ix1 = 3 + init_iy2_low, init_iy2_high = sample2_window_indices( + ix1, x_sample1, x_sample2, nwin) + assert (init_iy2_low, init_iy2_high) == (1, nwin+1) + assert len(x_sample2[init_iy2_low:init_iy2_high]) == nwin + + ix1 = 4 + init_iy2_low, init_iy2_high = sample2_window_indices( + ix1, x_sample1, x_sample2, nwin) + assert (init_iy2_low, init_iy2_high) == (2, nwin+2) + assert len(x_sample2[init_iy2_low:init_iy2_high]) == nwin + + +def test_right_edge_window(): + """ Setup: x1 == x2. Enforce proper behavior at the rightmost edge. + """ + n1, n2 = 20, 20 + x_sample1 = np.arange(n1) + x_sample2 = np.arange(n2) + + nwin = 5 + + ix1 = 19 + init_iy2_low, init_iy2_high = sample2_window_indices( + ix1, x_sample1, x_sample2, nwin) + assert (init_iy2_low, init_iy2_high) == (n2-nwin, n2) + assert len(x_sample2[init_iy2_low:init_iy2_high]) == nwin + + ix1 = 18 + init_iy2_low, init_iy2_high = sample2_window_indices( + ix1, x_sample1, x_sample2, nwin) + assert (init_iy2_low, init_iy2_high) == (n2-nwin, n2) + assert len(x_sample2[init_iy2_low:init_iy2_high]) == nwin + + ix1 = 17 + init_iy2_low, init_iy2_high = sample2_window_indices( + ix1, x_sample1, x_sample2, nwin) + assert (init_iy2_low, init_iy2_high) == (n2-nwin, n2) + assert len(x_sample2[init_iy2_low:init_iy2_high]) == nwin + + ix1 = 16 + init_iy2_low, init_iy2_high = sample2_window_indices( + ix1, x_sample1, x_sample2, nwin) + assert (init_iy2_low, init_iy2_high) == (n2-nwin-1, n2-1) + assert len(x_sample2[init_iy2_low:init_iy2_high]) == nwin + + ix1 = 15 + init_iy2_low, init_iy2_high = sample2_window_indices( + ix1, x_sample1, x_sample2, nwin) + assert (init_iy2_low, init_iy2_high) == (n2-nwin-2, n2-2) + assert len(x_sample2[init_iy2_low:init_iy2_high]) == nwin + + +def test_all_x1_less_than_x2(): + """ Setup: np.all(x1 < x2.min()). + + Enforce proper behavior at the leftmost edge. + """ + n1, n2 = 20, 20 + x_sample1 = np.arange(n1) + x_sample2 = np.arange(100, 100+n2) + + nwin = 5 + + for ix1 in range(n1): + init_iy2_low, init_iy2_high = sample2_window_indices( + ix1, x_sample1, x_sample2, nwin) + assert (init_iy2_low, init_iy2_high) == (0, nwin), "ix1 = {0}".format(ix1) + assert len(x_sample2[init_iy2_low:init_iy2_high]) == nwin + + +def test_all_x1_greater_than_x2(): + """ Setup: np.all(x1 < x2.min()). + + Enforce proper behavior at the leftmost edge. + """ + n1, n2 = 20, 20 + x_sample1 = np.arange(n1) + x_sample2 = np.arange(-100, -100+n2) + + nwin = 5 + + for ix1 in range(n1): + init_iy2_low, init_iy2_high = sample2_window_indices( + ix1, x_sample1, x_sample2, nwin) + assert (init_iy2_low, init_iy2_high) == (n2-nwin, n2), "ix1 = {0}".format(ix1) + assert len(x_sample2[init_iy2_low:init_iy2_high]) == nwin diff --git a/halotools/empirical_models/abunmatch/tests/test_single_unit.py b/halotools/empirical_models/abunmatch/tests/test_single_unit.py new file mode 100644 index 000000000..89f385d7d --- /dev/null +++ b/halotools/empirical_models/abunmatch/tests/test_single_unit.py @@ -0,0 +1,12 @@ +""" +""" +import numpy as np +from astropy.utils.misc import NumpyRNGContext +from ..bin_free_cam import conditional_abunmatch + + +fixed_seed = 5 + + +def test(): + pass diff --git a/halotools/utils/conditional_percentile.py b/halotools/utils/conditional_percentile.py index 4657b5e59..dd2b9f15d 100644 --- a/halotools/utils/conditional_percentile.py +++ b/halotools/utils/conditional_percentile.py @@ -13,8 +13,7 @@ def sliding_conditional_percentile(x, y, window_length, assume_x_is_sorted=False, add_subgrid_noise=True, seed=None): - r""" Estimate the conditional cumulative distribution function Prob(< y | x) - using a sliding window of length ``window_length``. + r""" Estimate the cumulative distribution function Prob(< y | x). Parameters ---------- diff --git a/halotools/utils/inverse_transformation_sampling.py b/halotools/utils/inverse_transformation_sampling.py index b48148d1d..1868fc70d 100644 --- a/halotools/utils/inverse_transformation_sampling.py +++ b/halotools/utils/inverse_transformation_sampling.py @@ -54,7 +54,7 @@ def monte_carlo_from_cdf_lookup(x_table, y_table, mc_input='random', Notes ----- - See the Transformation of Probability tutorial at https://github.com/jbailinua/probability + See the `Transformation of Probability tutorial `_ for pedagogical derivations associated with inverse transformation sampling. Examples