#PCFG Inside Probability

from nltk import PCFG, InsideChartParser
grammar = PCFG.fromstring("""
S -> NP VP [1.0]
NP -> NP PP [0.4] | 'he' [0.1] | 'dessert' [0.3] | 'lunch' [0.1] | 'saw' [0.1]
PP -> Pre NP [1.0]
VP -> Verb NP [0.3] | VP PP [0.7]
Pre -> 'with' [0.6] | 'in' [0.4]
Verb -> 'ate' [0.7] | 'saw' [0.3]
""")
parser = InsideChartParser(grammar)
tokens = "he saw lunch with dessert".split()
for tree in parser.parse(tokens):
    tree.pretty_print()
    print("PROBABILITY: ",tree.prob())
    #tree.draw()
    
-----------------------------------

#7(b)

import pickle
from copy import deepcopy
class Rule:
    def __init__(self,left,right,probability):
        self.right = right
        self.left = left
        self.probability = probability
    
    def __str__(self):
        return f"{self.left}->{self.right} {self.probability}"
    
    def __repr__(self):
        return self.__str__()

def find_rule_with_word(terminal):
    a=[]
    for i in rules:
        for j in i.right:
            if j == terminal:
                a.append(i)
    return a

def loop_print(i):
    for j in i:
        print(j)


Z = int(input("Enter no of rules : "))
rules=[]
for i in range(Z):
    print("-"*10)
    print(f"Rule {i+1}")
    key = input("Enter left side of the rule : ").upper()
    value = input("Enter right side of the rule in single string : ").upper().split()
    probability = float(input("Enter probability of the rule : "))
    rule = Rule(key,value,probability)
    rules.append(rule)
    
pickle.dump(rules,open("rules.data","wb"))

# Comment if entering manually if not then load from file
rules = pickle.load(open("rules.data","rb"))
z = len(rules)
print(z)
for i in rules:
    print(i)

# Check condition for rules

non_terminals = ["S","NP","N","VP","V","PP","P"]
total_prob = {i:0 for i in non_terminals}
for i in rules:
    if i.left not in non_terminals:
        print(f"Left side should be non terminal. The rule {i.left}-> {i.right} {i.probability} breaks it")
    total_prob[i.left]+=i.probability


for i in total_prob:
    if total_prob[i] != 1.0 and total_prob[i] != 0:
        print(f"Total probability should be one. Its is {total_prob[i]} for {i}")

words = [input(f"Enter word {i+1} : ").upper() for i in range(int(input("Enter no of words : ")))]


def find_possible_combinations(x,y,debug=False):
    comb = []
    for i in range(x,y):
        if debug :print(f"{x}{i} {i+1}{y}")
        comb.append(((x,i),(i+1,y)))
    return comb
find_possible_combinations(0,4,debug=True)

matrix=[[[] for j in range(len(words))] for i in range(len(words))]
for i in range(len(words)):
    for j in range(len(words)):
        # Loop for diagonal loopings
        if i+j < 5:
            x=j
            y=i+j
            if x == y:
                matrix[x][y] = find_rule_with_word(words[x])
            else:
                comb = find_possible_combinations(x,y)
                data=[]
                for k in comb:
                    f = matrix[k[0][0]][k[0][1]]
                    g = matrix[k[1][0]][k[1][1]]
                    for p in f:
                        for q in g:
                            for r in rules:
                                if r.right == [p.left,q.left]:  
                                    z
                                    a = deepcopy(r)
                                    a.probability = p.probability*q.probability*r.probability
                                    data.append(a)
                                    
                matrix[x][y] = data

for i in range(5):
    for j in range(5):
        print(i,j,matrix[i][j])

matrix[0][len(words)-1]


def all_same(items):
    return all(x.left == items[0].left and x.right == items[0].right for x in items)

if all_same(matrix[0][len(words)-1]):
    a = deepcopy(matrix[0][len(words)-1][0])
    a.probability = sum([i.probability for i in matrix[0][len(words)-1]])
    matrix[0][len(words)-1] = [a]

#bayesian D
from collections import defaultdict
import math

