Game Tree

We will go over how Backtracking can assist us in solving Game Tree. We will solve the Game Tree Problem by making use of the Minimax Algorithm. The Time and Space Complexity will be discussed at the end of the article.

Table of contents:

  1. Introduction
  2. Solving Game Tree
  3. Time and Space Complexity
  4. Game tree of other games
  5. Conclusion

Introduction

Searching through Game Tree is the core of board-game artificial intelligence. Let us consider how humans play a strategy game. A possible move for our next turn is the first thing we consider. We need to think about how our opponent will respond, then figure out how we would respond to that. We try to read out the variation and see if it’s a good result. We backtrack and look at a different variation to see if it was better. This is a description of how the tree-search algorithms are used in the game AI. Humans can only keep a few changes in their heads at a time, while computers can juggle millions. Humans use their intuition to make up for their lack of computing power. Chess and Go players who are experienced can spot a few moves ahead that are worth looking into.

The course of the game is determined by the players decisions in a deterministic game. Rolling dice or shuffling cards are examples of randomness involved in a non-deterministic game.

In perfect information games, both players can see the entire game state at all times. Everyone’s cards are visible on the table or board. In hidden information games, each player can only see a small portion of the game state. Players in card games are dealt a few cards and cannot see what the other players are holding. Players enjoy hidden information games because they must guess what the other players know based on their decisions.

Solving Game Tree

To program a computer to play a game, we have to consider how humans would make a similar decision. The strategy call minimaxing can be applied. You are trying to maximize your score while your opponent is trying to reduce it. Let’s see how it works.

Steps
  • The game is played by two players, Max and Min in the Minimax algorithm.
  • A deep-first search is performed for the exploration of the entire game tree.
  • At the terminal node, if it is Max Player turn, the player will chose the action that lead to the maximum benefit.
  • The algorithm will then backtrack and it is Min Player turn, the player will chose the action that lead to the minimum benefit.

Let’s take a closer look at the algorithm.

Pseudocode
minimax(position, maximizingPlayer)
    if depth == 0
        return static_evaluation_of_position
    else
        for each child of position
            depth = get_depth(child)
            key, value = minimax(child)
            leaf_data_dict[key] = value
        return compare_node(leaf_data_dict, depth)
    
compare_node(leaf_data_dict, depth)
    if depth is even
        mini_max_eval = min(leaf_data_dict)
    else
        min_max_eval = max(leaf_data_dict)
    return mini_max_eval
Implementation

game-tree-01 To illustrate how this algorithm work in actual game, let say there is only 3 cards involved, KQJ, whereby the ranking is K > Q and Q > J. At the beginning ot the game, each player put in an ante bet of 1 unit. Player 1 (Maximizing) draws the card J. For the purpose of this example, let assume Player 1 happened to see that Player 2 draw the card Q. At showdown, Player 1 would lose to Player 2. However, Player 1 could bluff his way by betting in hope that Player 2 will fold. However he will risk 2 units instead of 1 units. Minimax algorithm could help Player 1 find the optimal path to take.

We import the treelib module to allow us to use the tree data structure. Next the class TheNode stores additional information about the node in the game tree, such as the label, unique identifier ID, action for player to take and the static evaluation (value).

The add_node() function specify the node label, identifier, additional data stored in the object gt_node and the parent id of the node(if any). The create_tree() function call the add_node() function to generate the game tree.

import treelib as tl

class TheNode(object):
    def __init__(self, label, id, action, eval):
        self.label = label
        self.id = id   
        self.action = action
        self.eval = eval

def add_node(label, id, parent=None, action=None, eval=0):
    gt_node = TheNode(label, id, action, eval)
    tree.create_node(tag=label, identifier=id, data=gt_node, parent=parent)

def create_tree():    
    add_node("P1A", "p1a", None, "Gametree Decision")
        
    add_node("P2A", "p2a", "p1a", "CHECK")
    add_node("E1", "e1", "p2a", "CHECK", -1)
    add_node("P1B", "p1b", "p2a", "BET")
    add_node("E2", "e2", "p1b", "FOLD", -1)
    add_node("E3", "e3", "p1b", "CALL", -2)

    add_node("P2B", "p2b", "p1a", "BET")
    add_node("E4", "e4", "p2b", "FOLD",1)
    add_node("E5", "e5", "p2b", "CALL",-2)

The minimax() function is a recursive function that takes in the node_id value, which is the position to start evaluating the game tree from. First we declare the base case scenario, if the node is the leaf node, the function will return the static evaluation of the node (payout value).

Otherwise, the function will performed a depth first search of the game tree. It will loop through every child nodes and perform a minimax() function on the child nodes. The payout value and the node label will be stored in the dictionary leaf_data. Next it will called the compare_node() to perform an comparison of the payout value.

def minimax(node_id):    
    # base case, if no sub nodes, just return the value
    game_node = tree.get_node(node_id)
        
    if game_node.is_leaf():
        return [], game_node.data.eval
    else:
        leaf_data = {}
        key = []
        child_list = tree.children(node_id)        
        for child in child_list:          
            depth = tree.depth(child)
            key, value = minimax(child.identifier)
            #add eval data to dictionary
            if child.is_leaf():
                leaf_data[child.data.label] = child.data.eval
            else:
                # key, value = eval
                leaf_data[key[0]] = value
        return compare_node(leaf_data, key, depth)

In the compare_node() function, if the depth where the node is located at is even, it will return the Min value and label of the leaf_data dictionary. If the depth is odd, it will return the Max value and label of the leaf_data dictionary.

def compare_node(leaf_data, key, depth):        
        if (depth % 2) == 0:
            key.insert(0, min(leaf_data, key=leaf_data.get))
            mini_max_eval=leaf_data[min(leaf_data, key=leaf_data.get)]
            print(f"[Minimize] lowest of {leaf_data}")
            print(f"Choice selected will be {key[0]} : {mini_max_eval}")
        else:
            key.insert(0, max(leaf_data, key=leaf_data.get))
            mini_max_eval=leaf_data[max(leaf_data, key=leaf_data.get)] 
            print(f"[Maximize] highest of {leaf_data}")
            print(f"Choice selected will be {key[0]} : {mini_max_eval}")

        print("=============================================")     
        return key, mini_max_eval

The display_result function will print out the decision path taken to reach the optimal result.

def display_result(choice):
    solution = []
    for node in tree.rsearch(choice):
        solution.insert(0, tree[node].data.action + " - " + tree[node].tag)
    print (f"\n{solution}")

Below is the driver code needed to create the game tree and run the evaluation to determine the optimal decision for Player 1 to take.

if __name__ == '__main__':
    tree = tl.Tree()
    create_tree()
    # set key to True so that Tree is printed in the order it is inserted and not sorted by alphabetical order
    tree.show(key = lambda x : True, line_type="ascii-em")
    node_id = "p1a"
    eval = minimax(node_id)
    choice = eval[0][0].lower()
    display_result(choice)

Once we run the code, we should see the following output. From the result, we can see that Player 1 should avoid bluffing and risk losing 2 units. The optimal decision taken will result in the loss of 1 unit.

