Pendant Pendant Sum Monte Carlo Simulation


1.1 INTRODUCTION: THERMODYNAMICS AND STATISTICAL MECHANICS OF THE PERFECT GAS

Ludwig Boltzmann, who spent much of his life studying statistical mechanics, died in 1906, by his own hand.
Paul Ehrenfest, carrying on the work, died similarly in 1933.
Now it is our turn to study statistical mechanics.

Perhaps it will be wise to approach the subject cautiously. We will begin by considering the simplest meaningful example, the perfect gas, in order to get the central concepts sorted out.

States of Matter, by David Goodstein

Appendix 3 - Pendant Pendant Sum Monte Carlo Simulation

In the evaluation of Ascher Pendant Pendant Sum relationships in the KFG, the following conclusions were made:

  • The percentage of right sums to left sums was 54%/46%
  • Roughly 70% of a khipuโ€™s non-zero pendants are involved in being a sum or a summand.
  • 1.8 sums exist for every pendant cord (including 0 pendant cords)
  • The average handedness for a khipu tended towards 0, with some khipus having extreme handedness (indicative of misjoined khipus)

With these conclusions in mind letโ€™s compare the KFG pendant pendant sum distribution to a completely random distribution to see what we learn. Of special interest:

  • The number of sums per cord
  • The number of summands per sum
  • The total number of left vs right sums
  • The mean and std deviation of sum values
  • The mean and std deviation of sum handedness

To compare existing KFG khipus with simulated random khipus, we build a straw man khipu using either:

  • Values from the KFG (ie. the pendant cord values of each khipu) or
  • Values from a numerical distribution such as a uniform, bin sampled, or other distribution.

1. Building The Strawman Infrastructure

Since the strawman infrastructure is somewhat lengthy, it has been included at the end of this page.

2. Creating the Sample Distributions:

The obvious distribution to compare against is the Existing KFG:

  • Uniform - The simplest random distribution is a random uniform distribution between the min and max values of the KFG
  • Bin Sample - A distribution which very closely resembles that of the KFG is to pick cords at random from the existing KFG. This technique is also known as Bootstrapping
  • Jittered Bin - A distribution which more closely resembles that of the KFG is to pick cords at random from the existing KFG, and then add a random proportional offset (i.e. a jitter)

Thus we have one reference distribution, the KFG, and three random distributions, random uniform, random KFG bin samples and random jittered KFG bin samples. These distributions, and how they are created are detailed here.

Due to limits on page length, and on the amount of graphics plotly can draw, each of these distributions is created and examined on itโ€™s own separate page.

For each distribution we create the sample distribution and then compare it with the existing KFG distribution

3. Conclusions and Insights

Examining the distributions reveals the following insights:

  • The number of sums is a function of the number of pendants and the sample distribution

  • Random values sampled from a random distribution (uniform, jittered, etc.) are separable from the existing KFG distribution by having

    1. Larger mean sum values
    2. Sums that are farther away from the summands (ie. a larger handedness)
    3. Fewer summands
    4. Fewer sums per pendant.
  • When the values are sampled from the existing KFG the number of sums INCREASES. However these sums usually have very few summands (relative to the existing KFG distribution), and a high mean value, indicating it is a large number plus a small number.

  • Random values of any kind tend to have an equal number of left and right sums, unlike the existing KFG distribution which tends towards a rough 60/40 split.

4. Source Code for Examination

4.1 Pendant Summer