# Training data
data = [
    (['bass', 'food', 'super', 'sea'], 'Fish'),
    (['bass', 'aquatic', 'life'], 'Fish'),
    (['bass', 'animal', 'pisces'], 'Fish'),
    (['bass', 'play', 'band', 'sound'], 'Music'),
    (['bass', 'fusion', 'instrument', 'guitar'], 'Music')
]

test_sentence = ['bass', 'drums', 'instrument', 'sound']

# Step 1: Vocabulary and Class counts
word_freq = {'Fish': defaultdict(int), 'Music': defaultdict(int)}
class_counts = defaultdict(int)
total_words = {'Fish': 0, 'Music': 0}

for words, label in data:
    class_counts[label] += 1
    for word in words:
        word_freq[label][word.lower()] += 1
        total_words[label] += 1

# Step 2: Prior probabilities
total_sentences = len(data)
priors = {c: class_counts[c]/total_sentences for c in class_counts}

# Step 3: Laplace-smoothed likelihoods
vocab = set()
for c in word_freq:
    vocab.update(word_freq[c].keys())
vocab_size = len(vocab)

def compute_prob(words, label):
    log_prob = math.log(priors[label])
    for word in words:
        count = word_freq[label][word.lower()]
        # Laplace smoothing
        prob = (count + 1) / (total_words[label] + vocab_size)
        log_prob += math.log(prob)
    return log_prob

scores = {label: compute_prob(test_sentence, label) for label in class_counts}
predicted_class = max(scores, key=scores.get)

print("Predicted class for sentence 6:", predicted_class)
print("Log scores:", scores)


# CYK Algorithm
import numpy as np
from collections import defaultdict

# Sentence
words = ["students", "play", "football", "with", "friends"]

# Grammar (in CNF): {Non-terminal: [(production, probability)]}
grammar = {
    'S': [('NN VP', 0.50), ('VP NP', 0.50)],
    'NP': [('NN PP', 0.70), ('PP NN', 0.30)],
    'VP': [('VB NP', 0.30), ('VB NN', 0.35), ('VP NP', 0.20), ('VP PP', 0.15)],
    'PP': [('P VP', 0.50), ('P NN', 0.50)],
    'P': [('with', 0.70), ('without', 0.30)],
    'VB': [('play', 0.20), ('watch', 0.20), ('enjoy', 0.15), ('like', 0.15), ('listen', 0.10)],
    'NN': [('children', 0.15), ('cricket', 0.15), ('friends', 0.20),
           ('students', 0.10), ('football', 0.15), ('painting', 0.25)]
}

# Invert grammar: terminal to non-terminals
term_to_nonterm = defaultdict(list)
for lhs, rules in grammar.items():
    for rhs, prob in rules:
        if rhs not in ['VB NP', 'NN PP', 'VP NP', 'VB NN', 'P VP', 'PP NN', 'VP PP', 'VP VP']:
            term_to_nonterm[rhs].append((lhs, prob))

# CYK algorithm
n = len(words)
table = [[defaultdict(float) for _ in range(n)] for _ in range(n)]
back = [[{} for _ in range(n)] for _ in range(n)]

# Fill diagonal
for i, word in enumerate(words):
    for lhs, prob in term_to_nonterm[word]:
        table[i][i][lhs] = prob

# Fill table
for l in range(2, n+1):  # length of span
    for i in range(n - l + 1):
        j = i + l - 1
        for k in range(i, j):
            for A in grammar:
                for rule, rule_prob in grammar[A]:
                    if len(rule.split()) == 2:
                        B, C = rule.split()
                        if B in table[i][k] and C in table[k+1][j]:
                            prob = rule_prob * table[i][k][B] * table[k+1][j][C]
                            if prob > table[i][j].get(A, 0):
                                table[i][j][A] = prob
                                back[i][j][A] = (k, B, C)

# Show parse probability
print("Probability of best parse as S:", table[0][n-1].get("S", 0))

# (Optional) Build the parse tree
def build_tree(i, j, symbol):
    if i == j:
        return (symbol, words[i])
    if symbol in back[i][j]:
        k, B, C = back[i][j][symbol]
        return (symbol, build_tree(i, k, B), build_tree(k+1, j, C))
    return (symbol,)

import pprint
pprint.pprint(build_tree(0, n-1, "S"))