P1A
╠══ P2A
║   ╠══ E1
║   ╚══ P1B
║       ╠══ E2
║       ╚══ E3
╚══ P2B
    ╠══ E4
    ╚══ E5

[Maximize] highest of {'E2': -1, 'E3': -2}
Choice selected will be E2 : -1
=============================================
[Minimize] lowest of {'E1': -1, 'E2': -1}
Choice selected will be E1 : -1
=============================================
[Minimize] lowest of {'E4': 1, 'E5': -2}
Choice selected will be E5 : -2
=============================================
[Maximize] highest of {'E1': -1, 'E5': -2}
Choice selected will be E1 : -1
=============================================

['Gametree Decision - P1A', 'CHECK - P2A', 'CHECK - E1']
Here is the entire code block for reference
import treelib as tl

class TheNode(object):
    def __init__(self, label, id, action, eval):
        self.label = label
        self.id = id   
        self.action = action
        self.eval = eval

def add_node(label, id, parent=None, action=None, eval=0):
    gt_node = TheNode(label, id, action, eval)
    tree.create_node(tag=label, identifier=id, data=gt_node, parent=parent)

def create_tree():    
    add_node("P1A", "p1a", None, "Gametree Decision")
        
    add_node("P2A", "p2a", "p1a", "CHECK")
    add_node("E1", "e1", "p2a", "CHECK", -1)
    add_node("P1B", "p1b", "p2a", "BET")
    add_node("E2", "e2", "p1b", "FOLD", -1)
    add_node("E3", "e3", "p1b", "CALL", -2)

    add_node("P2B", "p2b", "p1a", "BET")
    add_node("E4", "e4", "p2b", "FOLD",1)
    add_node("E5", "e5", "p2b", "CALL",-2)

def minimax(node_id):    
    # base case, if no sub nodes, just return the value
    game_node = tree.get_node(node_id)
        
    if game_node.is_leaf():
        return [], game_node.data.eval
    else:
        leaf_data = {}
        key = []
        child_list = tree.children(node_id)        
        for child in child_list:          
            depth = tree.depth(child)
            key, value = minimax(child.identifier)
            #add eval data to dictionary
            if child.is_leaf():
                leaf_data[child.data.label] = child.data.eval
            else:
                # key, value = eval
                leaf_data[key[0]] = value
        return compare_node(leaf_data, key, depth)

def compare_node(leaf_data, key, depth):        
        if (depth % 2) == 0:
            key.insert(0, min(leaf_data, key=leaf_data.get))
            mini_max_eval=leaf_data[min(leaf_data, key=leaf_data.get)]
            print(f"[Minimize] lowest of {leaf_data}")
            print(f"Choice selected will be {key[0]} : {mini_max_eval}")
        else:
            key.insert(0, max(leaf_data, key=leaf_data.get))
            mini_max_eval=leaf_data[max(leaf_data, key=leaf_data.get)] 
            print(f"[Maximize] highest of {leaf_data}")
            print(f"Choice selected will be {key[0]} : {mini_max_eval}")

        print("=============================================")     
        return key, mini_max_eval

def display_result(choice):
    solution = []
    for node in tree.rsearch(choice):
        solution.insert(0, tree[node].data.action + " - " + tree[node].tag)
    print (f"\n{solution}")
        
if __name__ == '__main__':
    tree = tl.Tree()
    create_tree()
    # set key to True so that Tree is printed in the order it is inserted and not sorted by alphabetical order
    tree.show(key = lambda x : True, line_type="ascii-em")
    node_id = "p1a"
    eval = minimax(node_id)
    choice = eval[0][0].lower()
    display_result(choice)

Time and Space Complexity

Time Complexity: O(b^d), where b refer to the branching factor. In this example, our branching factor is 2, at each node there are 2 different decision to choose from. d refer to the depth of the game tree. In this example, the depth of the game tree is 3.

Space Complexity: O(b*d)

Game tree of other games

Tic-Tac-Toe
  • The goal of the game is to get three in a row on a 3 x 3 board.
  • The first and second players are known as X and O.
  • The players take turn to place Xs and Os on the game board until the one player has three in a row or all nine squares are filled.
  • In the event that no one has three in a row, the game reach stalemate and the result is a draw.
Tic-Tac-Toe Game Tree
game-tree-02
Connect Four
  • Just like tic tac toe, you need to connect four colored checker pieces in a row to win Connect Four.
  • This can be done in three different ways, horizontally, vertically or diagonally.
  • Each player will drop one piece of checker at a time.
  • You can either build your row or stop your opponent from getting four in a row.
Connect Four Game Tree
game-tree-03
Connect Four Game Tree
game-tree-03A

Conclusion

Minimax algorithm can be used to solve game tree of deterministic, perfect information games such as Chess and Checkers.

Consider using the counterfactual regret minimization algorithm in games like rock-paper-scissors and Poker which are non-deterministic and hidden information in nature.

This article was originally published at OpenGenus:

link to my article at OpenGenus

Backjumping

We are going to cover the concept of Backjumping which is an improvement to the Backtracking Algorithm. We will solve the N-Queens Problem by making use of the Backjumping Algorithm. The Time and Space Complexity will be discussed at the end of the article.

Table of contents:

  1. Introduction
  2. Backjumping
  3. Time and Space Complexity
  4. Conclusion

Introduction

The goal of the “N-Queens Problem” is to arrange N queens on a N × N chessboard so that none of them can attack one another by being in the same row, column, or diagonal. The problem has a simple solution when N = 1 and there is none when N = 2 and N = 3. The answer to the N-Queens problem, when N (board size) = 6, is as follows.

N-Queens Problem Solution
nqueen-problem nqueen-solution

The Gaschnig’s Backjumping algorithm will be used to solve the “N-Queens Problem”. It attempts to minimize the number of nodes visited within the search tree and consequently reduce the number of consistency checks performed by the search process.

Backjumping

Gaschnig’s Backjumping algorithm only backjumps in leaf dead ends. In other words, when one reach a dead end in the search tree, it do not go back to the parent node all the time (Backtracking method), but rather to an earlier ancestor node in the search tree. It performs only safe and maximal jumps, jumping back as much as possible without precluding solution (i.e miss out on any possible solutions.)

Steps
  • We begin solving from the leftmost column of the board.
  • A valid solution is obtained if all the queens are placed.
  • Place the queens one after the other in columns.
  • We check for all rows in the current column for constraints before placing.
  • If no other Queens are in the same row or diagonal and thus satisfy the constraints, we will place that Queen at that location and record the row and column as part of the solution matrix.
  • If such a column does not exist, the algorithm will return false and performs a backjump.

Let’s take a closer look at the algorithm.

Pseudocode
backjump_solver(starting_row)
    if(a queen in placed in every columns)
        return true
    for(all rows in the current column)
        if (safe_to_place(column, row) is true)
            return true
    backjump max(conflict_set)
Implementation

nqueen-conflict

