def joint(*factors): """ Return a Factor that combines the scope and beliefs of the input factors. factorA.scope, factorB.scope, factorC.scope -> new_factor.scope: [c, a, d, e], [e, f, d], [g, e, a] -> [c, a, d, e, f, g] up to permutation factorA.beliefs.shape, factorB.beliefs.shape, factorC.beliefs.shape -> new_factor.shape: (4, 2, 5, 6), (6, 7, 5), (3, 6, 2) -> (4, 2, 5, 6, 7, 3) up to permutation Does not modify the input. """ joint_scope = list(set.union(set(), *(set(factor.scope) for factor in factors))) #the order that the joint scope variables appear in each factor's scope subscopes = [[rvar for rvar in joint_scope if rvar in factor.scope] for factor in factors] #the index mapping of those subscopes to each factor's scope permutations = [tuple(map(factor.scope.index, subscope)) for (factor, subscope) in zip(factors, subscopes)] #make room for new variables in order to broadcast to a common shape slices = [tuple(slice(None) if rvar in factor.scope else scipy.newaxis for rvar in joint_scope) for factor in factors] beliefs = [factor.beliefs.transpose(permutation)[slice_] for (factor, permutation, slice_) in zip(factors, permutations, slices)] #scipy.multiply.reduce may not return an array in all circumstances; #furthermore, it may return an array of sub-arrays #TODO: rely on numpy 1.70 for access to keepdims = True #joint_beliefs = scipy.multiply.reduce(beliefs, keepdims = True).squeeze() #FOR NOW, use broadcast_arrays to match shapes of all beliefs #before the multiply operation to prevent the array nesting bug... #also, scipy.broadcast_arrays(*[]) fails so we need an explicit check if not beliefs: return Factor.null() joint_beliefs = scipy.multiply.reduce(scipy.broadcast_arrays(*beliefs)) return Factor(joint_scope, joint_beliefs)
def multi_epoch(self, velocity, sigvel, mass, dates, pfalse=1e-4, log_minv=-3, log_maxv=4, log_stepv=0.02): """Returns a callable Basefitter which computes the log-likelihood to reproduce the observed multi-epoch radial velocity distribution. Uses the current settings of the binary properties to calculate the distribution of radial velocity offsets due to binary orbital motions. Arguments: - `velocity`: list-like with for every star an array-like containing the observed radial velocities in km/s. - `sigvel`: list-like with for every star an array-like containing the measurement uncertainties in km/s. - `mass`: 1D array-like (or single number) giving best estimate for mass of the observed stars in solar masses. - `dates`: list-like with for every star an array-like containing the date of observations in years. - `pfalse`: probability of false detection (i.e. detecting a single star as a binary). Lowering this will decrease the number of detected binaries. - `log_minv`: 10_log of the lowest velocity bin in km/s (should be significantly smaller than the velocity dispersion). - `log_maxv`: 10_log maximum of the largest velocity bin. - `log_stepv`: step size in 10_log(velocity) space. """ if sp.asarray(mass).ndim == 0: mass = [mass] * len(velocity) for test_length in ('sigvel', 'mass', 'dates'): if len(locals()[test_length]) != len(velocity): raise ValueError( '%s does not have the same length as the velocity list' % test_length) unique_dates = sp.unique(reduce(sp.append, dates)) vbin = {} for date in unique_dates: vbin.update({date: self.velocity(1., time=date)[0]}) vmean = [] sigmean = [] single_mass = [] pdet_single = [] pdet_rvvar = [] pbin = [] is_single = [] vbord = 10**sp.arange(log_minv, log_maxv, log_stepv) vbound = sp.append(-vbord[::-1], sp.append(0, vbord)) for mult_vel, mult_sigvel, pmass, epochs in zip( velocity, sigvel, mass, dates): epochs, mult_vel, mult_sigvel = sp.broadcast_arrays( epochs, mult_vel, mult_sigvel) if epochs.size == 1: mean_rv = mult_vel[0] mean_sig = mult_sigvel[0] rv_binoffset = vbin[ epochs[0]] # + sp.randn(self.size) * mult_sigvel[0] pdet = 0. rvvariable = False else: weight = mult_sigvel**-2 rv_offset_per_epoch = sp.zeros((self.size, len(epochs))) rv_binoffset_per_epoch = sp.zeros((self.size, len(epochs))) for ixepoch, (date, sv) in enumerate(zip(epochs, mult_sigvel)): rv_binoffset_per_epoch[:, ixepoch] = vbin[date] rv_offset_per_epoch[:, ixepoch] = rv_binoffset_per_epoch[:, ixepoch] * pmass**( 1. / 3.) + sp.randn(self.size) * sv rv_offset_mean = sp.sum( rv_offset_per_epoch * weight[sp.newaxis, :], -1) / sp.sum(weight) chisq = sp.sum( (rv_offset_per_epoch - rv_offset_mean[:, sp.newaxis])**2. * weight[sp.newaxis, :], -1) isdetected = sp.stats.chisqprob(chisq, len(epochs) - 1) < pfalse pdet = float(sp.sum(isdetected)) / isdetected.size rv_binoffset = (sp.sum( rv_binoffset_per_epoch * weight[sp.newaxis, :], -1) / sp.sum(weight))[~isdetected] mean_rv = sp.sum(mult_vel * weight) / sp.sum(weight) mean_sig = sp.sum(weight)**-.5 rvvariable = sp.stats.chisqprob( sp.sum((mean_rv - mult_vel)**2 * weight), len(epochs) - 1) < pfalse if rvvariable: pdet_rvvar.append(pdet) else: vmean.append(mean_rv) sigmean.append(mean_sig) pdet_single.append(pdet) single_mass.append(pmass) prob_bin = sp.histogram( abs(rv_binoffset), bins=sp.append( 0, vbord))[0] * 1. / rv_binoffset.size pbin.append( sp.append(prob_bin[::-1], prob_bin) / 2. / (vbound[1:] - vbound[:-1])) is_single.append(not rvvariable) pbin = sp.array(pbin).T vmean = sp.array(vmean) sigmean = sp.array(sigmean) single_mass = sp.array(single_mass) pdet_single = sp.array(pdet_single) pdet_rvvar = sp.array(pdet_rvvar) is_single = sp.array(is_single, dtype='bool') return fitter.BinaryFit(vmean, sigmean, single_mass, vbound, pbin, pdet_single, pdet_rvvar, is_single)
def multi_epoch(self, velocity, sigvel, mass, dates, pfalse=1e-4, log_minv=-3, log_maxv=4, log_stepv=0.02): """Returns a callable Basefitter which computes the log-likelihood to reproduce the observed multi-epoch radial velocity distribution. Uses the current settings of the binary properties to calculate the distribution of radial velocity offsets due to binary orbital motions. Arguments: - `velocity`: list-like with for every star an array-like containing the observed radial velocities in km/s. - `sigvel`: list-like with for every star an array-like containing the measurement uncertainties in km/s. - `mass`: 1D array-like (or single number) giving best estimate for mass of the observed stars in solar masses. - `dates`: list-like with for every star an array-like containing the date of observations in years. - `pfalse`: probability of false detection (i.e. detecting a single star as a binary). Lowering this will decrease the number of detected binaries. - `log_minv`: 10_log of the lowest velocity bin in km/s (should be significantly smaller than the velocity dispersion). - `log_maxv`: 10_log maximum of the largest velocity bin. - `log_stepv`: step size in 10_log(velocity) space. """ if sp.asarray(mass).ndim == 0: mass = [mass] * len(velocity) for test_length in ('sigvel', 'mass', 'dates'): if len(locals()[test_length]) != len(velocity): raise ValueError('%s does not have the same length as the velocity list' % test_length) unique_dates = sp.unique(reduce(sp.append, dates)) vbin = {} for date in unique_dates: vbin.update({date: self.velocity(1., time=date)[0]}) vmean = [] sigmean = [] single_mass = [] pdet_single = [] pdet_rvvar = [] pbin = [] is_single = [] vbord = 10 ** sp.arange(log_minv, log_maxv, log_stepv) vbound = sp.append(-vbord[::-1], sp.append(0, vbord)) for mult_vel, mult_sigvel, pmass, epochs in zip(velocity, sigvel, mass, dates): epochs, mult_vel, mult_sigvel = sp.broadcast_arrays(epochs, mult_vel, mult_sigvel) if epochs.size == 1: mean_rv = mult_vel[0] mean_sig = mult_sigvel[0] rv_binoffset = vbin[epochs[0]]# + sp.randn(self.size) * mult_sigvel[0] pdet = 0. rvvariable = False else: weight = mult_sigvel ** -2 rv_offset_per_epoch = sp.zeros((self.size, len(epochs))) rv_binoffset_per_epoch = sp.zeros((self.size, len(epochs))) for ixepoch, (date, sv) in enumerate(zip(epochs, mult_sigvel)): rv_binoffset_per_epoch[:, ixepoch] = vbin[date] rv_offset_per_epoch[:, ixepoch] = rv_binoffset_per_epoch[:, ixepoch] * pmass ** (1. / 3.) + sp.randn(self.size) * sv rv_offset_mean = sp.sum(rv_offset_per_epoch * weight[sp.newaxis, :], -1) / sp.sum(weight) chisq = sp.sum((rv_offset_per_epoch - rv_offset_mean[:, sp.newaxis]) ** 2. * weight[sp.newaxis, :], -1) isdetected = sp.stats.chisqprob(chisq, len(epochs) - 1) < pfalse pdet = float(sp.sum(isdetected)) / isdetected.size rv_binoffset = (sp.sum(rv_binoffset_per_epoch * weight[sp.newaxis, :], -1) / sp.sum(weight))[~isdetected] mean_rv = sp.sum(mult_vel * weight) / sp.sum(weight) mean_sig = sp.sum(weight) ** -.5 rvvariable = sp.stats.chisqprob(sp.sum((mean_rv - mult_vel) ** 2 * weight), len(epochs) - 1) < pfalse if rvvariable: pdet_rvvar.append(pdet) else: vmean.append(mean_rv) sigmean.append(mean_sig) pdet_single.append(pdet) single_mass.append(pmass) prob_bin = sp.histogram(abs(rv_binoffset), bins=sp.append(0, vbord))[0] * 1. / rv_binoffset.size pbin.append(sp.append(prob_bin[::-1], prob_bin) / 2. / (vbound[1:] - vbound[:-1])) is_single.append(not rvvariable) pbin = sp.array(pbin).T vmean = sp.array(vmean) sigmean = sp.array(sigmean) single_mass = sp.array(single_mass) pdet_single = sp.array(pdet_single) pdet_rvvar = sp.array(pdet_rvvar) is_single = sp.array(is_single, dtype='bool') return fitter.BinaryFit(vmean, sigmean, single_mass, vbound, pbin, pdet_single, pdet_rvvar, is_single)