Code
# To build the Strawman Khipu we use the same Pendant Summer that the pendant-pendant sum fieldmark uses.
# We add the ability to calculate handedness based on cord position instead of cluster position
#+======================================================================================================================+
#|                                                                                                                      |
#|                                                 Pendant Summer Class                                                 |
#|                                                                                                                      |
#+======================================================================================================================+
class PendantSummer():
    """ Builds a list of contiguous sum ranges. 
        Given: an ordered list of pendant cord values (knotted values)
        Returns a list of tuples (summand index, (start index, end_index))  where the summand index is the index of the summand in the list of pendant cord values.
        and the start/end index is the range pendant_cord_values[start_index:end_index] (i.e. end_index is +1...)
        The tuples are organized by right handed sums, and left handed sums or are None if no sum is found.

        Use:
        foo = PendantSummer('MY_KHIPU_NAME', [1, 0,3,4...]) # pendant cord values
        foo.right_sums # Can be none
        foo.left_sums  # Can be none
    """
    def __init__(self, name, pendant_vals):
        self.name = name
        self.right_pendant_vals = np.array(pendant_vals)
        self.left_pendant_vals = np.array(pendant_vals[::-1])
        self.num_pendants = len(pendant_vals)
        
        ## Output values are stored here.
        (self.right_sums, self.right_handedness) = self.search_right_sums()
        (self.left_sums, self.left_handedness) = self.search_left_sums()

        self.num_left_sums = len(self.left_sums)
        self.num_right_sums = len(self.right_sums)
        self.num_sums = self.num_left_sums+self.num_right_sums


    def search_right_sums(self):
        self.right_sum_partials = [np.cumsum(self.right_pendant_vals[i:]) for i in range(0,self.num_pendants)]
        right_sums = {}
        
        for summand_index in range(self.num_pendants-2):
            # FILTER/REMOVE ANY SUMS <= 10
            if self.right_pendant_vals[summand_index] > 10:
                if found_search := self.search_sum_at(summand_index, self.right_pendant_vals, self.right_sum_partials): 
                    right_sums[summand_index] = found_search
                
        if right_sums: right_sums = self.filter_sums(right_sums)      
        if right_sums: right_sums = {key: val for key, val in sorted(right_sums.items(), key = lambda keyval: keyval[0])}

        right_handedness = []
        if right_sums:
            for (summand_index, sum_range) in right_sums.items():
                (start_index, end_index) = sum_range
                handed_distance = (start_index+(end_index-1))/2 - summand_index
                right_handedness.append(handed_distance)

        return (right_sums, right_handedness)
    
    def search_left_sums(self):
        self.left_sum_partials = [np.cumsum(self.left_pendant_vals[i:]) for i in range(0,self.num_pendants)]
        left_sums = {}
        
        for summand_index in range(self.num_pendants-2):
            # FILTER/REMOVE ANY SUMS <= 10
            if self.left_pendant_vals[summand_index] > 10:
                if found_search := self.search_sum_at(summand_index, self.left_pendant_vals, self.left_sum_partials): 
                    (reversed_start_index, reversed_end_index) = found_search
                    start_index = self.num_pendants-reversed_end_index
                    end_index = self.num_pendants-reversed_start_index
                    summand_index = (self.num_pendants-summand_index)-1
                    
                    left_sums[summand_index] = (start_index,end_index)

        if left_sums: left_sums = self.filter_sums(left_sums)      
        if left_sums: left_sums = {key: val for key, val in sorted(left_sums.items(), key = lambda keyval: keyval[0])}  
        
        left_handedness = []
        if left_sums:
            for (summand_index, sum_range) in left_sums.items():
                (start_index, end_index) = sum_range
                handed_distance = (start_index+(end_index-1))/2 - summand_index
                left_handedness.append(handed_distance)
        
        return (left_sums, left_handedness)

    
    def search_sum_at(self, summand_index, pendant_vals, sum_partials):
        search_val = pendant_vals[summand_index]
        for start_index in range(summand_index + 1, self.num_pendants):
            if pendant_vals[start_index] > 0:
                cum_sum_partial = np.where(sum_partials[start_index]==search_val)
                if len(cum_sum_partial[0]):
                    end_index = cum_sum_partial[0][0] + start_index + 1
                    return (start_index, end_index) if end_index-start_index >= 2 else None
            
        return None

    def filter_sums(self, sums):
        # Note that we have already filtered to remove any searches whose sum cord <= 10
        # And any searches whose cord = 0 
        filtered_sums = {}
        for (summand_index, sum_range) in sums.items():
            (start_index, end_index) = sum_range
            sum_cord = self.right_pendant_vals[summand_index]
            # Here we filter things like 5=1+1+1+1+1
            keep_sum = sum_cord > (end_index-start_index)
            if keep_sum:
                filtered_sums[summand_index] = sum_range
                
        return filtered_sums
    
    
def search_cords_that_sum(aKhipu):
    left_sum_cord_tuples = []
    right_sum_cord_tuples = []

    pendant_cords = aKhipu.pendant_cords()
    pendant_vals = [aCord.knotted_value() for aCord in pendant_cords]
    pendant_summer = PendantSummer(aKhipu.name(), pendant_vals)

    right_sum_cord_tuples =[]
    left_sum_cord_tuples =[]
    for (summand_index, sum_range) in pendant_summer.right_sums.items():
        right_sum_cord_tuples.append((pendant_cords[summand_index], pendant_cords[sum_range[0]:sum_range[1]]))
    for (summand_index, sum_range) in pendant_summer.left_sums.items():
        left_sum_cord_tuples.append((pendant_cords[summand_index], pendant_cords[sum_range[0]:sum_range[1]]))

    return (left_sum_cord_tuples, right_sum_cord_tuples)

4.2 Strawman Khipu