To illustrate the conflict set concept, the Queen at row 5 is in conflict with Queen at (row 0), (rows 2 and 4), (rows 3 and 4), (rows 1 and 4), (rows 2 and 3) and (row 0).

In this instance, a dead end is reached in the search tree. To perform a safe and maximal jump, only the earliest row that is in conflict will be selected. The resulting conflict set will be rows(0, 2, 3, 1, 2, 0). To determine the maximal jump, we will select the latest row that is in conflict. The Backjumping algorithm will jump to the Queen at row 3.

Implementation of the above backtracking algorithm :

The numpy, string and tabulate libraries are imported to enable us to “pretty print” our results in a table format. The use of dictionary comprehension below is to construct the conflict_set variable which tracks the “row in conflict” during the placing of the Queens across the columns.

import numpy as np
import string
from tabulate import tabulate

class NQueenSolver:

    def __init__(self, size_of_board):
        self.size = size_of_board       
        self.columns = []
        self.num_of_places = 0
        self.num_of_backjumps = 0
        self.conflict_set = {new_list: [] for new_list in range(self.size)}

The place_queen function take in a variable starting_row and we will set the initial value as 0. If the length of the variable columns is equal to the variable size, this means that every column has a queen and we have found a solution. Our program will proceed to print out the solution.

    def place_queen(self, starting_row=0):
        if len(self.columns) == self.size:
            print(f"Board size: {self.size} x {self.size}")
            print(f"{self.num_of_places} attempts were made to place the Queens which include {self.num_of_backjumps} backjumps.")
            return self.columns

If no solution is found yet, we will proceed to search for a safe location to place the Queen. A for loop is used to check each row in the current column for a safe location to place the Queen. Details of the is_safe function will be explained later in this article. If a safe location is found, the row location will be recorded in the columns variable and the place_queen function will be called to proceed to work on the next column.

        else:
            for row in range(starting_row, self.size):
                if self.is_safe(len(self.columns), row) is True:
                    self.columns.insert(len(self.columns),row)
                    self.num_of_places += 1
                    return self.place_queen()

If we are unable to find any safe location for the current column, we will retrieve the conflict_set for the particular column. The greatest value of the conflict_set will be the maximal column to backjump to. The column that we backjumped to and the subsequent columns will be emptied by setting their value to []. The place_queen function will be called to proceed to work on the backjumped column.

            max_jump = self.conflict_set[len(self.columns)]
            max_jump = set(max_jump)
            last_row = max(max_jump)
            self.num_of_backjumps += 1
            for i in range(last_row+1, self.size):
                self.conflict_set[i] = []
            previous_variable = self.columns[last_row]
            self.columns = self.columns[:last_row]
            return self.place_queen(starting_row = previous_variable+1)

The is_safe function determine whether a position to place the Queen is safe. The first portion if row == conflict_row check for horizontal conflicts. The second portion elif abs(conflict_row - row) == abs(conflict_col - col): check for diagonal conflicts (they are on the same diagonal if their delta are the same).

In both conflict check, the column that is in conflict will be recorded in the conflict_set variable.

    def is_safe(self, col, row):
        for conflict_col, conflict_row in enumerate(self.columns):
            if row == conflict_row:                
                self.conflict_set[col].append(conflict_col)         
                return False
            elif abs(conflict_row - row) == abs(conflict_col - col):
                self.conflict_set[col].append(conflict_col)            
                return False
        return True

The below codes will attempt to run our N-Queen solver program.

size = input("Enter the size of the board:")
n = int(size)
queens = NQueenSolver(n)
queens.place_queen(0)

We convert the result to a numpy array for “pretty printing”. We used string.ascii_uppercase to create our header row labels and tabulate function display the numpy array in a grid format.

board = np.full((n, n), " ")
for col_queen, row_queen in enumerate(queens.columns):
    board[row_queen][col_queen] = 'Q'   
headers = list(string.ascii_uppercase)
headers = headers[:n]
table = tabulate(board, headers, showindex=range(0,n), tablefmt="fancy_grid")
print(table)

Once we run the code, we should see the following output.

Board size: 6 x 6
31 attempts were made to place the Queens which include 20 backjumps.
╒════╤═════╤═════╤═════╤═════╤═════╤═════╕
│    │ A   │ B   │ C   │ D   │ E   │ F   │
╞════╪═════╪═════╪═════╪═════╪═════╪═════╡
│  0 │     │     │     │ Q   │     │     │
├────┼─────┼─────┼─────┼─────┼─────┼─────┤
│  1 │ Q   │     │     │     │     │     │
├────┼─────┼─────┼─────┼─────┼─────┼─────┤
│  2 │     │     │     │     │ Q   │     │
├────┼─────┼─────┼─────┼─────┼─────┼─────┤
│  3 │     │ Q   │     │     │     │     │
├────┼─────┼─────┼─────┼─────┼─────┼─────┤
│  4 │     │     │     │     │     │ Q   │
├────┼─────┼─────┼─────┼─────┼─────┼─────┤
│  5 │     │     │ Q   │     │     │     │
╘════╧═════╧═════╧═════╧═════╧═════╧═════╛
Here is the entire code block for reference
import numpy as np
import string
from tabulate import tabulate

class NQueenSolver:

    def __init__(self, size_of_board):
        self.size = size_of_board       
        self.columns = []
        self.num_of_places = 0
        self.num_of_backjumps = 0
        self.conflict_set = {new_list: [] for new_list in range(self.size)}

    def place_queen(self, starting_row=0):
        if len(self.columns) == self.size:
            print(f"Board size: {self.size} x {self.size}")
            print(f"{self.num_of_places} attempts were made to place the Queens which include {self.num_of_backjumps} backjumps.")
            return self.columns
        else:
            for row in range(starting_row, self.size):
                if self.is_safe(len(self.columns), row) is True:
                    self.columns.insert(len(self.columns),row)
                    self.num_of_places += 1
                    return self.place_queen()

            max_jump = self.conflict_set[len(self.columns)]
            max_jump = set(max_jump)
            last_row = max(max_jump)
            self.num_of_backjumps += 1
            for i in range(last_row+1, self.size):
                self.conflict_set[i] = []
            previous_variable = self.columns[last_row]
            self.columns = self.columns[:last_row]
            return self.place_queen(starting_row = previous_variable+1)

    def is_safe(self, col, row):
        for conflict_col, conflict_row in enumerate(self.columns):
            if row == conflict_row:                
                self.conflict_set[col].append(conflict_col)         
                return False
            elif abs(conflict_row - row) == abs(conflict_col - col):
                self.conflict_set[col].append(conflict_col)            
                return False
        return True

size = input("Enter the size of the board:")
n = int(size)
queens = NQueenSolver(n)
queens.place_queen(0)
board = np.full((n, n), " ")
for col_queen, row_queen in enumerate(queens.columns):
    board[row_queen][col_queen] = 'Q'   
headers = list(string.ascii_uppercase)
headers = headers[:n]
table = tabulate(board, headers, showindex=range(0,n), tablefmt="fancy_grid")
print(table)

Time and Space Complexity

