diff --git a/Data Structures and Algorithms/Sorting Algorithms/Cycle_Sort.py b/Data Structures and Algorithms/Sorting Algorithms/Cycle_Sort.py
new file mode 100644
index 00000000..1afd985f
--- /dev/null
+++ b/Data Structures and Algorithms/Sorting Algorithms/Cycle_Sort.py	
@@ -0,0 +1,50 @@
+from typing import List
+
+def cycle_sort(nums: List[int]) -> int:
+
+    writes = 0
+
+    for cycle_start in range(len(nums) - 1):
+        current = nums[cycle_start]
+
+        # Find the target position for the current item.
+        target_position = cycle_start
+        for i in range(cycle_start + 1, len(nums)):
+            if nums[i] < current:
+                target_position += 1
+
+        # Skip if the item is already in the correct position.
+        if target_position == cycle_start:
+            continue
+
+        # Handle duplicates by finding the next available position.
+        while current == nums[target_position]:
+            target_position += 1
+
+        nums[target_position], current = current, nums[target_position]
+        writes += 1
+
+        # Rotate the rest of the cycle.
+        while target_position != cycle_start:
+            target_position = cycle_start
+            for i in range(cycle_start + 1, len(nums)):
+                if nums[i] < current:
+                    target_position += 1
+
+            while current == nums[target_position]:
+                target_position += 1
+
+            nums[target_position], current = current, nums[target_position]
+            writes += 1
+
+    return writes
+
+
+if __name__ == "__main__":
+    arr = [1, 8, 3, 9, 10, 10, 2, 4]
+    print("Before sort:", arr)
+
+    writes = cycle_sort(arr)
+
+    print("After sort:", arr)
+    print(f"Number of writes: {writes}")
diff --git a/Data Structures and Algorithms/Sorting Algorithms/Pigeonhole_Sort.py b/Data Structures and Algorithms/Sorting Algorithms/Pigeonhole_Sort.py
new file mode 100644
index 00000000..11a60934
--- /dev/null
+++ b/Data Structures and Algorithms/Sorting Algorithms/Pigeonhole_Sort.py	
@@ -0,0 +1,33 @@
+# Python program to implement Pigeonhole Sort
+
+def pigeonhole_sort(a):
+	# size of range of values in the list 
+	# (ie, number of pigeonholes we need)
+	my_min = min(a)
+	my_max = max(a)
+	size = my_max - my_min + 1
+
+	# our list of pigeonholes
+	holes = [0] * size
+
+	# Populate the pigeonholes.
+	for x in a:
+		assert type(x) is int, "integers only"
+		holes[x - my_min] += 1
+
+	# Put the elements back into the array in order.
+	i = 0
+	for count in range(size):
+		while holes[count] > 0:
+			holes[count] -= 1
+			a[i] = count + my_min
+			i += 1
+			
+
+a = [8, 1, 2, 7, 4, 5, 8]
+print("Sorted order is : ", end = ' ')
+
+pigeonhole_sort(a)
+		
+for i in range(0, len(a)):
+	print(a[i], end = ' ')
diff --git a/Data Structures and Algorithms/Sorting Algorithms/README.md b/Data Structures and Algorithms/Sorting Algorithms/README.md
new file mode 100644
index 00000000..7ddfda4b
--- /dev/null
+++ b/Data Structures and Algorithms/Sorting Algorithms/README.md	
@@ -0,0 +1,49 @@
+# Cycle Sort Algorithm
+
+## Overview
+Cycle Sort is a comparison-based sorting algorithm that is efficient when minimizing memory writes is important. It is an in-place sorting algorithm that rearranges the elements by identifying cycles in the permutation of elements.
+
+## Algorithm Explanation
+The algorithm works by:
+1. Identifying the correct position of each element in the array.
+2. Placing the element in its correct position and replacing the element already there in the cycle.
+3. Repeating the process for the remaining unsorted elements.
+
+## Complexity
+- **Time Complexity**:
+  - Best, Worst, and Average Case: O(n²) (due to nested cycles).
+- **Space Complexity**: O(1) (in-place sorting).
+
+## Usage Example
+```python
+from Cycle_Sort import cycle_sort
+
+arr = [4, 5, 3, 2, 1]
+print("Original array:", arr)
+writes = cycle_sort(arr)
+print("Sorted array:", arr)
+print("Number of writes performed:", writes)
+```
+# Pigeonhole Sort Algorithm
+
+## Overview
+Pigeonhole Sort is a sorting algorithm that works well for sorting lists where the range of values (i.e., the difference between the maximum and minimum values) is not significantly larger than the number of elements in the list. It is a non-comparison-based sorting algorithm.
+
+The algorithm works by placing each element into its corresponding "pigeonhole" (a slot or bucket) and then iterating through the pigeonholes in order to reconstruct the sorted list.
+
+## Complexity
+- **Time Complexity**:
+  - The time complexity of Pigeonhole Sort is O(n + range), where n is the number of elements in the list and range is the difference between the maximum and minimum values.
+
+  - This makes it efficient for lists with a small range of values.
+- **Space Complexity**: The space complexity is O(range), as it requires additional space for the holes list.
+- **Limitations**: Pigeonhole Sort is not suitable for lists with a large range of values, as it would require a lot of memory for the holes list.
+
+## Usage Example
+```python
+from PigeonHole_Sort import pigeonhole_sort
+
+arr = [4, 5, 3, 2, 1]
+print("Original array:", arr)
+writes = pigeonhole_sort(arr)
+print("Sorted array:", arr)
diff --git a/Data Structures and Algorithms/queues.py b/Data Structures and Algorithms/queues.py
new file mode 100644
index 00000000..95f67510
--- /dev/null
+++ b/Data Structures and Algorithms/queues.py	
@@ -0,0 +1,27 @@
+class Queue:
+    def __init__(self):
+        self.queue = []
+
+    def enqueue(self, item):
+        self.queue.append(item)
+
+    def dequeue(self):
+        if not self.is_empty():
+            return self.queue.pop(0)
+        return "Queue is empty"
+
+    def is_empty(self):
+        return len(self.queue) == 0
+
+    def peek(self):
+        return self.queue[0] if not self.is_empty() else None
+
+    def size(self):
+        return len(self.queue)
+
+# Example Usage
+q = Queue()
+q.enqueue(10)
+q.enqueue(20)
+print(q.dequeue())  # Output: 10
+print(q.peek())     # Output: 20
diff --git a/Data Structures and Algorithms/union_find/README.md b/Data Structures and Algorithms/union_find/README.md
new file mode 100644
index 00000000..6f39cf37
--- /dev/null
+++ b/Data Structures and Algorithms/union_find/README.md	
@@ -0,0 +1,84 @@
+# Union Find (Disjoint Set Union) - Implementation and Use
+
+## Table of Contents
+- [Why Union Find?](#why-union-find)
+- [Functions and Examples](#functions-and-examples)
+- [Setup](#setup)
+- [Additional Resources](#additional-resources)
+- [Leetcode Questions](#leetcode-questions)
+
+## Why Union Find?
+Union Find is a popular data structure that allows us to solve many different types of graph
+problems. It works best with undirected graphs, and it allows us to figure out whether a node
+is connected to another node.
+
+Some problems it can be used to solve:
+- Find the minimum spanning tree in a graph (Kruskal's)
+- Check if there is a path between two nodes
+- Finding redundant edges 
+- Representing networks 
+
+
+## Functions and Examples
+Union Find seems complex at first, but it is actually a lot easier when you understand that there are 
+only two functions.
+- Find(n) : returns the parent of a node n
+- Union(n1, n2) : connects n1 and n2 if they are not previously connected
+
+Let's look at an example!  
+```python
+u = UnionFind(7)  # create a UnionFind object with 7 nodes (numbered 0 to 6)
+
+u.union(0, 1)     # connects 0 and 1 together
+u.union(5, 6)     # connects 5 and 6 together
+
+u.find(1)         # returns 0, since 0 is parent of 1
+u.find(5)         # returns 5, since 5 is its own parent
+
+u.union(1, 2)     # connects 2 to the component 0-1
+u.find(2)         # 2s parent is now 0
+
+# Now our structure looks like this
+
+# 0-1-2   3  4  5-6
+
+u.union(1, 6)    # first we find the parents of 1 and 6
+                 # parents are 0, and 5
+                 # connect the smaller component to the bigger
+                 # now 5's parent is 0
+
+u.find(6)       # now this goes:
+                # 6 parent is 5 -> 5 parent is 0 -> 0 is its own parent
+```
+
+And that's it! You can use the sample code to test different examples with Union Find.
+In the code, par keeps track of the parent of each node and rank keeps track of the size of 
+each component.
+
+## Setup
+
+First clone the repo
+ > `cd union_find` to get into this folder.   
+ >  call the verify function anywhere, consider adding ``` if __name__ == '__main__'```  
+ > `python union_find.py` to run the demo
+
+ You can modify the structure in the verify function and play around with it.
+
+ ## Additional Resources
+
+ Here are some resources I found useful when learning: 
+ - Neetcode Graph Videos on YouTube 
+ - William Fiset - Union Find Video on YouTube
+ - Union Find Medium Article by Claire Lee 
+ - Union Find Visualizer - Visualgo 
+
+ ## Leetcode Questions
+ - 200 - Number of Islands
+ - 684 - Redundant Connection
+ - 695 - Max Area of an Island
+ - 827 - Making a Large Island 
+ - 2316 - Count Unreachable Pairs of Nodes in an Undirected Graph
+ - 2421 - Maximum Score of a Good Path
+ - 2709 - Greatest Common Divisor Traversal 
+
+ I hope this was helpful. If there are any mistakes or issues or if you want to contribute to union find, feel free to contact me at rawateshaan0 [at] gmail [dot] com
\ No newline at end of file
diff --git a/Data Structures and Algorithms/union_find/union_find.py b/Data Structures and Algorithms/union_find/union_find.py
new file mode 100644
index 00000000..b01f53ca
--- /dev/null
+++ b/Data Structures and Algorithms/union_find/union_find.py	
@@ -0,0 +1,88 @@
+# Basic implementation of the Union Find data structure
+# Assume we have n nodes labeled from 0 to n - 1
+
+class UnionFind:
+    def __init__(self, n):
+        # every node is originally its own parent
+        self.par = [i for i in range(n)]    
+        # self.par = list(range(n)) -- also valid
+
+        # every node originally is in its own 
+        # component of size 1 - this changes during
+        # the union operation
+        self.rank = [1] * n
+
+    def find(self, n) -> int:
+        '''
+        Finds the parent node of n 
+        '''
+
+        # can be optimized with path compression
+        while n != self.par[n]:
+            n = self.par[n]
+        return n
+    
+
+    def union(self, n1, n2) -> bool:
+        '''
+        Connects two nodes together if not 
+        already connected
+        '''
+
+        # find the parent of node 1 and 2
+        p1 = self.find(n1)      
+        p2 = self.find(n2)
+
+        # nodes are already connected
+        # cannot union together
+        if p1 == p2:            
+            return False
+        
+        # for efficiency, make bigger component
+        # parent of smaller component - reduces
+        # number of steps we have to take in find()
+
+        if self.rank[p1] >= self.rank[p2]:
+            # p2 is smaller, so when union it has a
+            # new parent, p1
+            self.par[p2] = p1
+
+            # p1 gets all the nodes of p2, increasing
+            # its rank, or size
+            self.rank[p1] += self.rank[p2]
+        else:
+            self.par[p1] = p2
+            self.rank[p2] += self.rank[p1]
+
+        return True
+    
+    def nodes_connected(self, n1, n2) -> bool:
+        '''
+        Returns if two nodes are connected
+        '''
+
+        # connected if parent is the same
+        return self.find(n1) == self.find(n2)
+    
+
+
+def verify():
+    n = 7
+    u = UnionFind(n)
+
+    # False, nodes not connected
+    print(u.nodes_connected(0, 1))
+
+    # True, just connected 0 and 1
+    u.union(0, 1)
+    print(u.nodes_connected(0, 1))
+
+    # Rank is 2, includes 0 and 1
+    print(u.rank[0])
+
+    u.union(4, 5)
+    u.union(1, 4)
+
+    # True, 0 - 1 and 4 - 5 are connected
+    # 1 to 4 connects both components
+    print(u.nodes_connected(0, 5))
\ No newline at end of file
diff --git a/FLASK PROJECTS/Anniversary time/README.md b/FLASK PROJECTS/Anniversary time/README.md
new file mode 100644
index 00000000..5a415f7a
--- /dev/null
+++ b/FLASK PROJECTS/Anniversary time/README.md	
@@ -0,0 +1,3 @@
+# Anniversary Timing
+
+Simple timing page implemented using flask
diff --git a/FLASK PROJECTS/Anniversary time/app.py b/FLASK PROJECTS/Anniversary time/app.py
new file mode 100644
index 00000000..434405a0
--- /dev/null
+++ b/FLASK PROJECTS/Anniversary time/app.py	
@@ -0,0 +1,17 @@
+from flask import Flask, render_template
+from datetime import datetime
+
+app = Flask(__name__)
+
+# 在此定义纪念日日期
+anniversary_date = datetime(2024, 6, 16)
+
+@app.route('/')
+def index():
+    current_date = datetime.now()
+    delta = current_date - anniversary_date
+    days_passed = delta.days
+    return render_template('index.html', days_passed=days_passed, anniversary_date=anniversary_date.strftime("%Y-%m-%d %H:%M:%S"))
+
+if __name__ == '__main__':
+    app.run(debug=False)
diff --git a/FLASK PROJECTS/Anniversary time/static/background.jpg b/FLASK PROJECTS/Anniversary time/static/background.jpg
new file mode 100644
index 00000000..a6c19a97
Binary files /dev/null and b/FLASK PROJECTS/Anniversary time/static/background.jpg differ
diff --git a/FLASK PROJECTS/Anniversary time/static/style.css b/FLASK PROJECTS/Anniversary time/static/style.css
new file mode 100644
index 00000000..16fae375
--- /dev/null
+++ b/FLASK PROJECTS/Anniversary time/static/style.css	
@@ -0,0 +1,32 @@
+body {
+    margin: 0;
+    padding: 0;
+    font-family: Arial, sans-serif;
+    background: url('background.jpg') no-repeat center center fixed;
+    background-size: cover;
+    color: white;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 100vh;
+}
+
+.container {
+    text-align: center;
+    background-color: rgba(0, 0, 0, 0.5);
+    padding: 20px;
+    border-radius: 10px;
+}
+
+h1 {
+    font-size: 3em;
+}
+
+.time {
+    font-size: 2em;
+    margin-top: 20px;
+}
+
+.time span {
+    font-weight: bold;
+}
diff --git a/FLASK PROJECTS/Anniversary time/templates/index.html b/FLASK PROJECTS/Anniversary time/templates/index.html
new file mode 100644
index 00000000..0afa920f
--- /dev/null
+++ b/FLASK PROJECTS/Anniversary time/templates/index.html	
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Anniversary</title>
+    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
+    <script>
+        function updateTimer() {
+            const anniversaryDate = new Date("{{ anniversary_date }}");
+            const currentDate = new Date();
+            const timeDiff = currentDate - anniversaryDate;
+
+            const days = Math.floor(timeDiff / (1000 * 60 * 60 * 24));
+            const hours = Math.floor((timeDiff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
+            const minutes = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60));
+            const seconds = Math.floor((timeDiff % (1000 * 60)) / 1000);
+
+            document.getElementById("days").innerText = days;
+            document.getElementById("hours").innerText = hours;
+            document.getElementById("minutes").innerText = minutes;
+            document.getElementById("seconds").innerText = seconds;
+        }
+
+        setInterval(updateTimer, 1000);
+    </script>
+</head>
+<body onload="updateTimer()">
+    <div class="container">
+        <h1>It has passed the xx anniversary</h1>
+        <div class="time">
+            <span id="days">0</span> 天
+            <span id="hours">0</span> 小时
+            <span id="minutes">0</span> 分钟
+            <span id="seconds">0</span> 秒
+        </div>
+    </div>
+</body>
+</html>
diff --git a/INVESTMENT_RULES/README.md b/INVESTMENT_RULES/README.md
new file mode 100644
index 00000000..99198579
--- /dev/null
+++ b/INVESTMENT_RULES/README.md
@@ -0,0 +1,28 @@
+# List of python scripts that auomates calculation of really handful of Investment Rules.
+
+## 1. What Is the Rule of 72? 
+    The Rule of 72 is a simple way to determine how long an investment will take to double given a fixed annual rate of interest. Dividing 72 by the annual rate of return gives investors a rough estimate of how many years it will take for the initial investment to duplicate itself. 
+
+### Key takeaways 
+    The Rule of 72 is not precise, but is a quick way to get a useful ballpark figure.
+    For investments without a fixed rate of return, you can instead divide 72 by the number of years you hope it will take to double your money. This will give you an estimate of the annual rate of return you’ll need to achieve that goal.
+    The calculation is most accurate for rates of return of about 5% to 10%.
+    For more precise outcomes, divide 69.3 by the rate of return. While not as easy to do in one’s head, it is more accurate.
+
+### How the Rule of 72 Works
+    For example, the Rule of 72 states that $1 invested at an annual fixed interest rate of 10% would take 7.2 years ((72 ÷ 10) = 7.2) to grow to $2. 
+
+For more details refer https://www.investopedia.com/ask/answers/what-is-the-rule-72/
+
+## 2. Real Rate of Return adjusted to Inflation
+    You know that investments have to do more than keep pace with inflation for you to build wealth. As Golden says, 
+    “A dollar today is not worth a dollar in the future.” But how do you determine what your investment return is after inflation?
+    This equation helps you compute your real return, or your return adjusted for inflation. 
+
+    For example, if an investment returns 8 percent, and inflation is 3 percent, this is how you’d set up the problem:
+    [ ( 1.08 ÷ 1.03 ) - 1 ] x 100 = 4.85 percent real return
+    
+    “You’re losing to inflation every year,” says Charles Sachs, a wealth manager at Kaufman Rossin Wealth in Miami.
+    “Long term, inflation runs about 3 percent. So your money buys half as much in 20 years.”
+
+    Learn more here--> https://finance.yahoo.com/news/6-investment-formulas-financial-success-172744221.html
\ No newline at end of file
diff --git a/INVESTMENT_RULES/inflation_adjusted_return.py b/INVESTMENT_RULES/inflation_adjusted_return.py
new file mode 100644
index 00000000..6a59cd2f
--- /dev/null
+++ b/INVESTMENT_RULES/inflation_adjusted_return.py
@@ -0,0 +1,18 @@
+Inflation_Adjsted_Return_Summary = """
+Learn More about this investment rule in README.md located in INVESTMENT_RULES folder** 
+ """
+
+print(Inflation_Adjsted_Return_Summary)
+
+# Get the Avg Investment Rate of Return and Avg Inflation Rate
+invest_rate_return = float(input("What is expected average  Rate of Return (don't use % sign): "))/100
+avg_inflration_rate = float(input("What is your avg inflation rate?: "))/100
+
+
+def inflation_adjusted_return(invest_rate_return, avg_inflration_rate):
+    # Simple formula is : ((1 + Investment return percentage) / (1 + Inflation rate percentage) - 1) x 100
+    inflration_adjusted_return_val = (((1 +invest_rate_return )/(1 +avg_inflration_rate)) - 1) * 100
+    return inflration_adjusted_return_val
+
+real_return = round(inflation_adjusted_return(invest_rate_return, avg_inflration_rate),2)
+print(f"Your Actual Rate of Return adjusted to the inflation is {real_return}%. Not {invest_rate_return*100}% ")
\ No newline at end of file
diff --git a/INVESTMENT_RULES/rule_of_72.py b/INVESTMENT_RULES/rule_of_72.py
new file mode 100644
index 00000000..f1b4f1f3
--- /dev/null
+++ b/INVESTMENT_RULES/rule_of_72.py
@@ -0,0 +1,11 @@
+# Get Aannual fixed interest rate
+fixed_interest_rate = input("Please enter the Annual Fixed interest rate (don't use % sign): ")
+
+
+def calculate_time_taken_to_double(fixed_interest_rate):
+    # A simple formula calulate the time it takes to double an investment.
+    time_taken = 72/float(fixed_interest_rate)
+    return time_taken
+
+time_taken_to_double = round(calculate_time_taken_to_double(fixed_interest_rate),2)
+print(f"Your investment will take {time_taken_to_double} year(s) to double!")
\ No newline at end of file
diff --git a/MachineLearning Projects/sudoku_solver/README.md b/MachineLearning Projects/sudoku_solver/README.md
new file mode 100644
index 00000000..4829dc59
--- /dev/null
+++ b/MachineLearning Projects/sudoku_solver/README.md	
@@ -0,0 +1,28 @@
+# Sudoku Solver
+
+* This app was built to allow users to solve their sudokus using a computer.
+* There is a Flask based webserver `web_interface.py` which when run gives a web interface to upload an image of a sudoku to be solved. The response is a solved sudoku.
+* There is a file `full_stack_http.py` which needs to be run alongside the webserver for the full app to run. This is in charge of opening multiple process channels to process the images that are sent to the webserver.
+* The app relies of Pytesseract to identify the characters in the sudoku image.
+
+# Operation
+
+* The image is first stripped of color.
+* It is then cropped to select the section of the sudoku. NOTE: This section is not dependent on the sudoku but has been hardcoded.
+* The resulting image is passed to `Pytesseract` to extract the characters and their position.
+* Using the characters and their position the grid size is determined.
+* The appropriate grid is created and filled with the discovered characters.
+* The grid is then solved with an algorithm contained in `sudoku.py`.
+* A snapshot of the solved grid is then created and sent back to the user.
+* The resultant snapshot is rendered on the browser page.
+
+# To Run
+
+* First install `Pytesseract`
+* Install `Flask`
+* Then run the `full_stack_http.py` file.
+* Then run the `web_interface.py` file.
+* Go to the browser and load the URL provided in the previous step.
+* Click the upload button.
+* Select your image and submit the form.
+* Wait for the result to be loaded.
\ No newline at end of file
diff --git a/MachineLearning Projects/sudoku_solver/__pycache__/image.cpython-311.pyc b/MachineLearning Projects/sudoku_solver/__pycache__/image.cpython-311.pyc
new file mode 100644
index 00000000..bc46b3c9
Binary files /dev/null and b/MachineLearning Projects/sudoku_solver/__pycache__/image.cpython-311.pyc differ
diff --git a/MachineLearning Projects/sudoku_solver/__pycache__/perspective.cpython-312.pyc b/MachineLearning Projects/sudoku_solver/__pycache__/perspective.cpython-312.pyc
new file mode 100644
index 00000000..73b55c0f
Binary files /dev/null and b/MachineLearning Projects/sudoku_solver/__pycache__/perspective.cpython-312.pyc differ
diff --git a/MachineLearning Projects/sudoku_solver/__pycache__/sudoku.cpython-312.pyc b/MachineLearning Projects/sudoku_solver/__pycache__/sudoku.cpython-312.pyc
new file mode 100644
index 00000000..d76e94c2
Binary files /dev/null and b/MachineLearning Projects/sudoku_solver/__pycache__/sudoku.cpython-312.pyc differ
diff --git a/MachineLearning Projects/sudoku_solver/config.cfg b/MachineLearning Projects/sudoku_solver/config.cfg
new file mode 100644
index 00000000..93d8c2b5
--- /dev/null
+++ b/MachineLearning Projects/sudoku_solver/config.cfg	
@@ -0,0 +1,4 @@
+UPLOAD_FOLDER="uploads"
+SECRET_KEY="secret"
+SOLVER_IP="localhost"
+SOLVER_PORT=3535
\ No newline at end of file
diff --git a/MachineLearning Projects/sudoku_solver/f1.jpg b/MachineLearning Projects/sudoku_solver/f1.jpg
new file mode 100644
index 00000000..12c2702e
Binary files /dev/null and b/MachineLearning Projects/sudoku_solver/f1.jpg differ
diff --git a/MachineLearning Projects/sudoku_solver/f2.jpg b/MachineLearning Projects/sudoku_solver/f2.jpg
new file mode 100644
index 00000000..dc232b54
Binary files /dev/null and b/MachineLearning Projects/sudoku_solver/f2.jpg differ
diff --git a/MachineLearning Projects/sudoku_solver/full_stack_http.py b/MachineLearning Projects/sudoku_solver/full_stack_http.py
new file mode 100644
index 00000000..d6232b81
--- /dev/null
+++ b/MachineLearning Projects/sudoku_solver/full_stack_http.py	
@@ -0,0 +1,136 @@
+import multiprocessing.util
+import socket
+from perspective import resolve_image
+from sudoku import Grid
+import argparse
+import multiprocessing
+import os
+
+temp_result_file = "resultfile.png"
+temp_input_file = "tempfile.jpg"
+
+def process_handle_transaction(proc_num:int, sock:socket.socket):
+    print(f"[{proc_num}] Waiting for client...")
+    sock2, address2 = sock.accept()
+    print(f"[{proc_num}] Connected to client with address: {address2}")
+    sock2.settimeout(1)
+    rec_buf = b''
+    split = temp_input_file.split('.')
+    my_temp_input_file = ".".join(i for i in split[:-1]) + str(proc_num) + "." + split[-1]
+    split = temp_result_file.split('.')
+    my_temp_result_file = ".".join(i for i in split[:-1]) + str(proc_num) + "." + split[-1]
+    try:
+        while True:
+            try:
+                rec = sock2.recv(1)
+                rec_buf += rec
+                if len(rec) == 0:
+                    print(f"[{proc_num}] Lost connection")
+                    break
+            except socket.timeout:
+                with open(my_temp_input_file, "wb") as f:
+                    f.write(rec_buf)
+                rec_buf = b''
+                grid_size, points = resolve_image(my_temp_input_file)
+                grid = Grid(rows=grid_size[0], columns=grid_size[1])
+                assignment_values = {}
+                for val,loc in points:
+                    assignment_values[loc] = val
+                grid.preassign(assignment_values)
+                grid.solve()
+                grid.save_grid_image(path=my_temp_result_file, size=(400,400))
+                with open(my_temp_result_file, "rb") as f:
+                    sock2.send(f.read())
+                os.remove(my_temp_input_file)
+                os.remove(my_temp_result_file)
+                sock2.close()
+                print(f"[{proc_num}] Finished!")
+                break
+    finally:
+        sock2.close()
+
+class Manager():
+    def __init__(self, address:tuple[str,int]):
+        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        self.address = address
+    
+    def wait_for_connect(self):
+        print("Waiting for client...")
+        self.sock2, self.address2 = self.sock.accept()
+        print(f"Connected to client with address: {self.address2}")
+        self.sock2.settimeout(1)
+    
+    def run(self):
+        self.sock.bind(self.address)
+        self.sock.listen()
+        print(f"Listening from address: {self.address}")
+        try:
+            while True:
+                self.wait_for_connect()
+                rec_buf = b''
+                while True:
+                    try:
+                        rec = self.sock2.recv(1)
+                        rec_buf += rec
+                        if len(rec) == 0:
+                            print("Lost connection")
+                            break
+                    except socket.timeout:
+                        with open(temp_input_file, "wb") as f:
+                            f.write(rec_buf)
+                        rec_buf = b''
+                        grid_size, points = resolve_image(temp_input_file)
+                        grid = Grid(rows=grid_size[0], columns=grid_size[1])
+                        assignment_values = {}
+                        for val,loc in points:
+                            assignment_values[loc] = val
+                        grid.preassign(assignment_values)
+                        grid.solve()
+                        grid.save_grid_image(path=temp_result_file, size=(400,400))
+                        with open(temp_result_file, "rb") as f:
+                            self.sock2.send(f.read())
+                        os.remove(temp_input_file)
+                        os.remove(temp_result_file)
+                        self.sock2.close()
+                        break
+        finally:
+            try:
+                self.sock2.close()
+            except socket.error:
+                pass
+            except AttributeError:
+                pass
+            self.sock.close()
+    
+    def run_multiprocessing(self, max_clients:int=8):
+        self.sock.bind(self.address)
+        self.sock.listen()
+        print(f"Listening from address: {self.address}")
+        processes:dict[int,multiprocessing.Process]= {}
+        proc_num = 0
+        try:
+            while True:
+                if len(processes) <= max_clients:
+                    proc = multiprocessing.Process(target=process_handle_transaction, args=(proc_num, self.sock))
+                    proc.start()
+                    processes[proc_num] = proc
+                    proc_num += 1
+                    proc_num%=(max_clients*2)
+                keys = list(processes.keys())
+                for proc_n in keys:
+                    if not processes[proc_n].is_alive():
+                        processes.pop(proc_n)
+        finally:
+            if len(processes):
+                for proc in processes.values():
+                    proc.kill()
+            self.sock.close()
+
+if "__main__" == __name__:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--port", type=int, default=3535, help="The port to host the server.")
+    parser.add_argument("--host", type=str, default="localhost", help="The host or ip-address to host the server.")
+    args = parser.parse_args()
+    address = (args.host, args.port)
+    manager = Manager(address)
+    manager.run_multiprocessing(max_clients=multiprocessing.cpu_count())
\ No newline at end of file
diff --git a/MachineLearning Projects/sudoku_solver/image.py b/MachineLearning Projects/sudoku_solver/image.py
new file mode 100644
index 00000000..24ca83d4
--- /dev/null
+++ b/MachineLearning Projects/sudoku_solver/image.py	
@@ -0,0 +1,141 @@
+import torch
+from torch.utils.data import Dataset, DataLoader
+import PIL.Image as Image
+import pandas as pd
+from tqdm import tqdm
+import numpy as np
+
+
+class SudokuDataset(Dataset):
+    def __init__(self, grid_locations_file:str, input_shape:tuple[int, int]) -> None:
+        super().__init__()
+        self.grid_locations = []
+        self.image_filenames = []
+        self.input_shape = input_shape
+        self.all_data = pd.read_csv(grid_locations_file, header=0)
+        self.image_filenames = list(self.all_data['filepath'].to_numpy())
+        self.grid_locations = [list(a[1:]) for a in self.all_data.values]
+        to_pop = []
+        for i,file in enumerate(self.image_filenames):
+            try:
+                Image.open(file)
+            except FileNotFoundError:
+                to_pop.append(i)
+                print(f"{file} not found.")
+        for i in reversed(to_pop):
+            self.image_filenames.pop(i)
+            self.grid_locations.pop(i)
+        # print(self.all_data.columns)
+        # print(self.grid_locations)
+    
+    def __len__(self) -> int:
+        return len(self.image_filenames)
+
+    def __getitem__(self, index) -> dict[str, torch.Tensor]:
+        image = Image.open(self.image_filenames[index]).convert("L")
+        size = image.size
+        image = image.resize(self.input_shape)
+        image = np.array(image)
+        image = image.reshape((1,*image.shape))
+        location = self.grid_locations[index]
+        for i in range(len(location)):
+            if i%2:
+                location[i] /= size[1]
+            else:
+                location[i] /= size[0]
+        return {
+            "image": torch.tensor(image, dtype=torch.float32)/255.,
+            "grid": torch.tensor(location, dtype=torch.float32)
+        }
+
+class Model(torch.nn.Module):
+    def __init__(self, input_shape:tuple[int,int], number_of_layers:int, dims:int, *args, **kwargs) -> None:
+        super().__init__(*args, **kwargs)
+        self.input_shape = input_shape
+        self.conv_layers:list = []
+        self.conv_layers.append(torch.nn.Conv2d(1, dims, (3,3), padding='same'))
+        for _ in range(number_of_layers-1):
+            self.conv_layers.append(torch.nn.Conv2d(dims, dims, (3,3), padding='same'))
+            self.conv_layers.append(torch.nn.LeakyReLU(negative_slope=0.01))
+            self.conv_layers.append(torch.nn.MaxPool2d((2,2)))
+            self.conv_layers.append(torch.nn.BatchNorm2d(dims))
+        self.flatten = torch.nn.Flatten()
+        self.location = [
+            torch.nn.Linear(4107, 8),
+            torch.nn.Sigmoid()
+        ]
+        self.conv_layers = torch.nn.ModuleList(self.conv_layers)
+        self.location = torch.nn.ModuleList(self.location)
+    
+    def forward(self, x:torch.Tensor) -> torch.Tensor:
+        for layer in self.conv_layers:
+            x = layer(x)
+        x = self.flatten(x)
+        location = x
+        for layer in self.location:
+            location = layer(location)
+        return location
+    
+def create_model(input_shape:tuple[int,int], number_of_layers:int, dims:int):
+    model = Model(input_shape, number_of_layers, dims)
+    for p in model.parameters():
+        if p.dim() > 1:
+            torch.nn.init.xavier_uniform_(p)
+    return model
+
+def get_dataset(filename:str, input_shape:tuple[int,int], batch_size:int) -> DataLoader:
+    train_dataset = SudokuDataset(filename, input_shape)
+    train_dataloader = DataLoader(train_dataset, batch_size, shuffle=True)
+    return train_dataloader
+
+def train(epochs:int, config:dict, model:None|Model = None) -> Model:
+    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
+    if not model:
+        print("========== Using new model =========")
+        model = create_model(config['input_shape'], config['number_of_layers'], config['dims']).to(device)
+    optimizer = torch.optim.Adam(model.parameters(), lr=config['lr'])
+    loss = torch.nn.MSELoss().to(device)
+    dataset = get_dataset(config['filename'], config['input_shape'], config['batch_size'])
+    prev_error = 0
+    try:
+        for epoch in range(1, epochs+1):
+            batch_iterator = tqdm(dataset, f"Epoch {epoch}/{epochs}:")
+            for batch in batch_iterator:
+                x = batch['image'].to(device)
+                y_true = batch['grid'].to(device)
+                # print(batch['grid'])
+                # return
+                y_pred = model(x)
+                error = loss(y_true, y_pred)
+                batch_iterator.set_postfix({"loss":f"Loss: {error.item():6.6f}"})
+                error.backward()
+                optimizer.step()
+                # optimizer.zero_grad()
+            if abs(error-0.5) < 0.05:# or (prev_error-error)<0.000001:
+                del(model)
+                model = create_model(config['input_shape'], config['number_of_layers'], config['dims']).to(device)
+                print("New model created")
+            prev_error = error
+    except KeyboardInterrupt:
+        torch.save(model, "model.pt")
+    return model
+
+def test(config:dict, model_filename:str):
+    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
+    model = torch.load("model.pt").to(device)
+    loss = torch.nn.MSELoss().to(device)
+    dataset = get_dataset(config['filename'], config['input_shape'], config['batch_size'])
+    
+
+if __name__ == '__main__':
+    config = {
+        "input_shape": (300,300),
+        "filename": "archive/outlines_sorted.csv",
+        "number_of_layers": 4,
+        "dims": 3,
+        "batch_size": 8,
+        "lr": 1e-5
+    }
+    # model = train(50, config)
+    model = torch.load("model.pt")
+    test(config, model)
\ No newline at end of file
diff --git a/MachineLearning Projects/sudoku_solver/model.pt b/MachineLearning Projects/sudoku_solver/model.pt
new file mode 100644
index 00000000..ba421a06
Binary files /dev/null and b/MachineLearning Projects/sudoku_solver/model.pt differ
diff --git a/MachineLearning Projects/sudoku_solver/perspective.py b/MachineLearning Projects/sudoku_solver/perspective.py
new file mode 100644
index 00000000..3c79c78d
--- /dev/null
+++ b/MachineLearning Projects/sudoku_solver/perspective.py	
@@ -0,0 +1,98 @@
+import cv2
+import numpy as np
+from pytesseract import pytesseract as pt
+
+def resolve_perspective(source_image:np.ndarray, points:np.ndarray, target_shape:tuple[int,int]) -> np.ndarray:
+    """Takes an source image and transforms takes the region demarkated by points and creates a rectangular image of target.
+
+    Args:
+        source_image (np.ndarray): the source image.
+        points (np.ndarray): a numpy array of 4 points that will demarkate the vertices of the region to be transformed.\n
+        \tShould be in the form of points from the point that would be transformed to the top left of the rectangle, clockwise
+        target_shape (tuple[int,int]): the target shape of the rectangular output image. Format [height, width].
+
+    Returns:
+        np.ndarray: the output image transformed
+    """
+    output_points:np.ndarray = np.array([
+        [0,0],
+        [target_shape[0]-1, 0],
+        [target_shape[0]-1, target_shape[1]-1],
+        [0,target_shape[1]-1]
+        ], dtype=np.float32)
+    transformation_matrix:cv2.typing.MatLike = cv2.getPerspectiveTransform(points.astype(np.float32), output_points)
+    output:cv2.typing.MatLike = cv2.warpPerspective(source_image, transformation_matrix, (target_shape[1], target_shape[0]), flags=cv2.INTER_LINEAR)
+    return output
+
+def get_grid_size(image:np.ndarray, boxes:list[list[int]], allowed_sizes:list[tuple[int,int]]=[(2,3),(3,3),(4,4)]) -> tuple[int,int]:
+    h,w = image.shape
+    for size in allowed_sizes:
+        s1 = float(w)/float(size[0])
+        s2 = float(h)/float(size[1])
+        for box in boxes:
+            _,x1,y1,x2,y2 = box
+            if (abs(int(x1/s1) - int(x2/s1)) + abs(int((h - y1)/s2) - int((h - y2)/s2))) > 0:
+                break
+        else:
+            return size
+        
+def get_points(image:np.ndarray, boxes:list[list[int]], grid_size:tuple[int,int]) -> list[tuple[int,tuple]]:
+    h,w = image.shape
+    size = grid_size[0] * grid_size[1]
+    s1 = float(w)/float(size)
+    s2 = float(h)/float(size)
+    results = []
+    for box in boxes:
+        val,x1,y1,x2,y2 = box
+        center_x = int((x1+x2)/2)
+        center_y = int((y1+y2)/2)
+        results.append((val, (int((h-center_y)/s2), int(center_x/s1))))
+    return results
+
+def resolve_image(path:str) -> tuple[tuple,list[tuple[int,tuple]]]:
+    # img = cv2.imread("images/image210.jpg")
+    img = cv2.imread(path)
+    numbers = [str(i) for i in range(10)]
+    max_size = 500
+    min_area = 150
+    *img_shape,_ = img.shape
+    max_ind = np.argmax(img_shape)
+    min_ind = np.argmin(img_shape)
+    next_shape = [0,0]
+    if max_ind != min_ind:
+        next_shape[max_ind] = max_size
+        next_shape[min_ind] = int(img_shape[min_ind]*max_size/img_shape[max_ind])
+    else:
+        next_shape = [max_size, max_size]
+    img = cv2.resize(img, tuple(reversed(next_shape)))
+    points = np.array([6,97,219,99,216,309,7,310])
+    points = points.reshape((4,2))
+    target_shape = (400,400)
+    output = resolve_perspective(img, points, target_shape)
+    output = cv2.cvtColor(output, cv2.COLOR_BGR2GRAY)
+    norm_img = np.zeros((output.shape[0], output.shape[1]))
+    output = cv2.normalize(output, norm_img, 0, 255, cv2.NORM_MINMAX)
+    output1 = cv2.threshold(output, 140, 255, cv2.THRESH_BINARY_INV)[1]
+    if np.average(output1.flatten()) > 128:
+        output = cv2.threshold(output, 140, 255, cv2.THRESH_BINARY)[1]
+    else:
+        output = output1
+    output = cv2.GaussianBlur(output, (1,1), 0)
+    boxes = pt.image_to_boxes(output, "eng", config=r'-c tessedit_char_whitelist=0123456789 --psm 13 --oem 3')
+    print(boxes)
+    h,w = output.shape
+    new_boxes_str = ""
+    new_boxes = []
+    for bt in boxes.splitlines():
+        b = bt.split(' ')
+        area = (int(b[1]) - int(b[3]))*(int(b[2]) - int(b[4]))
+        if b[0] in numbers and area > min_area:
+            output = cv2.rectangle(output, (int(b[1]), h - int(b[2])), (int(b[3]), h - int(b[4])), (255, 255, 255), 2)
+            new_boxes_str += bt + "\n"
+            new_boxes.append(list(int(i) for i in b[:5]))
+    grid_size = get_grid_size(output, new_boxes)
+    final_points = get_points(output, new_boxes, grid_size)
+    return grid_size,final_points
+
+if "__main__" == __name__:
+    print(resolve_image("f2.jpg"))
\ No newline at end of file
diff --git a/MachineLearning Projects/sudoku_solver/resultfile2_server.png b/MachineLearning Projects/sudoku_solver/resultfile2_server.png
new file mode 100644
index 00000000..d6af2f3c
Binary files /dev/null and b/MachineLearning Projects/sudoku_solver/resultfile2_server.png differ
diff --git a/MachineLearning Projects/sudoku_solver/sudoku.py b/MachineLearning Projects/sudoku_solver/sudoku.py
new file mode 100644
index 00000000..fa5dc225
--- /dev/null
+++ b/MachineLearning Projects/sudoku_solver/sudoku.py	
@@ -0,0 +1,373 @@
+"""This python script contains a class to solve a sudoku.
+"""
+
+from copy import deepcopy
+import pygame as pg
+
+VISITED_COLOR = (50,50,50)
+AGENT_COLOR = (255,0,0)
+BOARD_COLOR = (0,0,0)
+WALL_COLOR = (255,255,255)
+SOLUTION_COLOR = (0,255,0)
+START_CELL_COLOR = (200,0,200)
+END_CELL_COLOR = (0,128,128)
+
+SIZE = (600,600)
+
+class Cell():
+    """Cell element of sudoku.
+    """
+    def __init__(self, name:int|str, domain:list[int]) -> None:
+        """Initialise a cell of the sudoku.
+
+        Args:
+            name (int): the actual cell position.
+            domain (list[int]): list of all the possible values the cell can take.
+        """
+        self.name = name
+        self.value:int|str = None
+        self.domain:list[int] = deepcopy(domain)
+    
+class Grid():
+    """The actual sudoku grid.
+    """
+    def __init__(self, rows:int|None = None, columns:int|None = None) -> None:
+        """Initialise the sudoku grid.
+
+        Args:
+            rows (int | None, optional): The number of rows in a block eg 3 for a 9x9 sudoku. Defaults to None.
+            columns (int | None, optional): The number of columns in a block. Defaults to None.
+        """
+        self.rows = rows
+        self.columns = columns
+        if not self.rows or not self.columns:
+            return
+        self.grid_len = self.rows * self.columns
+        self.domain:list[int] = [i for i in range(1, min(10, self.grid_len+1))]
+        if self.grid_len >= 10:
+            self.domain.extend(chr(ord('A') + i - 10) for i in range(10, self.grid_len+1))
+        self.cells:list[Cell] = [Cell(i, self.domain) for i in range(self.grid_len * self.grid_len)]
+        self.unsolved_cells:list[int] = [i for i in range(self.grid_len * self.grid_len)]
+        self.solved_cells:list[int] = []
+        self.initial_solved:list[int] = []
+        self.initial_unsolved:list[int] = [i for i in range(self.grid_len * self.grid_len)]
+    
+    def preassign(self, values:dict[tuple, int]) -> None:
+        """Preassigns particular value to the cells already given in the problem.
+
+        Args:
+            values (dict[tuple, int]): a dictionary with keys of the (row,column) and value of the actual value of the cell.
+        """
+        for i, value in values.items():
+            number = int(i[0]*self.grid_len + i[1])
+            if number in self.initial_solved:
+                self.unassign_last(number)
+            self.cells[number].value = value
+            self.cells[number].domain = []
+            self.unsolved_cells.remove(number)
+            self.initial_unsolved.remove(number)
+            self.initial_solved.append(number)
+            self.solved_cells.append(number)
+    
+    def unassign_last(self, number:int|None = None):
+        """Unassigns either the last value assigned to a cell or a particular cell given by number.
+
+        Args:
+            number (int | None, optional): The number of the cell in the grid, starting from 0 at the top right and moving left. Defaults to None.
+        """
+        if not number:
+            number = self.solved_cells.pop()
+            self.initial_solved.pop()
+        else:
+            self.solved_cells.remove(number)
+            self.initial_solved.remove(number)
+        self.unsolved_cells.append(number)
+        self.initial_unsolved.append(number)
+        self.cells[number].domain = deepcopy(self.domain)
+        self.cells[number].value = None
+    
+    def solve(self) -> None:
+        """Tries to solve the sudoku.
+        """
+        while len(self.unsolved_cells) > 0:
+            changed = False
+            i = 0
+            # first update domains based on known cells
+            while i < len(self.solved_cells):
+                val = self.cells[self.solved_cells[i]].value
+                r,c = int(self.solved_cells[i]/self.grid_len), int(self.solved_cells[i]%self.grid_len)
+                # first check cells on the same row
+                for j in range(r*self.grid_len, (r+1)*self.grid_len):
+                    try:
+                        self.cells[j].domain.remove(val)
+                        if len(self.cells[j].domain) == 1:
+                            self.cells[j].value = self.cells[j].domain[0]
+                            self.cells[j].domain = []
+                            self.unsolved_cells.remove(j)
+                            self.solved_cells.append(j)
+                        changed = True
+                        i = -1
+                    except ValueError:
+                        pass
+                # next check cells on the same column
+                for k in range(self.grid_len):
+                    j = k*self.grid_len + c
+                    try:
+                        self.cells[j].domain.remove(val)
+                        if len(self.cells[j].domain) == 1:
+                            self.cells[j].value = self.cells[j].domain[0]
+                            self.cells[j].domain = []
+                            self.unsolved_cells.remove(j)
+                            self.solved_cells.append(j)
+                        changed = True
+                        i = -1
+                    except ValueError:
+                        pass
+                # next check cells on the same block
+                br = int(r/self.rows)
+                bc = int(c/self.columns)
+                for k in range(self.grid_len):
+                    cr = br*self.rows + int(k/self.columns)
+                    cc = bc*self.columns + int(k%self.columns)
+                    j = cr*self.grid_len + cc
+                    try:
+                        self.cells[j].domain.remove(val)
+                        if len(self.cells[j].domain) == 1:
+                            self.cells[j].value = self.cells[j].domain[0]
+                            self.cells[j].domain = []
+                            self.unsolved_cells.remove(j)
+                            self.solved_cells.append(j)
+                        changed = True
+                        i = -1
+                    except ValueError:
+                        pass
+                i += 1
+            # next check for unique value in domains of cells in row column or block
+            # first check rows
+            to_break = False
+            for k in range(self.grid_len):
+                values:dict[int|str, list[int]] = {val:[] for val in self.domain}
+                for m in range(self.grid_len):
+                    j = k*self.grid_len + m
+                    for v in self.cells[j].domain:
+                        values[v].append(j)
+                for val,ls in values.items():
+                    if len(ls) == 1:
+                        self.cells[ls[0]].value = val
+                        self.cells[ls[0]].domain = []
+                        self.unsolved_cells.remove(ls[0])
+                        self.solved_cells.append(ls[0])
+                        to_break = True
+                        break
+                if to_break:
+                    break
+            if to_break:
+                continue
+            # first check columns
+            to_break = False
+            for k in range(self.grid_len):
+                values:dict[int|str, list[int]] = {val:[] for val in self.domain}
+                for m in range(self.grid_len):
+                    j = m*self.grid_len + k
+                    for v in self.cells[j].domain:
+                        values[v].append(j)
+                for val,ls in values.items():
+                    if len(ls) == 1:
+                        self.cells[ls[0]].value = val
+                        self.cells[ls[0]].domain = []
+                        self.unsolved_cells.remove(ls[0])
+                        self.solved_cells.append(ls[0])
+                        to_break = True
+                        break
+                if to_break:
+                    break
+            if to_break:
+                continue
+            if not changed:
+                return
+    
+    def render_cells(self, window:pg.Surface) -> None:
+        """Draws the grid and populates it with the value of the cells.
+
+        Args:
+            window (pg.Surface): a pygame window to be used to populate the grid and cells.
+        """
+        size = window.get_size()
+        py = int(size[1] / self.grid_len)
+        px = int(size[0] / self.grid_len)
+        ball = pg.Rect(0, 0, size[0], size[1])
+        pg.draw.rect(window, BOARD_COLOR, ball)
+        for i in range(self.grid_len+1):
+            if i%self.columns:
+                pg.draw.line(window, VISITED_COLOR, (i*px, 0), (i*px, size[1]))
+            else:
+                pg.draw.line(window, WALL_COLOR, (i*px, 0), (i*px, size[1]))
+            if i%self.rows:
+                pg.draw.line(window, VISITED_COLOR, (0, i*py), (size[0], i*py))
+            else:
+                pg.draw.line(window, WALL_COLOR, (0, i*py), (size[0], i*py))
+        font = pg.font.SysFont(None, min(py, px))
+        for i in self.initial_solved:
+            text = font.render(str(self.cells[i].value), True, AGENT_COLOR, BOARD_COLOR)
+            textRect = text.get_rect()
+            y = int(i/self.grid_len)
+            x = int(i%self.grid_len)
+            textRect.center = (int((x+0.5)*px),int((y+0.5)*py))
+            window.blit(text, textRect)
+        for i in self.initial_unsolved:
+            if val:=self.cells[i].value:
+                text = font.render(str(val), True, SOLUTION_COLOR, BOARD_COLOR)
+                textRect = text.get_rect()
+                y = int(i/self.grid_len)
+                x = int(i%self.grid_len)
+                textRect.center = (int((x+0.5)*px),int((y+0.5)*py))
+                window.blit(text, textRect)
+            # else:
+            #     for dv in self.cells[i].domain:
+            #         text = font.render(str(val), True, SOLUTION_COLOR, BOARD_COLOR)
+            #         textRect = text.get_rect()
+            #         y = int(i/self.grid_len)
+            #         x = int(i%self.grid_len)
+            #         textRect.center = (int((x+0.5)*px),int((y+0.5)*py))
+            #         window.blit(text, textRect)
+    
+    def render_grid(self, size:tuple[int, int]=SIZE) -> None:
+        """Creates the grid window and renders it.
+
+        Args:
+            size (tuple[int, int], optional): The size of the window to be used. Defaults to (600,600).
+        """
+        pg.init()
+        window = pg.display.set_mode(size)
+        window.fill(BOARD_COLOR)
+        while True:
+            for event in pg.event.get():
+                if event.type == pg.QUIT:
+                    pg.display.quit()
+                    return
+            self.render_cells(window)
+            pg.display.update()
+    
+    def input_to_grid(self, size:tuple[int, int]=SIZE) -> None:
+        """Allows for input of the value of the grid cells by clicking on a cell and typing the value.
+
+        Args:
+            size (tuple[int, int], optional): The size of the window to which the grid will be rendered. Defaults to (600,600).
+        """
+        pg.init()
+        window = pg.display.set_mode(size)
+        window.fill(BOARD_COLOR)
+        size = window.get_size()
+        py = int(size[1] / self.grid_len)
+        px = int(size[0] / self.grid_len)
+        clicked_cell = None
+        while True:
+            for event in pg.event.get():
+                if event.type == pg.QUIT:
+                    pg.display.quit()
+                    return
+                if event.type == pg.MOUSEBUTTONUP:
+                    clicked_cell = event.dict['pos']
+                if event.type == pg.KEYDOWN:
+                    key = event.dict['unicode']
+                    if key >= '0' and key <= '9':
+                        if clicked_cell:
+                            pos = (int(clicked_cell[1] / py), int(clicked_cell[0] / px))
+                            if int(key) <= self.grid_len:
+                                self.preassign({pos:int(key)})
+                    elif key >= 'A' and key <= 'Z':
+                        if clicked_cell:
+                            pos = (int(clicked_cell[1] / py), int(clicked_cell[0] / px))
+                            if (ord(key) - ord('A') + 10) <= self.grid_len:
+                                self.preassign({pos:key})
+                    elif key == ' ':
+                        self.unassign_last()
+            self.render_cells(window)
+            pg.display.update()
+    
+    def save(self, filename:str) -> None:
+        """Saves the current state of the grid in a file.\n
+        Save format is:\n
+        rows,columns\n
+        (cell_number,cell_value)|(cell_number,cell_value)|...|(cell_number,cell_value)\n
+        (cell_number,cell_value)|(cell_number,cell_value)|...|(cell_number,cell_value)\n
+        \n
+        where the second line is the initial cell values before trying to solve\n
+        \t the third line is the initially unsolved cell values after solving if Grid.solve() has been run\n
+
+        Args:
+            filename (str): The path of the file to be saved to.
+        """
+        s = f"{self.rows},{self.columns}\n"
+        s += "|".join(f"({a},{self.cells[a].value})" for a in self.initial_solved)
+        s += "\n"
+        s += "|".join(f"({a},{self.cells[a].value})" for a in self.initial_unsolved)
+        with open(filename, 'w') as f:
+            f.write(s)
+            f.close()
+    
+    def load(self, filename:str):
+        """Loads the grid from a saved state file created by calling Grid.save(filename)
+
+        Args:
+            filename (str): The path to the file containing the grid status to be loaded.
+        """
+        with open(filename, 'r') as f:
+            for i,line in enumerate(f):
+                line = line.replace("\n","")
+                if i == 0:
+                    rows, columns = line.replace("(","").replace(")","").split(",")
+                    self.rows = int(rows)
+                    self.columns = int(columns)
+                elif i == 1:
+                    initial_solved_pairs = [tuple(int(i) for i in a.split(",")) for a in line.replace("(","").replace(")","").split("|")]
+                elif i == 2:
+                    initial_unsolved_pairs = [tuple(eval(i) for i in a.split(",")) for a in line.replace("(","").replace(")","").split("|")]
+            f.close()
+        self.grid_len = self.rows * self.columns
+        self.domain:list[int] = [i for i in range(1, min(10, self.grid_len+1))]
+        if self.grid_len >= 10:
+            self.domain.extend(chr(ord('A') + i - 10) for i in range(10, self.grid_len+1))
+        self.cells:list[Cell] = [Cell(i, self.domain) for i in range(self.grid_len * self.grid_len)]
+        self.unsolved_cells:list[int] = [i for i in range(self.grid_len * self.grid_len)]
+        self.solved_cells:list[int] = []
+        self.initial_solved:list[int] = []
+        self.initial_unsolved:list[int] = [i for i in range(self.grid_len * self.grid_len)]
+        for (number,value) in initial_solved_pairs:
+            self.initial_solved.append(number)
+            self.solved_cells.append(number)
+            self.cells[number].value = value
+            self.cells[number].domain = []
+            self.initial_unsolved.remove(number)
+            self.unsolved_cells.remove(number)
+        for (number,value) in initial_unsolved_pairs:
+            if value:
+                self.solved_cells.append(number)
+                self.cells[number].value = value
+                self.cells[number].domain = []
+                self.unsolved_cells.remove(number)
+    
+    def save_grid_image(self, path:str, size:tuple[int, int]=SIZE) -> None:
+        pg.init()
+        window = pg.display.set_mode(size)
+        window.fill(BOARD_COLOR)
+        self.render_cells(window)
+        pg.image.save(window, path)
+        pg.quit()
+          
+def main():
+    r = int(input("Enter number of rows in a block: "))
+    c = int(input("Enter number of columns in a block: "))
+    grid = Grid(r,c)
+    # grid = Grid()
+    # grid.load("s2.txt")
+    grid.input_to_grid()
+    grid.save("s3.txt")
+    grid.solve()
+    grid.save("s3.txt")
+    # grid = Grid()
+    # grid.load("s1.txt")
+    grid.render_grid()
+  
+if __name__ == "__main__":
+    main()
diff --git a/MachineLearning Projects/sudoku_solver/temp.ipynb b/MachineLearning Projects/sudoku_solver/temp.ipynb
new file mode 100644
index 00000000..0737eb93
--- /dev/null
+++ b/MachineLearning Projects/sudoku_solver/temp.ipynb	
@@ -0,0 +1,230 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 22,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class Node:\n",
+    "    def __init__(self,val):\n",
+    "        self.val = val\n",
+    "        self.to = {}"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 137,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class Node:\n",
+    "    def __init__(self,val):\n",
+    "        self.val:int = val\n",
+    "        self.to:dict[Node,tuple[int,int]] = {} # destinationNode:(steps,price)\n",
+    "    \n",
+    "    def __str__(self) -> str:\n",
+    "        children = ','.join(str(i.val) for i in self.to.keys())\n",
+    "        return f\"Node({self.val})\"\n",
+    "    \n",
+    "    def __repr__(self) -> str:\n",
+    "        children = ','.join(str(i.val) for i in self.to.keys())\n",
+    "        return f\"Node({self.val})\"\n",
+    "    \n",
+    "    def full(self) -> str:\n",
+    "        children = ','.join(str(i.val) for i in self.to.keys())\n",
+    "        return f\"Node({self.val})->[{children}]\"\n",
+    "\n",
+    "def update(node:Node, start:list[int]):\n",
+    "    # print(\"iter\", node, start)\n",
+    "    if node.val in start:\n",
+    "        # print(\"found: \", node, \" => \", start)\n",
+    "        return {}\n",
+    "    ret:dict[Node,set[tuple[int,int]]] = {\n",
+    "            i:set([tuple(node.to[i]),]) for i in node.to.keys()\n",
+    "        } # destinationNode:[(steps1,price1), (steps2,price2), ...]\n",
+    "    for destinationNode,(steps,price) in node.to.items():\n",
+    "        # print(f\"step {node} to {destinationNode}\")\n",
+    "        returned = update(destinationNode, [*start,node.val])\n",
+    "        # print(f\"{node.val} going to {destinationNode.val} got {returned}\")\n",
+    "        if returned == {}:\n",
+    "            # print(f\"here on\")\n",
+    "            ret[destinationNode].add((steps,price))\n",
+    "            continue\n",
+    "        for v,mylist in returned.items():\n",
+    "            # v is the a possible destination from our destination node\n",
+    "            # my list is a list of the steps and prices to that possible destination\n",
+    "            for (stp,prc) in mylist:\n",
+    "                newTuple = (stp+steps,prc+price)\n",
+    "                if ret.get(v):\n",
+    "                    ret[v].add(newTuple)\n",
+    "                else:\n",
+    "                    ret[v] = set([newTuple,])\n",
+    "    return ret"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 176,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from cmath import inf\n",
+    "\n",
+    "def findCheapestPrice(n: int, flights: list[list[int]], src: int, dst: int, k: int) -> int:\n",
+    "    nodes:dict[int,Node] = {}\n",
+    "    for s,d,p in flights:\n",
+    "        dnode = nodes.get(d)\n",
+    "        if dnode:\n",
+    "            snode = nodes.get(s)\n",
+    "            if snode:\n",
+    "                snode.to[dnode] = (1,p)\n",
+    "            else:\n",
+    "                nd = Node(s)\n",
+    "                nd.to[dnode] = (1,p)\n",
+    "                nodes[s] = nd\n",
+    "        else:\n",
+    "            snode = nodes.get(s)\n",
+    "            if snode:\n",
+    "                nd = Node(d)\n",
+    "                snode.to[nd] = (1,p)\n",
+    "                nodes[d] = nd\n",
+    "            else:\n",
+    "                nd1 = Node(s)\n",
+    "                nd2 = Node(d)\n",
+    "                nd1.to[nd2] = (1,p)\n",
+    "                nodes[s] = nd1\n",
+    "                nodes[d] = nd2\n",
+    "    for _,node in nodes.items():\n",
+    "        print(node.full())\n",
+    "    return method2(nodes, src, dst, k)\n",
+    "\n",
+    "def method1(nodes:dict[int,Node], src:int, dst:int, k:int) -> int:\n",
+    "    results = {}\n",
+    "    for val,node in nodes.items():\n",
+    "        ret = update(node, [])\n",
+    "        results[val] = ret\n",
+    "    desired = results[src].get(nodes[dst])\n",
+    "    if not desired:\n",
+    "        return -1\n",
+    "    filtered = []\n",
+    "    k = k + 1\n",
+    "    for d in desired:\n",
+    "        if d[0] <= k:\n",
+    "            filtered.append(d)\n",
+    "    return min(filtered, key=lambda x:x[1])\n",
+    "\n",
+    "def method2(nodes:dict[int,Node], src:int, dst:int, k:int) -> int:\n",
+    "    def recurse(node:Node, dst:int, k:int, visited:list[int]):\n",
+    "        results = []\n",
+    "        if k == 1:\n",
+    "            for nd in node.to.keys():\n",
+    "                if nd.val == dst:\n",
+    "                    return node.to[nd][1]\n",
+    "            return inf\n",
+    "        if node.val in visited:\n",
+    "            return inf\n",
+    "        for nd in node.to.keys():\n",
+    "            if nd.val == dst:\n",
+    "                results.append(node.to[nd][1])\n",
+    "            else:\n",
+    "                temp = recurse(nd, dst, k-1, [*visited, node.val]) + node.to[nd][1]\n",
+    "                results.append(temp)\n",
+    "        if len(results):\n",
+    "            return min(results)\n",
+    "        return inf\n",
+    "    \n",
+    "    k = k+1\n",
+    "    node = nodes[src]\n",
+    "    result = recurse(node, dst, k, [])\n",
+    "    if result == inf:\n",
+    "        return -1\n",
+    "    return result"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 157,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "100"
+      ]
+     },
+     "execution_count": 157,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "findCheapestPrice(n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 1)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 178,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Node(0)->[12,8,15,10]\n",
+      "Node(12)->[4,3,14,13,9,0,16,6]\n",
+      "Node(5)->[6,14,13,16,10,9,7]\n",
+      "Node(6)->[14,10,2,12]\n",
+      "Node(8)->[6,10,11,9,2,13,3]\n",
+      "Node(13)->[15,12,6,16,0,5,11,7,8]\n",
+      "Node(15)->[3,0,6,13,12,11,14,2]\n",
+      "Node(10)->[12,2,15,11,5,4,9,0,7]\n",
+      "Node(3)->[4,12,5,6,7,10]\n",
+      "Node(7)->[11,3,1,14,0,12,2]\n",
+      "Node(11)->[16,1,0,2,6,9]\n",
+      "Node(9)->[4,6,1,12,7,10,15,5]\n",
+      "Node(4)->[7,9,8,5,11,10]\n",
+      "Node(2)->[12,0,11,5,13,10,7]\n",
+      "Node(14)->[15,1,9,7,11,6]\n",
+      "Node(16)->[4,12,1,3,8,11,9,14]\n",
+      "Node(1)->[11,4,3,7]\n"
+     ]
+    },
+    {
+     "data": {
+      "text/plain": [
+       "47"
+      ]
+     },
+     "execution_count": 178,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "findCheapestPrice(n = 4, flights = [[0,12,28],[5,6,39],[8,6,59],[13,15,7],[13,12,38],[10,12,35],[15,3,23],[7,11,26],[9,4,65],[10,2,38],[4,7,7],[14,15,31],[2,12,44],[8,10,34],[13,6,29],[5,14,89],[11,16,13],[7,3,46],[10,15,19],[12,4,58],[13,16,11],[16,4,76],[2,0,12],[15,0,22],[16,12,13],[7,1,29],[7,14,100],[16,1,14],[9,6,74],[11,1,73],[2,11,60],[10,11,85],[2,5,49],[3,4,17],[4,9,77],[16,3,47],[15,6,78],[14,1,90],[10,5,95],[1,11,30],[11,0,37],[10,4,86],[0,8,57],[6,14,68],[16,8,3],[13,0,65],[2,13,6],[5,13,5],[8,11,31],[6,10,20],[6,2,33],[9,1,3],[14,9,58],[12,3,19],[11,2,74],[12,14,48],[16,11,100],[3,12,38],[12,13,77],[10,9,99],[15,13,98],[15,12,71],[1,4,28],[7,0,83],[3,5,100],[8,9,14],[15,11,57],[3,6,65],[1,3,45],[14,7,74],[2,10,39],[4,8,73],[13,5,77],[10,0,43],[12,9,92],[8,2,26],[1,7,7],[9,12,10],[13,11,64],[8,13,80],[6,12,74],[9,7,35],[0,15,48],[3,7,87],[16,9,42],[5,16,64],[4,5,65],[15,14,70],[12,0,13],[16,14,52],[3,10,80],[14,11,85],[15,2,77],[4,11,19],[2,7,49],[10,7,78],[14,6,84],[13,7,50],[11,6,75],[5,10,46],[13,8,43],[9,10,49],[7,12,64],[0,10,76],[5,9,77],[8,3,28],[11,9,28],[12,16,87],[12,6,24],[9,15,94],[5,7,77],[4,10,18],[7,2,11],[9,5,41]], src = 13, dst = 4, k = 13)"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "base",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.12.4"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/MachineLearning Projects/sudoku_solver/temp.py b/MachineLearning Projects/sudoku_solver/temp.py
new file mode 100644
index 00000000..553f0531
--- /dev/null
+++ b/MachineLearning Projects/sudoku_solver/temp.py	
@@ -0,0 +1,2 @@
+while True:
+    pass
\ No newline at end of file
diff --git a/MachineLearning Projects/sudoku_solver/tempfile1.jpg b/MachineLearning Projects/sudoku_solver/tempfile1.jpg
new file mode 100644
index 00000000..7e398e54
Binary files /dev/null and b/MachineLearning Projects/sudoku_solver/tempfile1.jpg differ
diff --git a/MachineLearning Projects/sudoku_solver/tempfile2.jpg b/MachineLearning Projects/sudoku_solver/tempfile2.jpg
new file mode 100644
index 00000000..b0069382
Binary files /dev/null and b/MachineLearning Projects/sudoku_solver/tempfile2.jpg differ
diff --git a/MachineLearning Projects/sudoku_solver/templates/index.html b/MachineLearning Projects/sudoku_solver/templates/index.html
new file mode 100644
index 00000000..8a812ec4
--- /dev/null
+++ b/MachineLearning Projects/sudoku_solver/templates/index.html	
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Sudoku Solver</title>
+</head>
+<body>
+    <h1>Sudoku Solver</h1>
+    <hr>
+    <h3>To solve a sudoku select the image of the sudoku and upload it to the page then hit submit.</h3>
+    <h3>The solution will be returned as an image on the next page.</h3>
+    <div>
+        <form action="" method="post" enctype="multipart/form-data">
+            <input type="file" name="image" id="image" value={{request.files.image}}>
+            <input type="submit" value="Submit" >
+        </form>
+    </div>
+</body>
+</html>
\ No newline at end of file
diff --git a/MachineLearning Projects/sudoku_solver/templates/result.html b/MachineLearning Projects/sudoku_solver/templates/result.html
new file mode 100644
index 00000000..94fbedb1
--- /dev/null
+++ b/MachineLearning Projects/sudoku_solver/templates/result.html	
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Solution</title>
+</head>
+<body>
+    <hr>
+    <a href="/">Back to Main Page</a>
+    <hr>
+    <div>
+        <h1>Solution</h1>
+    </div>
+    <div>
+        <img src="{{img}}" alt="img" style="height: max-content; width: max-content;">
+    </div>
+</body>
+</html>
\ No newline at end of file
diff --git a/MachineLearning Projects/sudoku_solver/test_full_stack.py b/MachineLearning Projects/sudoku_solver/test_full_stack.py
new file mode 100644
index 00000000..a24ab068
--- /dev/null
+++ b/MachineLearning Projects/sudoku_solver/test_full_stack.py	
@@ -0,0 +1,31 @@
+import socket
+
+result_file = "resultfile2_server.png"
+input_file = "f1.jpg"
+
+def main(address:tuple[str,int]):
+    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    sock.connect(address)
+    sock.settimeout(10)
+    with open(input_file, "rb") as f:
+        sock.send(f.read())
+    res_buf = b''
+    try:
+        while True:
+            try:
+                res = sock.recv(1)
+                res_buf += res
+                if 0 == len(res):
+                    sock.close()
+                    with open(result_file, "wb") as f:
+                        f.write(res_buf)
+                    break
+            except socket.timeout:
+                with open(result_file, "wb") as f:
+                    f.write(res_buf)
+                break
+    finally:
+        sock.close()
+
+if "__main__" == __name__:
+    main(("localhost", 3535))
\ No newline at end of file
diff --git a/MachineLearning Projects/sudoku_solver/verify_image.py b/MachineLearning Projects/sudoku_solver/verify_image.py
new file mode 100644
index 00000000..1ad57615
--- /dev/null
+++ b/MachineLearning Projects/sudoku_solver/verify_image.py	
@@ -0,0 +1,110 @@
+"""This code is to verify the image dataset and check that all the labels of the grid location are in the correct place.
+"""
+
+import PIL.Image as Image
+from matplotlib import pyplot as plt
+import numpy as np
+from image import SudokuDataset, get_dataset, tqdm, Model
+import torch
+
+img_size = (300,300)
+
+def mark(positions, image, color_value):
+    print(positions)
+    print(image.shape)
+    x0,y0,x1,y1,x2,y2,x3,y3 = positions
+    image = image.transpose()
+    grad = (y1 - y0)/(x1 - x0)
+    if x1 > x0:
+        for i in range(x1 - x0):
+            image[x0 + i, int(y0 + i * grad)] = color_value
+    else:
+        for i in range(x0 - x1):
+            image[x0 - i, int(y0 - i * grad)] = color_value
+    
+    grad = (y2 - y1)/(x2 - x1)
+    if x2 > x1:
+        for i in range(x2 - x1):
+            image[x1 + i, int(y1 + i * grad)] = color_value
+    else:
+        for i in range(x1 - x2):
+            image[x1 - i, int(y1 - i * grad)] = color_value
+    
+    grad = (y3 - y2)/(x3 - x2)
+    if x3 > x2:
+        for i in range(x3 - x2):
+            image[x2 + i, int(y2 + i * grad)] = color_value
+    else:
+        for i in range(x2 - x3):
+            image[x2 - i, int(y2 - i * grad)] = color_value
+    
+    grad = (y0 - y3)/(x0 - x3)
+    if x0 > x3:
+        for i in range(x0 - x3):
+            image[x3 + i, int(y3 + i * grad)] = color_value
+    else:
+        for i in range(x3 - x0):
+            image[x3 - i, int(y3 - i * grad)] = color_value
+    return image.transpose()
+
+# dataset = SudokuDataset("./archive/outlines_sorted.csv", img_size)
+# for item in dataset:
+#     try:
+#         image = item['image']
+#         grid = item['grid']
+#         x0,y0,x1,y1,x2,y2,x3,y3 = list(grid.numpy())
+#         x0 = int(x0 * img_size[0])
+#         x1 = int(x1 * img_size[0])
+#         x2 = int(x2 * img_size[0])
+#         x3 = int(x3 * img_size[0])
+#         y0 = int(y0 * img_size[1])
+#         y1 = int(y1 * img_size[1])
+#         y2 = int(y2 * img_size[1])
+#         y3 = int(y3 * img_size[1])
+#         image = mark((x0,y0,x1,y1,x2,y2,x3,y3), image.numpy()[0], 0.7)
+#         plt.imshow(image)
+#         plt.colorbar()
+#         plt.show()
+#     except KeyboardInterrupt:
+#         break
+
+def test(config:dict, model_filename:str):
+    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
+    model = torch.load(model_filename).to(device)
+    model.eval()
+    loss = torch.nn.MSELoss().to(device)
+    dataset = get_dataset(config['filename'], config['input_shape'], config['batch_size'])
+    batch_iterator = tqdm(dataset)
+    for batch in batch_iterator:
+        x = batch['image'].to(device)
+        y_true = batch['grid'].to(device)
+        # print(batch['grid'])
+        # return
+        y_pred = model(x)
+        error = loss(y_true, y_pred)
+        batch_iterator.set_postfix({"loss":f"Loss: {error.item():6.6f}"})
+        x0,y0,x1,y1,x2,y2,x3,y3 = list(y_pred.detach().numpy()[1])
+        print(x0,y0,x1,y1,x2,y2,x3,y3)
+        x0 = int(x0 * img_size[0])
+        x1 = int(x1 * img_size[0])
+        x2 = int(x2 * img_size[0])
+        x3 = int(x3 * img_size[0])
+        y0 = int(y0 * img_size[1])
+        y1 = int(y1 * img_size[1])
+        y2 = int(y2 * img_size[1])
+        y3 = int(y3 * img_size[1])
+        image = mark((x0,y0,x1,y1,x2,y2,x3,y3), x.detach().numpy()[0][0], 0.7)
+        plt.imshow(image)
+        plt.colorbar()
+        plt.show()
+
+config = {
+    "input_shape": (300,300),
+    "filename": "archive/outlines_sorted.csv",
+    "number_of_layers": 4,
+    "dims": 3,
+    "batch_size": 8,
+    "lr": 1e-5
+}
+# model = train(50, config)
+test(config, "model.pt")
\ No newline at end of file
diff --git a/MachineLearning Projects/sudoku_solver/web_interface.py b/MachineLearning Projects/sudoku_solver/web_interface.py
new file mode 100644
index 00000000..6bc14604
--- /dev/null
+++ b/MachineLearning Projects/sudoku_solver/web_interface.py	
@@ -0,0 +1,129 @@
+from flask import Flask, render_template, redirect, url_for, request, flash, session
+from werkzeug.utils import secure_filename
+import os
+from random import choices, choice
+from string import ascii_letters, digits
+from time import sleep
+from datetime import datetime
+import socket
+
+app = Flask(__name__)
+
+app.config.from_pyfile("config.cfg")
+
+def manage_solution(input_file, result_file) -> int:
+    def send(input_file:str, sock:socket.socket) -> int:
+        try:
+            with open(input_file, "rb") as f:
+                sock.send(f.read())
+            return 1
+        except FileNotFoundError:
+            return -2
+        except socket.error:
+            return -1
+    
+    def connect() -> socket.socket:
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        sock.connect((app.config['SOLVER_IP'], int(app.config['SOLVER_PORT'])))
+        sock.settimeout(10)
+        return sock
+    
+    def manage_full_send(input_file:str, sock:socket.socket):
+        tries = 0
+        while tries < 5:
+            send_state = send(input_file, sock)
+            if send_state == 1:
+                break
+            elif send_state == -2:
+                return -2
+            elif send_state == -1:
+                sock = connect()
+            tries += 1
+        return send_state
+    
+    sock = connect()
+    send_state = manage_full_send(input_file, sock)
+    if send_state == -1:
+        return -1
+    elif send_state == -2:
+        return -2
+    res_buf = b''
+    try:
+        while True:
+            try:
+                res = sock.recv(1)
+                res_buf += res
+                if 0 == len(res):
+                    sock.close()
+                    with open(result_file, "wb") as f:
+                        f.write(res_buf)
+                    break
+            except socket.timeout:
+                with open(result_file, "wb") as f:
+                    f.write(res_buf)
+                break
+    finally:
+        sock.close()
+        return 0
+
+@app.route('/', methods=['POST', 'GET'])
+def index():
+    if "POST" == request.method:
+        print(request)
+        if 'image' not in request.files:
+            flash('No file part.', "danger")
+        else:
+            file = request.files['image']
+            if '' == file.filename:
+                flash("No file selected.", "danger")
+            else:
+                ext = "." + file.filename.split('.')[-1]
+                filename = datetime.now().strftime("%d%m%y%H%M%S") + "_" + "".join(i for i in choices(ascii_letters+digits, k=3)) + ext
+                filename = os.path.join(app.config['UPLOAD_FOLDER'], filename)
+                print(filename)
+                file.save(filename)
+                session['filename'] = filename
+                return redirect(url_for('result'))
+    else:
+        if session.get('solved'):
+            session.pop('solved')
+        if session.get('filename'):
+            try:
+                os.remove(session['filename'])
+                session.pop('filename')
+            except FileNotFoundError:
+                pass
+    return render_template('index.html', request=request)
+
+@app.route('/result', methods=['GET'])
+def result():
+    if not session.get('solved'):
+        filename = session.get('filename')
+        if not filename:
+            return redirect(url_for('/'))
+        solution = ""
+        result_file = ".".join(i for i in filename.split(".")[:-1]) + "_sol.png"
+        result_file = result_file.split("/")[-1]
+        full_result_file = "static/" + result_file
+        result_file = f"../static/{result_file}"
+        result = manage_solution(filename, full_result_file)
+        os.remove(session['filename'])
+        if result == 0:
+            session['filename'] = full_result_file
+            print("solved")
+            solution = result_file
+            session['solved'] = solution
+        else:
+            session.pop('filename')
+            flash(f"There was an issue, Error {result}", "danger")
+            redirect(url_for('/'))
+    else:
+        solution = session['solved']
+    return render_template('result.html', img=solution)
+
+if "__main__" == __name__:
+    app.run(
+        host="192.168.1.88",
+        port=5000,
+        debug=True
+    )
\ No newline at end of file
diff --git a/OTHERS/Encryption/README.md b/OTHERS/Encryption/README.md
new file mode 100644
index 00000000..46b85add
--- /dev/null
+++ b/OTHERS/Encryption/README.md
@@ -0,0 +1,133 @@
+
+# 🔐 Simple Symmetric Encryption in Python
+
+This project demonstrates the basics of **symmetric encryption** using Python and the [`cryptography`](https://cryptography.io/en/latest/) library. It's a great starting point for beginners interested in **cybersecurity**, **cryptography**, or contributing to **open-source security tools**.
+
+---
+
+## 📚 What You'll Learn
+
+- How to generate secure keys
+- How to encrypt and decrypt messages
+- Basic key management (saving & loading keys)
+- How `Fernet` (AES-based encryption) works under the hood
+
+---
+
+## 🚀 Getting Started
+
+### 1. Clone the Repository
+
+```bash
+git clone https://github.com/your-username/simple-encryption-python.git
+cd simple-encryption-python
+```
+
+### 2. Install Dependencies
+
+Make sure you have Python 3.6+ installed.
+
+Install required package using `pip`:
+
+```bash
+pip install cryptography
+```
+
+### 3. Run the Code
+
+```bash
+python simple_encryption.py
+```
+
+On first run, it will generate a `secret.key` file that is used for both encryption and decryption. Each time you run the script, it:
+- Loads the existing key
+- Encrypts a sample message
+- Decrypts it back and displays the result
+
+---
+
+## 📂 File Structure
+
+```
+simple-encryption-python/
+│
+├── simple_encryption.py   # Main script to run encryption and decryption
+├── secret.key             # Auto-generated AES-based symmetric key (DO NOT SHARE)
+├── README.md              # Documentation
+```
+
+---
+
+## 🔒 Security Note
+
+This example is for educational purposes only. If you’re building a production-grade application:
+- Never store raw keys in plaintext
+- Use environment variables or secure vaults (e.g., AWS KMS, HashiCorp Vault)
+- Handle exceptions and errors securely
+
+---
+
+## 🧠 How It Works (In Brief)
+
+- **Fernet** is a module in the `cryptography` package that provides:
+  - AES-128 in CBC mode
+  - HMAC-SHA256 authentication
+  - Random IVs for each encryption
+
+- The encryption key is:
+  - Generated once and saved to `secret.key`
+  - Loaded on subsequent runs
+
+- The message is:
+  - Encrypted using `.encrypt()`
+  - Decrypted using `.decrypt()`
+
+---
+
+## 💡 Sample Output
+
+```
+[*] Key loaded from 'secret.key'
+
+Original Message: This is a secret message.
+Encrypted Message: b'gAAAAABlZ...'
+Decrypted Message: This is a secret message.
+```
+
+---
+
+## 🤝 Contributing
+
+Contributions are welcome! You can help by:
+- Improving the CLI interface
+- Adding file encryption support
+- Implementing password-based key derivation
+- Writing unit tests
+
+To contribute:
+1. Fork the repo
+2. Create a new branch (`git checkout -b my-feature`)
+3. Commit your changes
+4. Push and create a Pull Request
+
+---
+
+## 📜 License
+
+This project is licensed under the **MIT License**. See [LICENSE](LICENSE) for details.
+
+---
+
+## 👨‍💻 Author
+
+**Afolabi Adewale**  
+A data and security enthusiast exploring the intersection of Python, encryption, and open-source software.  
+[GitHub Profile](https://github.com/your-username)
+
+---
+
+## 🔗 Related Resources
+
+- [Python `cryptography` docs](https://cryptography.io/en/latest/)
+- [Understanding Symmetric vs Asymmetric Encryption](https://www.cloudflare.com/learning/ssl/how-does-ssl-work/)
+- [OWASP Crypto Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html)
diff --git a/OTHERS/Encryption/symmetricEncryption.py b/OTHERS/Encryption/symmetricEncryption.py
new file mode 100644
index 00000000..9d132e07
--- /dev/null
+++ b/OTHERS/Encryption/symmetricEncryption.py
@@ -0,0 +1,72 @@
+# simple_encryption.py
+
+"""
+A simple example of symmetric encryption using Python's 'cryptography' package.
+
+This script:
+1. Generates a secure encryption key
+2. Encrypts a message using the key
+3. Decrypts it back to the original message
+
+Author: Afolabi Adewale
+"""
+
+from cryptography.fernet import Fernet
+
+# 1. Generate a key for encryption and decryption
+def generate_key():
+    """
+    Generates a symmetric key for Fernet (uses AES encryption internally).
+    """
+    key = Fernet.generate_key()
+    with open("secret.key", "wb") as key_file:
+        key_file.write(key)
+    print("[+] Key generated and saved to 'secret.key'")
+    return key
+
+# 2. Load the existing key from file
+def load_key():
+    """
+    Loads the previously generated key from the file.
+    """
+    return open("secret.key", "rb").read()
+
+# 3. Encrypt a message
+def encrypt_message(message: str, key: bytes) -> bytes:
+    """
+    Encrypts a message using the provided symmetric key.
+    """
+    f = Fernet(key)
+    encrypted = f.encrypt(message.encode())
+    return encrypted
+
+    
+# 4. Decrypt a message
+def decrypt_message(encrypted_message: bytes, key: bytes) -> str:
+    """
+    Decrypts an encrypted message using the same symmetric key.
+    """
+    f = Fernet(key)
+    decrypted = f.decrypt(encrypted_message)
+    return decrypted.decode()
+
+# 5. Main runner
+if __name__ == "__main__":
+    # Create or load the key
+    try:
+        key = load_key()
+        print("[*] Key loaded from 'secret.key'")
+    except FileNotFoundError:
+        key = generate_key()
+
+    # Example message
+    message = "This is a secret message."
+    print(f"\nOriginal Message: {message}")
+
+    # Encrypt it
+    encrypted = encrypt_message(message, key)
+    print(f"Encrypted Message: {encrypted}")
+
+    # Decrypt it
+    decrypted = decrypt_message(encrypted, key)
+    print(f"Decrypted Message: {decrypted}")
diff --git a/PYTHON APPS/ResolutionSwapper/Readme.md b/PYTHON APPS/ResolutionSwapper/Readme.md
new file mode 100644
index 00000000..ebd6eeb2
--- /dev/null
+++ b/PYTHON APPS/ResolutionSwapper/Readme.md	
@@ -0,0 +1,21 @@
+# Resolution
+This is a small application for switching monitor resolution without having to go into your computer settings. Very useful if you have an older system, and are having trouble running newer/more demanding games.
+
+# Prerequisites
+pywintypes
+win32con
+win32api
+time
+Pyinstaller (optional, but recommended)
+
+# Usage
+This app currently features 720p, 1080p and 1440p resolutions. If you wish to add more, add them in the same format as the other resolutions. The number is a string, so you could choose to set the input to letters if that is your preference.
+
+When run, the app will create a popup terminal for you to enter the resolution you want. It will first check if what you input is valid, then set your monitor's resolution based on the preset dimensions.
+
+# Export to exe 
+ For ease of use, I'd recommend exporting this to an exe using pyinstaller.
+ Instructions for this can be found here -
+ ```https://pyinstaller.org/en/stable/usage.html```
+From there, create a shortcut to have it on your taskbar. To set the image, you can use any image you choose, but it will need to be in a .ico format. You can find converters to make these from other formats online.
+That done, simply click on the icon, then enter your resolution in the terminal popup (only the width). 
diff --git a/PYTHON APPS/ResolutionSwapper/ResolutionMulti.py b/PYTHON APPS/ResolutionSwapper/ResolutionMulti.py
new file mode 100644
index 00000000..ff38e433
--- /dev/null
+++ b/PYTHON APPS/ResolutionSwapper/ResolutionMulti.py	
@@ -0,0 +1,30 @@
+import pywintypes
+import win32con
+import win32api
+import time
+
+
+devmode = pywintypes.DEVMODEType()
+valid = 0
+while valid == 0:
+    heightinp = input('Set resolution: -- ')
+    if heightinp in ['720','1080','1440']:
+        valid += 1
+    else:
+        print('Invalid resolution. Please try again')
+        time.sleep(2)
+
+
+if heightinp == '720':
+    devmode.PelsWidth = 1280
+    devmode.PelsHeight =720
+if heightinp == '1080':
+    devmode.PelsWidth = 1920
+    devmode.PelsHeight =1080
+if heightinp == '1440':
+    devmode.PelsWidth = 2560
+    devmode.PelsHeight = 1440
+
+devmode.Fields = win32con.DM_PELSWIDTH | win32con.DM_PELSHEIGHT
+
+win32api.ChangeDisplaySettings(devmode, 0)
diff --git a/README.md b/README.md
index 8505b537..f664078e 100644
--- a/README.md
+++ b/README.md
@@ -113,15 +113,16 @@ guide [HERE](https://github.com/larymak/Python-project-Scripts/blob/main/CONTRIB
 | 64    | [Umbrella Reminder](https://github.com/larymak/Python-project-Scripts/tree/main/TIME%20SCRIPTS/Umbrella%20Reminder)                                   | [Edula Vinay Kumar Reddy](https://github.com/vinayedula)    |
 | 65    | [Image to PDF](https://github.com/larymak/Python-project-Scripts/tree/main/IMAGES%20%26%20PHOTO%20SCRIPTS/Image%20to%20PDF)                       | [Vedant Chainani](https://github.com/Envoy-VC)              |
 | 66    | [KeyLogger](https://github.com/larymak/Python-project-Scripts/tree/main/OTHERS/KeyLogger)                                                         | [Akhil](https://github.com/akhil-chagarlamudi)              |
-| 67    | [PDF Text Extractor](https://github.com/SamAddy/Python-project-Scripts/tree/main/PYTHON%20APPS/PDF-Text-Extractor)                                                         | [Samuel Addison](https://github.com/SamAddy)
-| 68    | [Analyze docx file](https://github.com/larymak/Python-project-Scripts/tree/main/AUTOMATION/analyzing%20and%20writing%20.docx%20file)                                     | [Kashaan Mahmood](https://github.com/Kashaan-M)
-| 69    | [Bitcoin Price](https://github.com/larymak/Python-project-Scripts/tree/main/WEB%20SCRAPING/Bitcoin%20Price)                                                                | [Olu-Olagbuji Delight](https://github.com/Dheelyte)           
-| 70    | [Password Generator](https://github.com/larymak/Python-project-Scripts/tree/main/GUI/Password%20Generator)                                                                | [LpCodes](https://github.com/LpCodes)     
-| 71    | [HTML to Excel](https://github.com/larymak/Python-project-Scripts/tree/main/CONVERSION%20SCRIPTS/HTML%20to%20Excel)                                                                | [LpCodes](https://github.com/LpCodes)     
+| 67    | [PDF Text Extractor](https://github.com/SamAddy/Python-project-Scripts/tree/main/PYTHON%20APPS/PDF-Text-Extractor)                                                         | [Samuel Addison](https://github.com/SamAddy)|
+| 68    | [Analyze docx file](https://github.com/larymak/Python-project-Scripts/tree/main/AUTOMATION/analyzing%20and%20writing%20.docx%20file)                                     | [Kashaan Mahmood](https://github.com/Kashaan-M)|
+| 69    | [Bitcoin Price](https://github.com/larymak/Python-project-Scripts/tree/main/WEB%20SCRAPING/Bitcoin%20Price)                                                                | [Olu-Olagbuji Delight](https://github.com/Dheelyte)           |
+| 70    | [Password Generator](https://github.com/larymak/Python-project-Scripts/tree/main/GUI/Password%20Generator)                                                                | [LpCodes](https://github.com/LpCodes)     |
+| 71    | [HTML to Excel](https://github.com/larymak/Python-project-Scripts/tree/main/CONVERSION%20SCRIPTS/HTML%20to%20Excel)                                                                | [LpCodes](https://github.com/LpCodes)     |
 | 72    | [Star pattern](https://github.com/larymak/Python-project-Scripts/tree/main/OTHERS/Star%20pattern)                                                                | [LpCodes](https://github.com/LpCodes)     |
 | 73    | [Logging Helper](https://github.com/larymak/Python-project-Scripts/tree/main/OTHERS/add-multiprocessing-logger)                                                                | [Jerry W.](https://github.com/Jerry0420)     |
 | 74    | [Notepad](https://github.com/larymak/Python-project-Scripts/tree/main/PYTHON%20APPS/Notepad)                                                                | [Annarhysa Albert](https://github.com/Annarhysa)     |
 | 75   | [Quadratic Equation Solver](https://github.com/larymak/Python-project-Scripts/tree/main/GUI/Quadratic-Equation-Solver)                                                                | [Akinfenwa Ezekiel](https://github.com/Ezek-iel)     |
 | 76   | [Internet Connectivity Monitor](https://github.com/larymak/Python-project-Scripts/tree/main/AUTOMATION/InternetConnectivityMonitor)                                                                | [Prince Khunt](https://github.com/princekhunt)     |
 | 76   | [E-commerce](https://github.com/larymak/Python-project-Scripts/tree/main/FLASK%20PROJECTS/E-commerce)                                                                | [Eter Nada](https://github.com/tarenjk24)     |
-
+| 77   | [Resolution Swapper](https://github.com/larymak/Python-project-Scripts/tree/main/PYTHON%20APPS/ResolutionSwapper)                                                                | [KnightBlue](https://github.com/KnightBlue14)     |
+| 78 | [Anniversary time](https://github.com/larymak/Python-project-Scripts/tree/main/FLASK%20PROJECTS/Anniversary%20time) | [gyyzzz](https://github.com/gyyzzz) |
diff --git a/gemini chatbot/Gemini-Chatbot b/gemini chatbot/Gemini-Chatbot
new file mode 160000
index 00000000..9d3c59a9
--- /dev/null
+++ b/gemini chatbot/Gemini-Chatbot	
@@ -0,0 +1 @@
+Subproject commit 9d3c59a9e3b949733d4fb3361e3abfc061d70271
diff --git a/gemini chatbot/chatbot.py b/gemini chatbot/chatbot.py
new file mode 100644
index 00000000..1f8fe269
--- /dev/null
+++ b/gemini chatbot/chatbot.py	
@@ -0,0 +1,24 @@
+import google.generativeai as genai
+
+genai.configure(api_key="GOOGLE_GEMINI_API_KEY")
+
+model = genai.GenerativeModel("gemini-1.5-flash")
+
+chat = model.start_chat(history=[])
+
+def chat_with_gemini():
+    print("🤖 Gemini ChatBot (type 'exit' to quit)")
+    while True:
+        user_input = input("You: ")
+        if user_input.lower() in ["exit", "quit"]:
+            print("Bot: Goodbye 👋")
+            break
+
+        try:
+            response = chat.send_message(user_input)
+            print("Bot:", response.text)
+        except Exception as e:
+            print("⚠️ Error:", e)
+
+if __name__ == "__main__":
+    chat_with_gemini()
\ No newline at end of file
diff --git a/gemini chatbot/readme.txt b/gemini chatbot/readme.txt
new file mode 100644
index 00000000..9022c64f
--- /dev/null
+++ b/gemini chatbot/readme.txt	
@@ -0,0 +1,19 @@
+---------------------------------------------- Gemini ChatBot 🤖 ----------------------------------------------
+
+Welcome to the Gemini ChatBot Project — an intelligent, Python-based chatbot powered by Google’s Gemini 1.5 Flash model. This project allows you to have smart, interactive conversations directly in the terminal. It’s ideal for developers, students, or hobbyists exploring Generative AI and conversational interfaces. Whether you're curious about large language models or looking to contribute to open-source AI tools, this chatbot is a perfect place to start.
+
+The Gemini ChatBot maintains conversation history, enabling more context-aware and meaningful interactions. It uses Google’s official Generative AI Python SDK to communicate with the Gemini API. The code is written for clarity and modularity, making it easy to customize, extend, and learn from.
+
+To get started, you’ll need Python 3.8 to 3.10 installed on your machine (Python 3.13 may not be fully compatible yet) and an API key from Google AI Studio, which you can obtain for free. The chatbot is designed to run in a command-line environment and responds to your input in real-time, simulating a natural conversation. You can modify the script to integrate it into other platforms later, such as a web app or a GUI interface.
+
+To ensure clean package management and avoid conflicts with other Python projects, it’s recommended to set up a Conda environment. Start by opening Anaconda Prompt or your terminal, then create a new environment with Python 3.10. Once the environment is created and activated, you can install the required google-generativeai package using pip. After that, you're ready to run the chatbot.
+
+To launch the chatbot, navigate to the project folder in your terminal and run the Python script. The chatbot will start with a greeting and wait for your input. Simply type anything you’d like to ask or discuss. You can chat freely, and the model will respond with AI-generated replies. To end the session at any time, just type “exit” or “quit,” and the chatbot will close the conversation with a goodbye message.
+
+The project folder is organized for simplicity and ease of use. The main script is chatbot.py, where all the chatbot logic is implemented. The requirements.txt file contains a list of dependencies, so others can easily replicate your environment using pip. A .gitignore file is included to exclude virtual environment folders and temporary files from version control.
+
+Looking ahead, there are many exciting directions this project could take. You can add a web interface using Streamlit or Flask, implement support for file or image inputs using Gemini Pro Vision, enable chat logging to save previous conversations, or even integrate secure key storage using environment variables. These enhancements are perfect opportunities for open-source contributions.
+
+If you’d like to contribute, you’re welcome to fork the repository, create your own feature branch, and submit a pull request. Feedback, improvements, and new ideas are encouraged — collaboration is at the heart of open-source.
+
+Thank you for checking out the Gemini ChatBot! Whether you’re here to learn, experiment, or build, we hope this project helps you get one step closer to the future of AI-powered applications. Let’s build something incredible together. 🚀
diff --git a/gemini chatbot/requirements.txt b/gemini chatbot/requirements.txt
new file mode 100644
index 00000000..ac4a68a3
--- /dev/null
+++ b/gemini chatbot/requirements.txt	
@@ -0,0 +1 @@
+google-generativeai>=0.3.2