Code
from statistics import mean, stdev
class StrawmanKhipu:
    def __init__(self, name, source, pendant_values):
        self.name = name
        self.source = source
        self.pendant_values = pendant_values
        self.summer = PendantSummer(self.name, self.pendant_values)

    def num_nonzero_pendants(self):
        return len([aVal for aVal in self.pendant_values if aVal > 0])

    def num_sums(self):
        return self.summer.num_sums

    def num_right_sums(self):
        return self.summer.num_right_sums

    def num_left_sums(self):
        return self.summer.num_left_sums

    def num_sums_per_nonzero_pendant(self):
        return float(self.num_sums())/float(self.num_nonzero_pendants()) if self.num_nonzero_pendants() > 0 else 0.0
        
    def num_cords_per_sum(self):
        return float(self.num_nonzero_pendants())/float(self.num_sums()) if self.num_sums() > 0 else 0.0

    def mean_cord_value(self):
        the_mean = mean(self.pendant_values) if self.pendant_values else 0.0
        return the_mean

    def stdev_cord_value(self):
        the_stdev = stdev(self.pendant_values) if len(self.pendant_values)>1 else 0.0
        return the_stdev

    def mean_sum_value(self):
        if self.summer.right_sums:
            right_indices = self.summer.right_sums.keys() # Summand Indices
            right_cord_vals = [self.pendant_values[i] for i in right_indices]
        else:
            right_cord_vals = []
        if self.summer.left_sums:
            left_indices = self.summer.left_sums.keys() # Summand Indices
            left_cord_vals = [self.pendant_values[i] for i in left_indices]
        else:
            left_cord_vals = []

        all_cord_vals = right_cord_vals+left_cord_vals
        the_mean = mean(all_cord_vals) if all_cord_vals else 0.0
        return the_mean

    def stdev_sum_value(self):
        if self.summer.right_sums:
            right_indices = self.summer.right_sums.keys() # Summand Indices
            right_cord_vals = [self.pendant_values[i] for i in right_indices]
        else:
            right_cord_vals = []
        if self.summer.left_sums:
            left_indices = self.summer.left_sums.keys() # Summand Indices
            left_cord_vals = [self.pendant_values[i] for i in left_indices]
        else:
            left_cord_vals = []
        
        all_cord_vals = right_cord_vals+left_cord_vals
        the_stdev = stdev(all_cord_vals) if len(all_cord_vals)>1 else 0.0
        return the_stdev

    def mean_num_summands(self):
        num_summands = []
        for (summand_index, sum_range) in self.summer.left_sums.items():
            num_summands.append(sum_range[1]-sum_range[0])
        for (summand_index, sum_range) in self.summer.right_sums.items():
            num_summands.append(sum_range[1]-sum_range[0])
        return mean(num_summands) if num_summands else 0.0

    def stdev_num_summands(self):
        num_summands = []
        for (summand_index, sum_range) in self.summer.left_sums.items():
            num_summands.append(sum_range[1]-sum_range[0])
        for (summand_index, sum_range) in self.summer.right_sums.items():
            num_summands.append(sum_range[1]-sum_range[0])
        return stdev(num_summands) if len(num_summands)>1 else 0.0

    def mean_right_handedness(self):
        return mean(self.summer.right_handedness) if self.summer.right_handedness else 0.0

    def stdev_right_handedness(self):
        return stdev(self.summer.right_handedness) if len(self.summer.right_handedness)>1 else 0.0

    def mean_left_handedness(self):
        return mean(self.summer.left_handedness) if self.summer.left_handedness else 0.0

    def stdev_left_handedness(self):
        return stdev(self.summer.left_handedness) if len(self.summer.left_handedness)>1 else 0.0

    def dataframe_tuple(self):
        return (self.name, self.source, self.summer.num_pendants, self.mean_cord_value(), self.stdev_cord_value(), 
                self.num_right_sums(), self.num_left_sums(), self.num_sums(), self.mean_num_summands(),  self.stdev_num_summands(),
                self.mean_sum_value(), self.stdev_sum_value(), self.num_sums_per_nonzero_pendant(),
                self.mean_right_handedness(), self.stdev_right_handedness(),
                self.mean_left_handedness(), self.stdev_left_handedness())

    @staticmethod
    def dataframe_columns():
        return ["name", "source", "num_pendants", "mean_cord_value", "stdev_cord_value",
                        "num_right_sums", "num_left_sums", "num_sums", "mean_num_summands",  "stdev_num_summands",
                        "mean_sum_value", "stdev_sum_value", "num_sums_per_nonzero_pendant",  
                        "mean_right_handedness", "stdev_right_handedness", "mean_left_handedness", "stdev_left_handedness"]