Time Complexity: O(N!)

Space: O(N * (N-D)) where D refer to the depth of the backjump.

Conclusion

Backjumping algorithm is an improvement to the backtracking method. It improve efficiency as it reduces the space used by the recursion stack to reach a solution.

This article was originally published at OpenGenus:

link to my article at OpenGenus

Solving Crossword using Backtracking

We are going to cover the Backtracking Algorithm for Crossword and compared it with the Brute Force approach. The Time and Space Complexity will be discussed at the end of the article.

Table of contents:

  1. Introduction
  2. Naive Approach
  3. Backtracking
  4. Time and Space Complexity
  5. Conclusion

Introduction

A crossword is a word puzzle that usually consists of a square or rectangular grid of white and black-shaded squares. By solving clues that lead to the solutions, the goal is to fill the white squares with letters, producing words or phrases. The solution words and phrases are inserted in the grid from left to right (“across”) and from top to bottom in languages that are written left to right (“down”). The words or phrases are separated by the shaded squares.

Crossword Puzzle Solution
crossword-puzzle crossword-solution

The goal is to fill all of the empty cells in a 6x6 grid with words such that the grid becomes valid. The constraint here is that the character at the intersection point have to be the same.

Naive Approach

The process in the naive approach fills in the empty cells with no logic and then examines whether it was a valid placement. This can take a long time and be quite inefficient. The Backtracking Algorithm would be an improvement to this naive approach.

Backtracking

Backtracking is a form of brute-force approach that comes into play when addressing a problem that involves evaluating several options. Since we do not know which one is accurate and we try to solve the problem using the trial and error method, one decision at a time, until we get the desired answer.

The method described above can be used to solve the crossword puzzle. This backtracking algorithm traverses all the vacant cells, incrementally fills the cells with possible words retrieved from a dictionary file, then backtracks when a word filled does not comply with the constraint. This process is repeated until all of the cells are filled.

Steps
  • We begin by looking for a continuous row/column of empty cells.
  • A valid crossword is obtained when all of the cells are filled.
  • We try to fill the continuous row/column of empty cells with values retrieved from the dictionary file.
  • We examine the intersection point between the horizontal and vertical words for constraint before placing.
  • If we can satisfy the constraints, we will place that word at that location, start-coordinates(a,b), end-coordinates(x,y) and then restart the procedure by looking for the next continuous row/column of empty cells.
  • If none of the words can be placed, we’ll have to backtrack and alter the values for previously visited cells.

Let’s take a closer look at the algorithm.

Pseudocode
backtrack_solver(crossword)
    if(no more cells are there to explore)
        return true
    for(all available possibilities)
        try one possibility p
        if(backtrack_solver(crossword with possibility p made) is true)
            return true
        unmake possibility p
    return false
Implementation

Implementation of the above backtracking algorithm :

The copy library is imported to enable the usage of the deepcopy() function. In the case of deep copy, an object is copied in another object. It means that any changes made to a copy of an object are not reflected in the original. The shapely library is imported to allow us to find the intersection point(constraint) in the crossword.

The class is used to represents each word in the crossword puzzle. When we apply backtracking later, we use the value variable to assign values to each word.

import copy
from shapely.geometry import LineString

class Word:
	#coordinates of the starting and ending point
	start_coord = ()
	end_coord = ()	
	#horizontal word = 0, vertical word = 1
	orientation = 0	
	#word length
	length = 0
	#value assigned to this word
	value = ''

The load_crossword_puzzle and load_dictionary function accept an input (filename) and will proceed to read the content of the file. In order to have useful data to work with, it perform a “clean up” by removing the tabs, newlines and spaces and return the result as a list.

def load_crossword_puzzle(filename):
	crossword = []
	with open(filename, 'r') as cfile:
		puzzle = cfile.readlines()
	for line in puzzle:
		replaced = line.replace("\t", "")
		replaced = replaced.replace("\n", "")
		replaced = replaced.replace(" ", "")
		crossword.append(list(replaced))
	return crossword

def load_dictionary(filename):
	dictionary = []
	with open(filename, 'r') as dfile:
		wordslist = dfile.readlines()
	for word in wordslist:
		replaced = word.replace("\n", "")
		dictionary.append(replaced)
	return dictionary

The function find_horizontal_words(crossword) verify whether the orientation of the words in the crossword puzzle are horizontal. It achieve this by iterating each row of the puzzle and check if the character is 0. If this is the first instance of 0 character is found, that is the previous character is #, the function will save the starting coordinates (row, column) and track the length of the word. The function will continue to iterate to the next column and check for the 0 character. If the next character is #, the function will save the previous column position as the ending coordinates of the word.

The function find_vertical_words(crossword) verify whether the orientation of the words in the crossword puzzle are vertical. The function is similar to the find_horizontal_words(crossword), except that it iterate through each column of the puzzle to locate the words.

def find_horizontal_words(crossword):
	horizontal_words = []
	for row in range(len(crossword)):		
		column = 0
		word = Word()
		finished = False
		prev = '#' #prev mean the previous char in the word

		while column <= len(crossword[row])-1:			
			if crossword[row][column] == '0':				
				if prev == '0':
					word.length += 1
					prev = '0'
					if column == len(crossword[row])-1:
						if not finished:
							finished = True
						word.end_coord = (row, column)
						prev = '#'

				elif prev == "#":
					if finished:
						finished = False
					word.start_coord = (row, column)
					word.length += 1
					prev = '0'

			elif crossword[row][column] == '#':
				if prev == '0':
					if not finished:
						finished = True
					if word.length > 1:
						word.end_coord = (row, column-1)
					else:
						word = Word()
					prev = '#'

			if word.length > 1 and finished:
				word.orientation = 0
				horizontal_words.append(word)
				word = Word()
				finished = False	

			column += 1		
	return horizontal_words

def find_vertical_words(crossword):
	vertical_words = []
	word = Word()
	started = False
	
	for column in range(0, len(crossword[0])):
		started = False
		for row in range(0, len(crossword)-1):
			if crossword[row][column] == '0' and crossword[row+1][column] == '0':
				if started == False:
					started = True
					word.start_coord = (row, column)
				
				if row == len(crossword)-2 and started:
					word.end_coord = (row+1, column)
					word.length = word.end_coord[0] - word.start_coord[0] + 1
					word.orientation = 1
					vertical_words.append(word)
					word = Word()
					started = False
			else:
				if started:
					word.end_coord = (row, column)
					word.length = word.end_coord[0] - word.start_coord[0] + 1
					word.orientation = 1
					vertical_words.append(word)
					word = Word()
					started = False
	return vertical_words

Next we defined the backtracking algorithm function. The function take in three variable, assigned_variable_list, not_assigned_variable_list and dict. The not_assigned_variable_list consist of all the horizontal and vertical words pending to be filled in the crossword. The dict variable is the value returned by the load_dictionary() function. The assigned_variable_list hold all the values that satisfy the constraint in the crossword.

Next, the get_possible_values() is called to return all possible words(values) that fit the word length of the crossword. The check_constraint() function is called to ensure the value assigned satisfy the constraint of the crossword.

If all possible values are unable to satisfy the constraint, this meant that the previous “word” assigned was wrong. The algorithm will then backtrack and leave the word cells unassigned to try other possibilities.

def backtracking(assigned_variable_list, not_assigned_variable_list, dict):

	#there are no variables to assign a value so we are done
	if len(not_assigned_variable_list) == 0:
		return assigned_variable_list

	var = not_assigned_variable_list[0]	
	possible_val = get_possible_values(var, assigned_variable_list, dict)

	for val in possible_val:
		# we create the variable check_var to do the checking and avoid assigning values which do not comply with the constraint
		check_var = copy.deepcopy(var)
		check_var.value = val
		if check_constraint(check_var, assigned_variable_list):
			var.value = val
			result = backtracking(assigned_variable_list+[var], not_assigned_variable_list[1:], dict)
			if result != None:
				return result
            # we've reached here because the choice we made by putting some 'word' here was wrong 
            # hence now leave the word cell unassigned to try another possibilities 
			var.value = ''
	return None

The get_possible_values function is to go through the dictionary and return all possible value that fit the word length of the crossword that we are solving. The function also check values already assigned to the crossword to avoid duplication

#returns all possible values for the desired variable
def get_possible_values(var, assigned_variable_list, dict):
	possibles_values = []
	for val in dict:
		if len(val) == var.length:
			possibles_values.append(val)
	
	for item in assigned_variable_list:
		if item.value in possibles_values:
			possibles_values.remove(item.value)
	return possibles_values

The function check_constraint() check the proposed value to be assigned (var) against the assigned_variable_list. If they are the same orientation (both are horizontal or both are vertical), no further check is needed. If otherwise (one is horizontal and the other is vertical), the check_intersections() will be called to help check for the intersection point (if any).

The check_intersections() make use of the LineString function from the shapely module which we imported earlier. We treat the words as if they were lines and use this idea to find the intersection point of the horizontal and vertical words. This intersection point is the constraint which the algorithm must apply to find a valid solution.

#checks var against assigned variable list
def check_constraint(var, assigned_variable_list):
	if assigned_variable_list != None:
		for word in assigned_variable_list:
			#if orientation is equal they will never intersect
			if var.orientation != word.orientation:
				intersection = check_intersections(var, word)
				if len(intersection) != 0:
					if var.orientation == 0: #horizontal 
						if var.value[int(intersection[0][1]-var.start_coord[1])] != word.value[int(intersection[0][0]-word.start_coord[0])]:          
							return False
					else: #vertical 
						if var.value[int(intersection[0][0]-var.start_coord[0])] != word.value[int(intersection[0][1]-word.start_coord[1])]:          
							return False
	return True

# determine the constraint which the algorithm must apply to get a valid solution
def check_intersections(w1, w2):
	line1 = LineString([w1.start_coord, w1.end_coord])
	line2 = LineString([w2.start_coord, w2.end_coord])
	intersection_point = line1.intersection(line2)
	if not intersection_point.is_empty:
		return [intersection_point.coords[0]] #result(float)
	else:
		return []

The function insert_word_to_puzzle() will now insert the word found by our solver into the crossword base on their orientation, starting coordinates and ending coordinates.

def insert_word_to_puzzle(crossword, word, coord, orientation):
	pos_count = 0
	for char in word:
		if orientation == 0: #horizontal if orientation == 0
			crossword[coord[0]][coord[1]+pos_count] = char
		else:
			crossword[coord[0]+pos_count][coord[1]] = char
		pos_count += 1
	return crossword

The below codes will attempt to run our crossword solver. puzzle.txt is our crossword layout we are trying to solve. An example of the layout is as follow, # represent the dark shaded area of the crossword and 0 represent the word cell to be filled.

#	0	#	#	#	#
#	0	#	#	#	#
#	0	#	#	#	0
#	0	0	0	0	0
#	0	#	#	#	0
#	0	#	#	#	0

The file words.txt is a list of word separated by a new line for our solver to use as possible values to fill in the word cells. An example is as follow

ability
able
about
above
accept
according
cw_puzzle = load_crossword_puzzle("puzzle.txt")
dict = load_dictionary("words.txt")
horizontal_word = find_horizontal_words(cw_puzzle)
vertical_word = find_vertical_words(cw_puzzle)
total_words = horizontal_word + vertical_word
assign_var_list = []
suggested_solution = backtracking(assign_var_list, total_words, dict)

print("---------- Crossword ---------")
for line in cw_puzzle:
	print(line)
print("------------------------------")

print("---------- Solution ----------")

if suggested_solution is None:
	print("No solution found")
else: 
	for word in suggested_solution:
		insert_word_to_puzzle(cw_puzzle, word.value, word.start_coord, word.orientation)

	for line in cw_puzzle:
		print(line)

print("------------------------------")

Once we run the code, we should see the following output:

---------- Crossword ---------
['#', '0', '#', '#', '#', '#']
['#', '0', '#', '#', '#', '#']
['#', '0', '#', '#', '#', '0']
['#', '0', '0', '0', '0', '0']
['#', '0', '#', '#', '#', '0']
['#', '0', '#', '#', '#', '0']
------------------------------
---------- Solution ----------
['#', 'a', '#', '#', '#', '#']
['#', 'l', '#', '#', '#', '#']
['#', 'w', '#', '#', '#', 'i']
['#', 'a', 'b', 'o', 'u', 't']
['#', 'y', '#', '#', '#', 'e']
['#', 's', '#', '#', '#', 'm']
------------------------------

Time and Space Complexity

Time Complexity

O((M * P)^D): Each continuous cells will have only one word so D+1 words will be used. At each intersection, we need to check P words. M is the average length of word and this is needed as we need to check constraints (reading time of a string).

Space Complexity

O(L) : where L is the length of the given word. This space is used for recursion stack.

Conclusion

Other ways to solve crossword include:

  • Forward Checking
  • Dynamic Variable Ordering
  • Conflict-Directed Backjumping
  • Arc Consistency

This article was originally published at OpenGenus:

link to my article at OpenGenus

System Design of Restaurant Management System

We will take a look at the key features a restaurant management system needs to offer, its high-level, low-level design, database design, and some of the features that turnkey software solutions offer.

Table of contents:

  1. Restaurant Management System
  2. System Requirements
    • Functional Requirements
    • Non - Functional Requirements
    • Software Requirements
  3. System Design
    • Architecture of the System
    • Subsystem Decomposition
    • Low-Level Design
    • Database Design
  4. Turnkey Software Solutions

Restaurant Management System

A restaurant management system is a software that helps the restaurant industry streamline their food business operations. It provides a complete set of features including Point of Sales system, payment processing, table reservations, inventory management , accounting and employee management.

System Requirements

The system we will be designing will provide the most important features that every restaurant management system must offer. However, do note that a restaurant management system can have other sub-systems for accounting, employee management, etc.

Functional Requirements

The restaurant management system should at least include the following features,

  1. Allow to add and remove item to order
  2. Allow to add and remove table reservation
  3. Allow to add and remove menu items
  4. Generate bills
  5. Manage payment of bills
  6. Generate kitchen order tickets

A better understanding of the functional requirements can be gained from the use-case diagram below.

UML-fee-mgmt

Non - Functional Requirements

Usability
The system should provide an interactive user-friendly interface that is easily understandable for all users.

Availability
The System should be available at least during the restaurant operating hours and must be recovered within an hour if it fails. The system should respond to the requests within two seconds.

Dependability
The system should provide consistent performance with easy tracking of records and updating of records.

Maintainability
The software should be easily maintainable. Adding additional features and making changes to the software must be as simple as possible.

Security
Only authorized users can access the system to view and change the data.

Software Requirements
  1. A server running Windows Server/Linux operating system.
  2. A backend language such as like Java, Python to process the orders.
  3. Front-end frameworks like Angular/React/Vue for the user interface.
  4. Relational database management system such as MySQL, PostgreSQL.

System Design

With our understanding of the functional and non-functional requirements of our system, we will now look at the system architecture, subsystems decomposition and database design.

Architecture of the System

The restaurant management system follows a simple 3 tier client/server architecture. The client can use web browsers on a tablet to access the system (restaurant menu) through the local area network of the restaurant using Hypertext Transfer Protocol Secure (HTTPS).

System-Architecture

The middle tier includes the server which presents the website to the user and controls the business logic. It controls the interactions between the application and the user. The server also send the user orders to both the Point of Sales system and Kitchen Order Tickets printers. Common web server technology used here can be Apache, Nginx, etc.

The data tier maintains the application’s data such as order data, menu data, reservation data, etc. It stores these data in a relational database management system like PostgreSQL. The client tier interacts with the server to make requests and retrieve data from the database. It then displays to the user the data retrieved from the server.

Subsystem Decomposition

Decomposing the system into smaller units called subsystems will help reduce the complexity of the system. Subsystems are just packages holding related classes. Our restaurant management system is also decomposed into subsystems as follows. The major subsystems are ‘Authentication’, ‘Menu’, ‘Reservation’, ‘Order’, and ‘Kitchen’ systems.

subsystems

The Authentication subsystem authenticates a user to grant access based on the role of the user.

The Menu subsystem generates the restaurant menu, which involves assigning a price to each item.

The Reservation system aid the customer in the booking of table and handle the payment of the booking fee in the event of a no-show.

The Order subsystem registers the user selection from the menu. It compile the order for the Point of Sales system for billing. It also send the data to the Kitchen subsystem for processing.

The Kitchen subsystem deals with printing of the Kitchen Order Tickets and adjusting the inventory.

Low-Level Design

The following code shows some of the classes involved in Restaurant Management System Software.

class Person:
    def __init__(
        self, 
        first_name: str, 
        last_name: str, 
        gender: str, 
        phone: int
        ):

class Staff(Person):
    def __init__(
        self, 
        userid: str,
        email: str,
        role: str,
        password: str):
        
    def login(self, email: str, password: str):
        
    def logout(self):
        
class Customer(Person):
    def __init__(
        self, 
        email: str,
        table_id: int):
        
    def add_item_order(self, table_id: int, menu_id: int):
        
    def remove_item_order(self, table_id: int, menu_id: int):
                
    def add_reservation(self, email: str, date: str, time: str):

    def edit_reservation(self, email: str, date: str, time: str):

class Restaurant(Staff):
    def __init__(self):
       
    def add_item_order(self, table_id: int, menu_id: int):
        
    def remove_item_order(self, table_id: int, menu_id: int):
                
    def add_reservation(self, email: str, date: str, time: str):

    def edit_reservation(self, email: str, date: str, time: str):        
                
    def create_invoice(self, student_id: int, amount: int):

class Kitchen(Staff):
    def __init__(self):
       
    def manage_inventory(self, item_id: int, quantity: int):        
                
    def view_order(self, order_id: int):

class Menu:
    def __init__(
        self, 
        menu_id: int, 
        menu_name: str, 
        menu_price: int, 
        menu_description: str, 
        menu_category: str, 
        menu_image: str,
        menu_quantity: int
        ):

class Inventory:
    def __init__(
        self, 
        inventory_id: int, 
        inventory_name: str, 
        inventory_cost: int, 
        inventory_description: str, 
        inventory_category: str, 
        inventory_quantity:int
        ):

The Person class is the base class for all the employees of the restaurant. The Staff class adds the necessary information for authenticating a user. The Restaurant class inherits from the Staff class and has functions to add and remove items to the order, manage table reservations and create invoice to bill the customer.

The Kitchen class also inherit from the Staff class. The Kitchen class can view the customer order and manage the inventory of the ingredients they used.

The Customer class can add item and remove item to the order before submitting. In addition they can create table reservation request and amend it.

The Menu class contains information about the item on the restaurant menu. The Inventory class contains information to keep track of the ingredients used by the restaurant to prepare the food.

Database Design

A restaurant management system software needs to store data about the Order, Menu, Reservations, etc. Therefore, we have identified the major tables that will be implemented on the selected relational database management system.

database-design

The above database diagram shows the schema for the restaurant management software database.

In a relational database system, relationships often come in three different types. These are one-to-one, one-to-many, and many-to-many relationships. The system under consideration has one-to-many and many-to-many relationships.

The Reservation, Order and Menu tables store data of the reservations, orders and menu. Each Order is associated with one or many order items. Each order item has a food item and ingredients associated with it. The Menu table acts as relation between the Restaurant_Staff and Food_Item tables. The Payment table stores the details about a specific order.

Turnkey Software Solutions

Below are some of the turnkey software solutions available:

  • LimeTray
  • LAVU
  • Zoho

In general, the features provided by the turnkey software solutions include customer facing tools such a web base menu to take orders and table reservations. The software provide automatic processing and tranmit the data to the Point Of Sales system for billing. The system also can digitize kitchen order tickets by displaying orders on-screen and provide tools for inventory management.

This article was originally published at OpenGenus:

link to my article at OpenGenus

System Design of IP/ Patent Management System

We will take a look at the key features a IP/ Patent management system (IPMS) needs to offer, its high-level, low-level design, database design, and some of the features that turnkey software solutions offer.

Table of contents:

  1. IP/ Patent Management System (IPMS)
    • Types of Intellectual Property (IP)
    • Patent Docketing
  2. System Requirements
    • Functional Requirements
    • Non - Functional Requirements
    • Software Requirements
  3. System Design
    • Architecture of the System
    • Subsystem Decomposition
    • Low-Level Design
    • Database Design
  4. Turnkey Software Solutions

IP/ Patent Management System (IPMS)

The IPMS is a management and policy encompassing tool that aids in the accumulation and enhancement of the values associated with a good intellectual property (IP) portfolio. When it comes to IP, strategy implementation involves input from several organizational subsystems. As a result, this aids in the maintenance of the framework as well as the portfolio. This process comprises of maintaining the patent portfolio, which includes increasing the flow of potential patents for the decision process. Valuation, cost management and determining the best conversion parameters to extract value from the patent are part of the process.

Types of Intellectual Property (IP)

Generally speaking, Intellectual Property (IP) include the following types: Copyrights, Patents, Trademarks and Trade Secrets.

Copyright refers to the legal rights that authors and artists have over their literary and artistic creations. Books, music, paintings, sculpture, and films are all covered by copyright, as are computer programs, databases, ads, maps, and technical drawings.

An invention is protected by a patent, which is an exclusive right awarded to the inventor. In general, a patent gives the patent owner the right to decide how - or whether - others can use his or her creation. The patent owner gives up this privilege in exchange for making technical information about the invention publicly available in the published patent document.

A trademark is a symbol that distinguishes one company’s goods or services from those of other companies. Trademarks date back to ancient times, when artists used to sign their wares with their signature or “mark.”

IP rights on proprietary knowledge that may be sold or licensed are known as trade secrets. Unauthorized acquisition, use, or disclosure of such secret information by others in a manner that is inconsistent with honest business practices is considered an unfair practice and a breach of trade secret protection.

Patent Docketing

A method or system for handling the patent application process is called patent docketing. The patent application procedure entails a significant amount of documentation. A new portfolio should be uploaded into the docketing system whenever the patent application is drafted. The docketer should make sure the information is specific and thorough. The portfolio contains the inventor’s name, contact information, invention, industry served, and other related papers.

System Requirements

The IP/ Patent management system should at least include the following features,

  1. IP portfolio management such as
    • monitoring of patent application progress (such as submission of additional information)
    • monitoring of deadline of patent application
    • recording of data and documents to support patent application
    • generate report of patent application for submission to patent registration office
  2. IP search function of similar registered patent such as
    • searching within internal database of patents held by the organization
    • searching external database of patent registration office

A better understanding of the functional requirements can be gained from the use-case diagram below. UML-ip-mgmt

Non - Functional Requirements

Usability
The system should provide an interactive user-friendly interface that is easily understandable for all users.

Availability
The System should be available at least during the Patent Registration office operating hours and must be recovered within an hour if it fails. The system should respond to the requests within two seconds.

Dependability
The system should provide consistent performance with easy tracking of records and updating of records.

Maintainability
The software should be easily maintainable and adding new features and making changes to the software must be as simple as possible.

Scalability
For a large organization like a university, the expected user load for the system will be significant. University are a research center with a massive database of information. The ability to scale is critical to the system’s adoption success.

Applying technology such Hadoop’s MapReduce will provide the ability to scale the system. Hadoop is a highly scalable platform, due to its ability to store and distribute enormous data sets across a large number of machines. The servers used here are relatively inexpensive and can run in parallel. The system’s processing power can be enhanced by adding extra servers. Traditional relational database management systems(RDBMS) couldn’t handle big data sets at scale.

One such application of MapReduce is Distributed Grep. In plain-text data collections, Distributed Grep looks for lines that match a regular expression. It’s used to look for a specific pattern in a large number of files. This will come in handy if the patent application reviewer has to look for related patents in a huge database.

Security
A user login mechanism will be established to prevent unauthorized employees from accessing the system. Any member who has access to the system will be requested to create a username and password that will allow them to use the system’s capabilities.

To prevent unauthorized parties from accessing, viewing, or changing parts and information within the system, the system is expected to have several access levels depending on which staff member is accessing the system, such as Inventor, Reviewer or Approver. This means that certain aspects of the system will be restricted to a small group of people based on their position or authority within the organization.

On top of all the other security measures mentioned above, system authentication is by far one of the most significant forms of security that should be included into this system. For certain access levels, an authentication mechanism such as ID scanning may be used to validate specific staff members and grant them access to specific system items.

Software Requirements
  1. A server running Windows Server/Linux OS.
  2. A backend language such as like Java, Python to process the patent applications.
  3. Front-end frameworks like Angular/React/Vue for the user interface.
  4. Relational DBMS such as MySQL, PostgreSQL.

System Design

With our understanding of the functional and non-functional requirements of our system, we will now look at the system architecture, subsystem decomposition, and database design.

Architecture of the System

The IP/ Patent management system follows a simple 3 tier client/server architecture. The client can use web browsers to access the system through the local area network of the organization using the Hypertext transfer protocol secure (HTTPS) protocol.

System-Architecture

The middle tier which includes the server presents the website to the user and controls the business logic. It controls the interactions between the application and the user. The server also send notification to the user through the email server using the Simple Mail Transfer Protocol (SMTP). Common web server technology used here can be Apache, Nginx, etc.

The data tier maintains the application’s data such as patent application data, supporting documents data, existing patent data etc. It stores these data in a relational database management system (RDBMS) like PostgreSQL. The client tier interacts with the server to make requests and retrieve data from the database. It then displays to the user the data retrieved from the server.

Subsystem Decomposition

Decomposing the system into smaller units called subsystems will help reduce the complexity of the system. Subsystems are just packages holding related classes. Our IP/ Patent management system is also decomposed into subsystems as follows. The major subsystems are ‘Authentication’, ‘Documentation’, ‘Report’, ‘Monitor’, and ‘Research’ systems.

subsystems

The Authentication subsystem authenticates a user to grant access based on the role of the user.

The Documentation subsystem facilitate the creation, review and approval of a patent application.

The Report subsystem generate the patent application report for submission to the patent registration office.

The Monitor subsystem monitor the progress of the patent application. In addition, it send reminder/notification of any upcoming patent application deadline.

The Research subsystem deals with retrieval of similar patent that is registered or held by the organization.

Low-Level Design

The following code shows some of the classes involved in IP/ Patent Management System Software.

class Staff:
    def __init__(
        self, 
        first_name: str, 
        last_name: str, 
        gender: str, 
        phone: int
        userid: str,
        email: str,
        role: str,
        password: str
        ):
        
    def login(self, email: str, password: str):
        
    def logout(self):
        
class Inventor(Staff):
    def __init__(self):
       
    def add_patent_application(self, patent_id: int, patent_details: str, doc_filename: str):  
    
    def edit_patent_application(self, patent_id: int, patent_details: str, doc_filename: str):  

class Reviewer(Staff):
    def __init__(self):
       
    def view_patent_application(self, patent_id: int, review_comments: str):  
    
class Approver(Staff):
    def __init__(self): 
        
    def approve_patent_application(self, patent_id: int, review_comments: str, approval_status: str):
      

class Patent:
    def __init__(
        self, 
        patent_id: int, 
        patent_name: str, 
        patent_details: str, 
        doc_filename: str, 
        review_comments: str, 
        approval_status: str
        ):

The Staff class is the base class for all the employees of the organization which include the necessary information for authenticating a user. The Inventor class inherits from the Staff class and has functions to add and edit the patent application.

The Reviewer class also inherit from the Staff class. The Reviewer class can view the patent application and add comments to request more information about the patent application.

The Approver class also inherit from the Staff class. The Approver class can view the patent application and approve/reject the submission of the patent application.

The Patent class contains information about the patent application.

Database Design

A IP/ Patent management system software needs to store data about the Patent application, existing patent, etc. Therefore, we have identified the major tables that will be implemented on the selected RDBMS.

database-design

The above database diagram shows the schema for the IP/ Patent management system software database.

Generally, there are three types of relationships in a relational database system. These are one-to-one, one-to-many, and many-to-many relationships. The system under consideration has one-to-many and many-to-many relationships.

The Patent_Application tables store data of the patent application. The Patent_Application table acts as relation between the Inventor_Staff, Reviewer_Staff and Approver_Staff. The Approved_Patent table stores the details about approved patents held by the organization.

Turnkey Software Solutions

Below are some of the turnkey software solutions available:

  • IPzen Professional
  • Inteum
  • FoundationIP
  • Symphony

In general, the turnkey software solutions include customer facing tools such a web base interface to provide the following features on IP/ Patent management:

  • Information Disclosure Management
  • IP Portfolio Management
  • Trademark Tracking
  • Deadline Management
  • Docket Management
  • Document Management
  • Patent Tracking
  • Renewal Management
  • Spend Management

This article was originally published at OpenGenus:

link to my article at OpenGenus

System Design of Fee Management System

We will go over the essential elements a fee management system must have, its high-level, low-level design and database design. We will also look at some of the features that turnkey software solutions offer.

Table of contents:

  1. Fee Management System
  2. System Requirements
    • Functional Requirements
    • Non - Functional Requirements
    • Software Requirements
  3. High-Level Design
    • For a small setting
    • For a large setting
  4. Low-Level Design and Classes
  5. Database Design of Fee Management System
  6. Turnkey Software Solutions

Fee Management System

A fee management software is a software that automates the collection of fees and generation of receipts. For example in a school environment, the system manages tuition fees, miscellaneous fees, project fees and other charges for student services. It helps the school administrator to automate the fee collection, generate receipts, refunds and invoice reminders. It increases efficiency and reduces the cost needed for maintaining manual records and saves time and effort for both the user and the school administrator.

System Requirements

The key requirements that need to be offered by the fee management system can be classified into functional and non-functional requirements.

Functional Requirements
  1. Allow the school administrator to add and remove new students.
  2. Allow the school administrator to add and manage the subjects and the associated fees.
  3. Users can retrieve invoice and make payment.
  4. The system should notify the user and school administrator about the overdue invoices.

A more detailed list of key features that need to be supported by the system is given in the use case diagram. UML-fee-mgmt-system

There are 3 actors in the use case diagram. The User, the School Administrator and the System.

User - The user can log in, make payment for the invoice, print a copy of the invoice for their records.

School Administrator - The School Administrator registers new student account, calculate the fees payable and print out the invoices to issue to the students.

System - The system is the fee management system itself. It keeps track of the invoices and sends notifications to the user and school administrator about the overdue invoices.

Non - Functional Requirements

Usability
Usability is the main non-functional requirement for a fee management system. Everyone should be able to understand the user interface (UI) and obtain the necessary information without the need for any specialized training. Different languages can be provided based on the requirements.

Accuracy
Accuracy is another important non-functional requirement for the fee management system. The fees calculated should be correct, consistent, and reliable.

Availability
The System should be available at least during the academic year and must be recovered within an hour if it fails. The system should respond to the requests within two seconds.

Maintainability
The software should be easily maintainable, with adding new features and making changes as simple as possible.

Software Requirements

  1. A server running Windows Server/Linux OS
  2. A backend language such as like Java, Python to do the computation of the fees
  3. Front-end frameworks like Angular/React/Vue for the user interface.
  4. Relational DBMS such as MySQL, PostgreSQL.
  5. Containers and orchestration services like Kubernetes (for a large setting such as a University).

High Level Design

For a Small Setting (School):

high-level-design-small

In a school environment, the users are the students. The traffic is usually low with an estimated 1,000 concurrent users at the peak. A group of servers each with 16GB of RAM and a capable processor should be able to handle the load. Each server may need to handle several requests at a time. The requests can be queued and a reply can be sent to the user while the request is being processed.

The user can access the system through a client site or app with a registered account. The school administrator can access the admin dashboard and interface using the admin account.

Handling the authentication is done by a separate service. The authentication service has direct access to the accounts on the database. The notification service checks the database for any overdue invoices and alerts the user and the school administrator.

Data such as user accounts and fees information is primarily relational. We can use a relational database like MySQL to store them. For backup and availability, the database servers can be configured in a master-slave arrangement.

For a Large Setting (University):

high-level-design-large

For some Universities, the number of concurrent users can get up to 10,000 and above at its peak. These systems can also have separate user and admin interfaces for students from different faculty.

The servers may be distributed across different regions in the country with load balancers to distribute the traffic. Reverse proxy servers may be deploy for web acceleration using techniques such as compression, SSL termination and caching request. Services such as authentication and notification may be deployed as separate containers. This makes scalability considerably easier and makes it simpler to add new services.

We can adopt distributed database approach to spread the database across different campus locations. The data is located at the campus where it is most frequently accessed, yet each authorized user has access to the whole database.

Low-Level Design and Classes

Let’s examine the various classes and functions associated with the fee management system in order to get a better understanding of the system.

class Person:
    def __init__(
        self, 
        first_name: str, 
        last_name: str, 
        gender: str, 
        address: str, 
        phone: int
        ):

        
class Student(Person):
    def __init__(
        self, 
        userid: str,
        email: str,
        password: str):
        
    def login(self, email: str, password: str):
        
    def logout(self):
                
    def payInvoice(self, invoice_id: int):


class Administrator(Person):
    def __init__(
        self, 
        userid: str,
        email: str,
        password: str):
        
    def login(self, email: str, password: str):
        
    def logout(self):
                
    def createInvoice(self, student_id: int, amount: int):

The Person class is the base class and contains fields for the basic information such as name, address, etc. The Student class inherits the Person class and adds fields such as a unique user id, login email, password and functions to login and logout. Both the Student and Administrator classes inherit the Person class. The student can pay for the invoice and the school administrator can create invoices for the students.

Database Design in Fee Management System

database-design

The above image shows one possible database schema for the fee management system.

The student table stores the user details such as a unique user id, name, contact, and login information. The admin table references the user id as a foreign key and stores the role of the admin such as Supervisor, Assistant, etc.

The studentBill table contains the information about the id of the Student, the amount payable, category of the fees and a brief description of the invoice. The category table stores the charges corresponding to the invoice category. The payment table stores the amount paid and when the payment was made.

Turnkey Software Solutions

Below are some of the turnkey software solutions available:

  • SkoolBeep
  • Edumarshal
  • Camu

In general, the features provided by the turnkey software solutions include report generation facility such as fee receipts, fee payment reports that allow tracking of outstanding invoices. The software provide both automatic online and offline data backup facilities. In addition, the system is able to sync with payment gateways for a secure transaction. Real-time notification in the form of SMS, email, push notifications keep the students informed about fee dues and financial transactions.

This article was originally published at OpenGenus:

link to my article at OpenGenus