khushpatel2002 commited on
Commit
e4806d5
1 Parent(s): 5e1486f

Upload 22 files

Browse files
__init__.py ADDED
File without changes
algorithm/__init__.py ADDED
File without changes
algorithm/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (160 Bytes). View file
 
algorithm/__pycache__/solver.cpython-310.pyc ADDED
Binary file (4.28 kB). View file
 
algorithm/plot2D.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+
4
+ def plot_2d(C, A, B, initial_point):
5
+ n_constraints = A.shape[0] # Get the number of constraints
6
+
7
+ plt.figure(figsize=(8, 6))
8
+ plt.xlim(0, 5) # Adjust based on your problem
9
+ plt.ylim(0, 5) # Adjust based on your problem
10
+ plt.xlabel('X1')
11
+ plt.ylabel('X2')
12
+
13
+ # Plot feasible region (constraints)
14
+ x1 = np.linspace(0, 5, 100)
15
+
16
+ for i in range(n_constraints):
17
+ x2_i = (B[i] - A[i, 0] * x1) / A[i, 1] # Calculate x2 values for each constraint
18
+ plt.plot(x1, x2_i, label=f'{A[i, 0]}*X1 + {A[i, 1]}*X2 <= {B[i]}')
19
+
20
+ # Fill the feasible region
21
+ min_x2 = np.minimum.reduce([(B[i] - A[i, 0] * x1) / A[i, 1] for i in range(n_constraints)])
22
+ plt.fill_between(x1, min_x2, 0, where=(x1 >= 0) & (x1 <= 5), alpha=0.2)
23
+
24
+ # Plot the initial feasible solution point
25
+ plt.scatter(initial_point[0], initial_point[1], color='red', marker='o', label='Initial Point')
26
+
27
+ plt.legend()
28
+ plt.show()
29
+
30
+ # Example usage with variable-sized A and B:
31
+ C = np.array([2, 3]) # Objective function coefficients
32
+ A = np.array([[1, 1], # Constraint matrix
33
+ [2, 1],
34
+ [3, 1]]) # Add more rows for additional constraints
35
+ B = np.array([4, 5, 6]) # Right-hand side, add more values for additional constraints
36
+ initial_point = np.array([1.0, 3.0])
37
+ plot_2d(C, A, B, initial_point)
algorithm/solver.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from ast_parser.parser import EquationKind # Assuming EquationKind is imported from another module
3
+
4
+ class Solver:
5
+ """Solver takes in the equations which have been parsed from the input,
6
+ it converts them to matrix arrays and applies the matrix operations on
7
+ them.
8
+
9
+ Args:
10
+ objective_functions (list of Equation objects): List of objective functions.
11
+ constraints (list of Equation objects): List of constraint equations.
12
+ """
13
+ def __init__(self, objective_functions, constraints):
14
+ # Extract objective function variables and negate their coefficients
15
+ self.objective_functions = objective_functions[0].variables
16
+ self.objective_functions.pop('Z')
17
+
18
+ self.constraints = []
19
+
20
+ for i, _ in enumerate(constraints):
21
+ self.constraints.append(constraints[i])
22
+
23
+ # Negate coefficients of objective function for maximization
24
+ for key in self.objective_functions:
25
+ self.objective_functions[key] *= -1
26
+
27
+ # Add slack variables to constraints if necessary
28
+ for temp_dict in constraints:
29
+ for key in self.objective_functions.keys():
30
+ if key not in temp_dict.variables:
31
+ temp_dict.variables[key] = 0
32
+
33
+ slack = len(self.constraints)
34
+ i = 0
35
+
36
+ for _, constraint in enumerate(self.constraints):
37
+ if constraint.kind == EquationKind.LEQ:
38
+ constraint.variables["s_" + str(i)] = 1.0
39
+ self.objective_functions["s_" + str(i)] = 0
40
+ i += 1
41
+
42
+ while i < slack:
43
+ constraint.variables["s_" + str(i)] = 1.0
44
+ self.objective_functions["s_" + str(i)] = 0
45
+ i += 1
46
+
47
+ self.A, self.B, self.C = self.convert_to_matrices()
48
+
49
+
50
+ def get_results(self):
51
+
52
+ objective_values, solution, variable_names = self.advanced_simplex(self.A, self.B, self.C)
53
+ variable_names = list(variable_names.values())
54
+ results_str = "The vector of decision variables is : \n"
55
+ for i in range(len(objective_values)):
56
+ results_str+= f"{variable_names[i]} : {round(objective_values[i, 0], 2)}\n"
57
+ results_str+= "The optimal solution is {}\n".format(solution)
58
+ return results_str
59
+
60
+ def convert_to_matrices(self):
61
+ """Converts objective functions and constraints to matrices.
62
+
63
+ Returns:
64
+ A (numpy.ndarray): Coefficients matrix for constraints.
65
+ B (numpy.ndarray): Right-hand side matrix for constraints.
66
+ C (numpy.ndarray): Coefficients matrix for objective function.
67
+ """
68
+ num_constraints = len(self.constraints)
69
+ num_variables = len(self.objective_functions)
70
+
71
+ A = np.zeros((num_constraints, num_variables))
72
+ C = np.zeros((1, num_variables))
73
+ B = np.zeros((num_constraints, 1))
74
+
75
+ for i, constraint in enumerate(self.constraints):
76
+ for variable_name, coefficient in constraint.variables.items():
77
+ j = list(self.objective_functions.keys()).index(variable_name)
78
+ A[i, j] = coefficient
79
+
80
+ for variable_name, coefficient in self.objective_functions.items():
81
+ j = list(self.objective_functions.keys()).index(variable_name)
82
+ C[0, j] = coefficient
83
+
84
+ B[i, 0] = constraint.bound
85
+
86
+ return A, B, C
87
+
88
+ def advanced_simplex(self, A, b, C):
89
+ """Performs the advanced simplex algorithm to find the optimal solution.
90
+
91
+ Args:
92
+ A (numpy.ndarray): Coefficients matrix for constraints.
93
+ b (numpy.ndarray): Right-hand side matrix for constraints.
94
+ C (numpy.ndarray): Coefficients matrix for objective function.
95
+
96
+ Returns:
97
+ objective_values (numpy.ndarray): The values of the objective function variables.
98
+ solution (float): The optimal solution value.
99
+ """
100
+ n, m = A.shape
101
+
102
+ B = np.eye(n)
103
+ C_B = np.zeros((1, n))
104
+
105
+ count = 0
106
+
107
+ variable_names = list(self.objective_functions.keys())[-n:]
108
+ variable_names = {key: value for key, value in zip(range(n), variable_names)}
109
+
110
+ prev_solution = float('inf')
111
+ while True:
112
+ count += 1
113
+ B_inverse = np.around(np.linalg.inv(B), decimals=2)
114
+ for row in B_inverse:
115
+ for value in row:
116
+ if value == -0:
117
+ value = 0
118
+
119
+ X_B = np.matmul(B_inverse, b)
120
+ P_table = np.round(np.matmul(B_inverse, A), 2)
121
+ objective_values = np.matmul(C_B, P_table) - C
122
+ solution = np.round(np.matmul(C_B, X_B), 2)
123
+
124
+ if abs(prev_solution - solution) < 0.0001:
125
+ return X_B, solution, variable_names
126
+
127
+ entering_var_idx = np.argmin(objective_values)
128
+
129
+ ratios = []
130
+ for i in range(n):
131
+ if P_table[i, entering_var_idx] > 0:
132
+ ratios.append(X_B[i, 0] / P_table[i, entering_var_idx])
133
+ else:
134
+ ratios.append(np.inf)
135
+
136
+ exiting_var_idx = np.argmin(ratios)
137
+
138
+ temp_list = list(self.objective_functions.keys())
139
+
140
+ variable_names[exiting_var_idx] = temp_list[entering_var_idx]
141
+ B[:, exiting_var_idx] = A[:, entering_var_idx]
142
+ C_B[:, exiting_var_idx] = C[:, entering_var_idx]
143
+ prev_solution = solution
app.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+
3
+ from ast_parser import Parser, EquationKind
4
+ from algorithm import solver
5
+
6
+ def process(input_equation):
7
+
8
+ parser = Parser(input_equation)
9
+
10
+ equations = tuple(parser)
11
+
12
+ for equation in equations:
13
+ if equation.kind == EquationKind.GEQ and equation.bound < 0.0:
14
+ for variable, coefficient in equation.variables.items():
15
+ equation.variables[variable] *= -1.0
16
+
17
+ equation.bound *= -1.0
18
+ equation.kind = EquationKind.LEQ
19
+
20
+ continue
21
+
22
+ if equation.kind == EquationKind.GEQ:
23
+ raise ValueError("Equation kind must be either EQ or LEQ")
24
+ if equation.bound < 0.0:
25
+ raise ValueError("Equation bound must be non-negative")
26
+
27
+ demand: dict[str, list[int]] = {}
28
+ for i, equation in enumerate(equations):
29
+ for variable, coefficient in equation.variables.items():
30
+ if coefficient == 0.0:
31
+ continue
32
+
33
+ if variable not in demand:
34
+ demand[variable] = []
35
+
36
+ demand[variable].append(i)
37
+
38
+ objective_variables = tuple(
39
+ filter(lambda variable: len(demand[variable]) == 1, demand.keys())
40
+ )
41
+
42
+ objective_functions = tuple(
43
+ filter(
44
+ lambda function: function.kind == EquationKind.EQ,
45
+ map(lambda variable: equations[demand[variable][0]], objective_variables),
46
+ )
47
+ )
48
+
49
+ if len(objective_variables) != len(objective_functions):
50
+ raise ValueError("Objective functions must be equalities")
51
+
52
+ constraints = tuple(
53
+ filter(
54
+ lambda function: function not in objective_functions,
55
+ equations,
56
+ )
57
+ )
58
+
59
+ solver_instance = solver.Solver(objective_functions, constraints)
60
+
61
+ results = solver_instance.get_results()
62
+ return results
63
+
64
+ description = "This app calculates optimum value using Simplex Method. Enter all equations, following each with a comma. The last equation should be without a comma. As an example 'Z = 5x_1 + 4x_2', '6x_1 + 4x_2 <= 24'"
65
+ examples = [["Z = 5x_1 + 4x_2,6x_1 + 4x_2 <= 24"]]
66
+ demo = gr.Interface(fn=process, inputs="text", outputs="text", title="Optimization Assignment",examples=examples,description=description)
67
+
68
+ demo.launch()
ast_parser/__init__.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ast_parser.chars import (
2
+ is_alpha,
3
+ is_ascii,
4
+ is_coefficient_start,
5
+ is_digit,
6
+ is_lower_alpha,
7
+ is_upper_alpha,
8
+ is_variable_continue,
9
+ is_variable_start,
10
+ print_char_code,
11
+ )
12
+ from ast_parser.errors import LexerException, LinterException, PositionedException
13
+ from ast_parser.lexer import Lexer
14
+ from ast_parser.linter import Linter
15
+ from ast_parser.parser import Equation, EquationKind, Parser
16
+ from ast_parser.token import (
17
+ Location,
18
+ Token,
19
+ TokenKind,
20
+ is_binary_operator,
21
+ is_relational_operator,
22
+ )
23
+
24
+ __all__ = (
25
+ "is_digit",
26
+ "is_coefficient_start",
27
+ "is_lower_alpha",
28
+ "is_upper_alpha",
29
+ "is_alpha",
30
+ "is_variable_start",
31
+ "is_variable_continue",
32
+ "is_ascii",
33
+ "print_char_code",
34
+ "PositionedException",
35
+ "LexerException",
36
+ "LinterException",
37
+ "Lexer",
38
+ "Linter",
39
+ "EquationKind",
40
+ "Equation",
41
+ "Parser",
42
+ "TokenKind",
43
+ "Location",
44
+ "Token",
45
+ "is_binary_operator",
46
+ "is_relational_operator",
47
+ )
ast_parser/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (984 Bytes). View file
 
ast_parser/__pycache__/chars.cpython-310.pyc ADDED
Binary file (3.24 kB). View file
 
ast_parser/__pycache__/errors.cpython-310.pyc ADDED
Binary file (1.96 kB). View file
 
ast_parser/__pycache__/lexer.cpython-310.pyc ADDED
Binary file (9.67 kB). View file
 
ast_parser/__pycache__/linter.cpython-310.pyc ADDED
Binary file (7.22 kB). View file
 
ast_parser/__pycache__/parser.cpython-310.pyc ADDED
Binary file (6.72 kB). View file
 
ast_parser/__pycache__/token.cpython-310.pyc ADDED
Binary file (2.35 kB). View file
 
ast_parser/chars.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ast_parser.token import TokenKind
2
+
3
+
4
+ def is_digit(code: int) -> bool:
5
+ """Check if code is a digit.
6
+
7
+ Args:
8
+ code (int): Unicode code point.
9
+
10
+ Returns:
11
+ bool: True if code is a digit, False otherwise.
12
+ """
13
+ return 0x0030 <= code <= 0x0039 # <digit>
14
+
15
+
16
+ def is_coefficient_start(code: int) -> bool:
17
+ """Check if code is a coefficient start.
18
+
19
+ The Coefficient can start with a digit or a dot.
20
+
21
+ Args:
22
+ code (int): Unicode code point.
23
+
24
+ Returns:
25
+ bool: True if code is a Coefficient start, False otherwise.
26
+ """
27
+ return is_digit(code) or code == 0x002E # <digit> | `.`
28
+
29
+
30
+ def is_lower_alpha(code: int) -> bool:
31
+ """Check if code is a lowercased alpha.
32
+
33
+ Args:
34
+ code (int): Unicode code point.
35
+
36
+ Returns:
37
+ bool: True if code is a lowercased alpha, False otherwise.
38
+ """
39
+ return 0x0061 <= code <= 0x007A # <lower_alpha>
40
+
41
+
42
+ def is_upper_alpha(code: int) -> bool:
43
+ """Check if code is an uppercased alpha.
44
+
45
+ Args:
46
+ code (int): Unicode code point.
47
+
48
+ Returns:
49
+ bool: True if code is an uppercased alpha, False otherwise.
50
+ """
51
+ return 0x0041 <= code <= 0x005A # <upper_alpha>
52
+
53
+
54
+ def is_alpha(code: int) -> bool:
55
+ """Check if code is an alpha.
56
+
57
+ Args:
58
+ code (int): Unicode code point.
59
+
60
+ Returns:
61
+ bool: True if code is an alpha, False otherwise.
62
+ """
63
+ return is_lower_alpha(code) or is_upper_alpha(code) # <alpha>
64
+
65
+
66
+ def is_variable_start(code: int) -> bool:
67
+ """Check if code is a variable start.
68
+
69
+ The Variable can start with an alpha or an underscore.
70
+
71
+ Args:
72
+ code (int): Unicode code point.
73
+
74
+ Returns:
75
+ bool: True if code is a Variable start, False otherwise.
76
+ """
77
+ return is_alpha(code) or code == 0x005F # <alpha> | `_`
78
+
79
+
80
+ def is_variable_continue(code: int) -> bool:
81
+ """Check if code is a variable continue.
82
+
83
+ The Variable can continue with an alpha, an underscore or a digit.
84
+
85
+ Args:
86
+ code (int): Unicode code point.
87
+
88
+ Returns:
89
+ bool: True if code is a Variable continue, False otherwise.
90
+ """
91
+ return is_variable_start(code) or is_digit(code) # <alpha> | `_` | <digit>
92
+
93
+
94
+ def is_ascii(code: int) -> bool:
95
+ """Check if code is an ASCII.
96
+
97
+ Args:
98
+ code (int): Unicode code point.
99
+
100
+ Returns:
101
+ bool: True if code is an ASCII, False otherwise.
102
+ """
103
+ return 0x0020 <= code <= 0x007E # <ASCII>
104
+
105
+
106
+ def print_char_code(code: int | None) -> str:
107
+ """Describe code as a character or Unicode code point.
108
+
109
+ Args:
110
+ code (int | None): Unicode code point. None for EOF.
111
+
112
+ Returns:
113
+ str: Character or Unicode code point.
114
+ """
115
+ if code is None: # <EOF>
116
+ return TokenKind.EOF.value
117
+
118
+ return chr(code) if is_ascii(code) else f"U+{code:04X}"
119
+
120
+
121
+ __all__ = (
122
+ "is_digit",
123
+ "is_coefficient_start",
124
+ "is_lower_alpha",
125
+ "is_upper_alpha",
126
+ "is_alpha",
127
+ "is_variable_start",
128
+ "is_variable_continue",
129
+ "is_ascii",
130
+ "print_char_code",
131
+ )
ast_parser/errors.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ast_parser.token import Location
2
+
3
+
4
+ class PositionedException(Exception):
5
+ """Base class for exceptions with Location.
6
+
7
+ Args:
8
+ source (str): The source string being tokenized.
9
+ location (Location): The Location of the exception.
10
+ description (str, optional): An optional description of the
11
+ error.
12
+ """
13
+
14
+ source: str
15
+ """The source string being tokenized."""
16
+ location: Location
17
+ """The Location of the error in the source."""
18
+
19
+ def __init__(
20
+ self, source: str, location: Location, description: str | None = None
21
+ ) -> None:
22
+ super().__init__(description)
23
+
24
+ self.source = source
25
+ self.location = location
26
+
27
+
28
+ class LexerException(PositionedException):
29
+ """A LexerException is raised when the Lexer encounters an invalid
30
+ character or token.
31
+
32
+ Args:
33
+ source (str): The source string being tokenized.
34
+ location (Location): The Location of the exception.
35
+ description (str, optional): An optional description of the
36
+ error.
37
+ """
38
+
39
+
40
+ class LinterException(PositionedException):
41
+ """A LinterException is raised when the Linter encounters an invalid
42
+ token chain.
43
+
44
+ Args:
45
+ source (str): The source string being tokenized.
46
+ location (Location): The Location of the exception.
47
+ description (str, optional): An optional description of the
48
+ error.
49
+ """
50
+
51
+
52
+ __all__ = ("PositionedException", "LexerException", "LinterException")
ast_parser/lexer.py ADDED
@@ -0,0 +1,382 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable
4
+
5
+ from ast_parser.chars import (
6
+ is_coefficient_start,
7
+ is_digit,
8
+ is_variable_continue,
9
+ is_variable_start,
10
+ print_char_code,
11
+ )
12
+ from ast_parser.errors import LexerException
13
+ from ast_parser.token import Location, Token, TokenKind
14
+
15
+
16
+ class Lexer:
17
+ """A Lexer is a stateful stream generator in that every time it is
18
+ advanced, it returns the next token in the source.
19
+
20
+ Assuming the source lexes, the final Token emitted by the Lexer
21
+ will be of kind EOF, after which the Lexer will repeatedly return
22
+ the same EOF token whenever called.
23
+
24
+ Args:
25
+ source (str): The source string being tokenized.
26
+ """
27
+
28
+ _source: str
29
+ """The source string being tokenized."""
30
+
31
+ _token: Token
32
+ """The currently active Token."""
33
+
34
+ _line: int
35
+ """The current line number."""
36
+ _line_start: int
37
+ """The index of the start of the current line."""
38
+
39
+ def __init__(self, source: str) -> None:
40
+ self._source = source
41
+
42
+ self._token = Token(TokenKind.SOF, 0, 0, Location(0, 0), "")
43
+
44
+ self._line = 1
45
+ self._line_start = 0
46
+
47
+ @property
48
+ def source(self) -> str:
49
+ """Gets the source string being tokenized.
50
+
51
+ Returns:
52
+ str: The source string being tokenized.
53
+ """
54
+ return self._source
55
+
56
+ def __iter__(self) -> Lexer:
57
+ """Gets an iterator over the Tokens in the source.
58
+
59
+ Returns:
60
+ Lexer: An iterator over the tokens in the source.
61
+ """
62
+ return self
63
+
64
+ def __next__(self) -> Token:
65
+ """Gets the next Token from the source.
66
+
67
+ Raises:
68
+ StopIteration: The end of the source has been reached.
69
+ LexerException: Unexpected character, less than operator is
70
+ not allowed.
71
+ LexerException: Unexpected character, greater than operator
72
+ is not allowed.
73
+ LexerException: Invalid character: <code>.
74
+ LexerException: Invalid coefficient, unexpected digit after
75
+ 0: <code>.
76
+ LexerException: Invalid coefficient, expected digit but
77
+ got: <code>.
78
+
79
+ Returns:
80
+ Token: The next token from the source.
81
+ """
82
+ last_token = self._token
83
+ next_token = self._next_token()
84
+
85
+ if last_token.kind == TokenKind.EOF and next_token.kind == TokenKind.EOF:
86
+ if last_token.prev_token is None:
87
+ raise LexerException(
88
+ self._source,
89
+ last_token.location,
90
+ "Crude modification of the token chain is detected",
91
+ )
92
+
93
+ if last_token.prev_token.kind == TokenKind.EOF:
94
+ raise StopIteration
95
+
96
+ self._token = next_token
97
+
98
+ return last_token
99
+
100
+ self._token.next_token = next_token
101
+ next_token.prev_token = self._token
102
+
103
+ self._token = next_token
104
+
105
+ return last_token
106
+
107
+ def _create_token(self, kind: TokenKind, start: int, end: int, value: str) -> Token:
108
+ """Creates a token with the given parameters.
109
+
110
+ A token is created relative to the current state of the Lexer.
111
+
112
+ Args:
113
+ kind (TokenKind): The kind of token.
114
+ start (int): The index of the first character of the token.
115
+ end (int): The index of the first character after the token.
116
+ value (str): The value of the token.
117
+
118
+ Returns:
119
+ Token:
120
+ """
121
+ location = Location(self._line, 1 + start - self._line_start)
122
+
123
+ return Token(kind, start, end, location, value, self._token)
124
+
125
+ def _read_code(self, position: int) -> int | None:
126
+ """Reads the character code at the given position in the source.
127
+
128
+ Args:
129
+ position (int): The index of the character to read.
130
+
131
+ Returns:
132
+ int | None: The character code at the given position, or
133
+ None if the position is out of bounds.
134
+ """
135
+ return ord(self._source[position]) if position < len(self._source) else None
136
+
137
+ def _read_while(self, start: int, predicate: Callable[[int], bool]) -> int:
138
+ """Reads a sequence of characters from the source starting at
139
+ the given position while the predicate is satisfied.
140
+
141
+ Args:
142
+ start (int): The index of the first character of the token.
143
+ predicate (Callable[[int], bool]): A function that takes a
144
+ character code and returns whether it satisfies the
145
+ predicate.
146
+
147
+ Returns:
148
+ int: The index of the first character after the sequence.
149
+ """
150
+ position = start
151
+
152
+ while position < len(self._source) and predicate(ord(self._source[position])):
153
+ # print(
154
+ # self._source[position],
155
+ # ord(self._source[position]),
156
+ # is_digit(ord(self._source[position])),
157
+ # )
158
+ position += 1
159
+
160
+ return position
161
+
162
+ def _read_digits(self, start: int, first_code: int | None) -> int:
163
+ """Reads a sequence of digits from the source starting at the
164
+ given position.
165
+
166
+ Args:
167
+ start (int): The index of the first character of the token.
168
+ first_code (int | None): The code of the first character of
169
+ the token.
170
+
171
+ Raises:
172
+ LexerException: Unexpected character, expected digit but
173
+ got: <code>.
174
+
175
+ Returns:
176
+ int: The index of the first character after the digits.
177
+ """
178
+ if not is_digit(first_code): # not <digit>
179
+ raise LexerException(
180
+ self._source,
181
+ Location(self._line, 1 + start - self._line_start),
182
+ f"Unexpected character, expected digit but got: {print_char_code(first_code)}",
183
+ )
184
+
185
+ return self._read_while(start + 1, is_digit)
186
+
187
+ def _read_coefficient(self, start: int, first_code: int) -> Token:
188
+ """Reads a coefficient token from the source starting at the
189
+ given position.
190
+
191
+ Args:
192
+ start (int): The index of the first character of the token.
193
+ first_code (int): The code of the first character of the
194
+ token.
195
+
196
+ Raises:
197
+ LexerException: Invalid coefficient, expected digit but
198
+ got: <code>.
199
+
200
+ Returns:
201
+ Token: The coefficient token.
202
+ """
203
+ position, code = start, first_code
204
+
205
+ # Leftmost digits.
206
+ if code == 0x0030: # `0`
207
+ position += 1
208
+ code = self._read_code(position)
209
+
210
+ if is_digit(code): # <digit>
211
+ raise LexerException(
212
+ self._source,
213
+ Location(self._line, 1 + position - self._line_start),
214
+ f"Invalid coefficient, unexpected digit after 0: {print_char_code(code)}",
215
+ )
216
+ elif code != 0x002E: # not `.`
217
+ position = self._read_digits(position, code)
218
+ code = self._read_code(position)
219
+
220
+ # Rightmost digits.
221
+ if code == 0x002E: # `.`
222
+ position += 1
223
+ code = self._read_code(position)
224
+
225
+ position = self._read_digits(position, code)
226
+ code = self._read_code(position)
227
+
228
+ # Exponent.
229
+ if code in (0x0045, 0x0065): # `E` | `e`
230
+ position += 1
231
+ code = self._read_code(position)
232
+
233
+ if code in (0x002B, 0x002D): # `+` | `-`
234
+ position += 1
235
+ code = self._read_code(position)
236
+
237
+ position = self._read_digits(position, code)
238
+
239
+ return self._create_token(
240
+ TokenKind.COEFFICIENT, start, position, self._source[start:position]
241
+ )
242
+
243
+ def _read_variable(self, start: int) -> Token:
244
+ """Reads a variable token from the source starting at the given
245
+ position.
246
+
247
+ Args:
248
+ start (int): The index of the first character of the token.
249
+
250
+ Returns:
251
+ Token: The variable token.
252
+ """
253
+ position = self._read_while(start + 1, is_variable_continue)
254
+
255
+ return self._create_token(
256
+ TokenKind.VARIABLE, start, position, self._source[start:position]
257
+ )
258
+
259
+ def _next_token(self) -> Token:
260
+ """Gets the next token from the source starting at the given
261
+ position.
262
+
263
+ This skips over whitespace until it finds the next lexable
264
+ token, then lexes punctuators immediately or calls the
265
+ appropriate helper function for more complicated tokens.
266
+
267
+ Raises:
268
+ LexerException: Unexpected character, less than operator is
269
+ not allowed.
270
+ LexerException: Unexpected character, greater than operator
271
+ is not allowed.
272
+ LexerException: Invalid character: <code>.
273
+ LexerException: Invalid coefficient, unexpected digit after
274
+ 0: <code>.
275
+ LexerException: Invalid coefficient, expected digit but
276
+ got: <code>.
277
+
278
+ Returns:
279
+ Token: The next token from the source.
280
+ """
281
+ position = self._token.end
282
+
283
+ while position < len(self._source):
284
+ char = self._source[position]
285
+ code = ord(char)
286
+
287
+ match code:
288
+ # Ignored:
289
+ # - unicode BOM;
290
+ # - white space;
291
+ # - line terminator.
292
+ case 0xFEFF | 0x0009 | 0x0020: # <BOM> | `\t` | <space>
293
+ position += 1
294
+
295
+ continue
296
+ case 0x000A: # `\n`
297
+ position += 1
298
+
299
+ self._line += 1
300
+ self._line_start = position
301
+
302
+ continue
303
+ case 0x000D: # `\r`
304
+ position += (
305
+ 2 if self._read_code(position + 1) == 0x000A else 1
306
+ ) # `\r\n` | `\r`
307
+
308
+ self._line += 1
309
+ self._line_start = position
310
+
311
+ continue
312
+ # Single-char tokens:
313
+ # - binary plus and minus operators;
314
+ # - multiplication operator;
315
+ # - relational operators;
316
+ # - comma.
317
+ case 0x002B | 0x002D | 0x002A: # `+` | `-` | `*`
318
+ return self._create_token(
319
+ TokenKind(char), position, position + 1, char
320
+ )
321
+ case 0x003D: # `=`
322
+ if self._read_code(position + 1) == 0x003D:
323
+ return self._create_token(
324
+ TokenKind.EQ, position, position + 2, "=="
325
+ )
326
+
327
+ return self._create_token(
328
+ TokenKind.EQ, position, position + 1, char
329
+ )
330
+ case 0x003C: # `<`
331
+ if self._read_code(position + 1) == 0x003D: # `=`
332
+ return self._create_token(
333
+ TokenKind.LEQ, position, position + 2, "<="
334
+ )
335
+
336
+ raise LexerException(
337
+ self._source,
338
+ Location(self._line, 1 + position - self._line_start),
339
+ "Unexpected character, less than operator is not allowed",
340
+ )
341
+ case 0x003E: # `>`
342
+ if self._read_code(position + 1) == 0x003D: # `=`
343
+ return self._create_token(
344
+ TokenKind.GEQ, position, position + 2, ">="
345
+ )
346
+
347
+ raise LexerException(
348
+ self._source,
349
+ Location(self._line, 1 + position - self._line_start),
350
+ "Unexpected character, greater than operator is not allowed",
351
+ )
352
+ case 0x2264: # `≤`
353
+ return self._create_token(
354
+ TokenKind.LEQ, position, position + 1, char
355
+ )
356
+ case 0x2265: # `≥`
357
+ return self._create_token(
358
+ TokenKind.GEQ, position, position + 1, char
359
+ )
360
+ case 0x002C: # `,`
361
+ return self._create_token(
362
+ TokenKind.COMMA, position, position + 1, char
363
+ )
364
+
365
+ # Multi-char tokens:
366
+ # - coefficient;
367
+ # - variable.
368
+ if is_coefficient_start(code): # <digit> | `.`
369
+ return self._read_coefficient(position, code)
370
+ if is_variable_start(code): # <alpha> | `_`
371
+ return self._read_variable(position)
372
+
373
+ raise LexerException(
374
+ self._source,
375
+ Location(self._line, 1 + position - self._line_start),
376
+ f"Invalid character: {print_char_code(code)}",
377
+ )
378
+
379
+ return self._create_token(TokenKind.EOF, position, position, "")
380
+
381
+
382
+ __all__ = ("Lexer",)
ast_parser/linter.py ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ast_parser.errors import LinterException
2
+ from ast_parser.token import (
3
+ Token,
4
+ TokenKind,
5
+ is_binary_operator,
6
+ is_relational_operator,
7
+ )
8
+
9
+
10
+ class Linter:
11
+ """Linter enables real-time validation of the token chain.
12
+
13
+ Args:
14
+ source (str): The source string being tokenized.
15
+ """
16
+
17
+ _source: str
18
+
19
+ _variable_provided: bool
20
+ _relation_provided: bool
21
+
22
+ def __init__(self, source: str) -> None:
23
+ self._source = source
24
+
25
+ self._variable_provided = False
26
+ self._relation_provided = False
27
+
28
+ def lint(self, token: Token) -> None:
29
+ """The lint method checks the token for integrity, validity and
30
+ relevance. It raises a LinterException if the token is invalid
31
+ or unexpected.
32
+
33
+ Args:
34
+ token (Token): The starting token.
35
+ source (str): The source string being tokenized. Used for
36
+ exception messages.
37
+
38
+ Raises:
39
+ LinterException: Unexpected binary operator, term missed.
40
+ LinterException: Equation must contain only one relational
41
+ operator.
42
+ LinterException: Term must contain no more than one
43
+ variable.
44
+ LinterException: Unexpected comma at the beginning of the
45
+ equation.
46
+ LinterException: Unexpected comma, equation missed.
47
+ LinterException: Unexpected comma at the end of the
48
+ equation.
49
+ LinterException: Equation must contain a relational
50
+ operator.
51
+ """
52
+ if token.kind != TokenKind.SOF and (
53
+ token.prev_token is None or token.prev_token.next_token is not token
54
+ ):
55
+ raise LinterException(
56
+ self._source,
57
+ token.location,
58
+ "Crude modification of the token chain is detected",
59
+ )
60
+
61
+ if token.kind == TokenKind.EOF:
62
+ self._lint_eof(token)
63
+
64
+ if is_binary_operator(token):
65
+ self._lint_binary_operator(token)
66
+
67
+ self._variable_provided = False
68
+
69
+ if token.kind == TokenKind.MUL:
70
+ self._lint_multiplication_operator(token)
71
+
72
+ if is_relational_operator(token):
73
+ self._lint_relational_operator(token)
74
+
75
+ self._variable_provided = False
76
+ self._relation_provided = True
77
+
78
+ if token.kind == TokenKind.VARIABLE:
79
+ self._lint_variable(token)
80
+
81
+ self._variable_provided = True
82
+
83
+ if token.kind == TokenKind.COMMA:
84
+ self._lint_comma(token)
85
+
86
+ self._variable_provided = False
87
+ self._relation_provided = False
88
+
89
+ def _lint_eof(self, token: Token) -> None:
90
+ """Lint the EOF Token.
91
+
92
+ Args:
93
+ token (Token): The EOF Token.
94
+
95
+ Raises:
96
+ LinterException: Equation must contain a relational
97
+ operator.
98
+ LinterException: Unexpected binary operator at the end of
99
+ the equation.
100
+ LinterException: Unexpected EOF, right side of the equation
101
+ is missed.
102
+ LinterException: Unexpected comma at the end of the
103
+ equation.
104
+ """
105
+ if token.prev_token.kind != TokenKind.SOF and not self._relation_provided:
106
+ raise LinterException(
107
+ self._source,
108
+ token.location,
109
+ "Equation must contain a relational operator",
110
+ )
111
+ if is_binary_operator(token.prev_token):
112
+ raise LinterException(
113
+ self._source,
114
+ token.prev_token.location,
115
+ "Unexpected binary operator at the end of the equation",
116
+ )
117
+ if token.prev_token.kind == TokenKind.MUL:
118
+ raise LinterException(
119
+ self._source,
120
+ token.location,
121
+ "Unexpected EOF, multiplier missed",
122
+ )
123
+ if is_relational_operator(token.prev_token):
124
+ raise LinterException(
125
+ self._source,
126
+ token.location,
127
+ "Unexpected EOF, right side of the equation is missed",
128
+ )
129
+ if token.prev_token.kind == TokenKind.COMMA:
130
+ raise LinterException(
131
+ self._source,
132
+ token.location,
133
+ "Unexpected comma at the end of the equation",
134
+ )
135
+
136
+ def _lint_binary_operator(self, token: Token) -> None:
137
+ """Lint the binary operator Token.
138
+
139
+ Args:
140
+ token (Token): The binary operator Token.
141
+
142
+ Raises:
143
+ LinterException: Unexpected binary operator, term missed.
144
+ """
145
+ if is_binary_operator(token.prev_token):
146
+ raise LinterException(
147
+ self._source,
148
+ token.location,
149
+ "Unexpected binary operator, term missed",
150
+ )
151
+ if token.prev_token.kind == TokenKind.MUL:
152
+ raise LinterException(
153
+ self._source,
154
+ token.location,
155
+ "Unexpected binary operator, multiplier missed",
156
+ )
157
+
158
+ def _lint_multiplication_operator(self, token: Token) -> None:
159
+ """Lint the multiplication operator Token.
160
+
161
+ Args:
162
+ token (Token): The multiplication operator Token.
163
+
164
+ Raises:
165
+ LinterException: Unexpected multiplication operator, term
166
+ missed.
167
+ """
168
+ if (
169
+ is_binary_operator(token.prev_token)
170
+ or is_relational_operator(token.prev_token)
171
+ or token.prev_token.kind == TokenKind.COMMA
172
+ ):
173
+ raise LinterException(
174
+ self._source,
175
+ token.location,
176
+ "Unexpected multiplication operator, term missed",
177
+ )
178
+ if token.prev_token.kind == TokenKind.MUL:
179
+ raise LinterException(
180
+ self._source,
181
+ token.location,
182
+ "Unexpected multiplication operator, multiplier missed",
183
+ )
184
+
185
+ def _lint_relational_operator(self, token: Token) -> None:
186
+ """Lint the relational operator Token.
187
+
188
+ Args:
189
+ token (Token): The relational operator Token.
190
+
191
+ Raises:
192
+ LinterException: Equation must contain only one relational
193
+ operator.
194
+ LinterException: Unexpected binary operator, term missed.
195
+ LinterException: Unexpected relational operator, left side
196
+ of the equation is missed.
197
+ """
198
+ if token.prev_token.kind in (TokenKind.SOF, TokenKind.COMMA):
199
+ raise LinterException(
200
+ self._source,
201
+ token.location,
202
+ "Unexpected relational operator, left side of the equation is missed",
203
+ )
204
+ if self._relation_provided:
205
+ raise LinterException(
206
+ self._source,
207
+ token.location,
208
+ "Equation must contain only one relational operator",
209
+ )
210
+ if is_binary_operator(token.prev_token):
211
+ raise LinterException(
212
+ self._source,
213
+ token.location,
214
+ "Unexpected binary operator, term missed",
215
+ )
216
+ if token.prev_token.kind == TokenKind.MUL:
217
+ raise LinterException(
218
+ self._source,
219
+ token.location,
220
+ "Unexpected relational operator, multiplier missed",
221
+ )
222
+
223
+ def _lint_variable(self, token: Token) -> None:
224
+ """Lint the variable Token.
225
+
226
+ Args:
227
+ token (Token): The variable Token.
228
+
229
+ Raises:
230
+ LinterException: Term must contain no more than one
231
+ variable.
232
+ """
233
+ if self._variable_provided:
234
+ raise LinterException(
235
+ self._source,
236
+ token.location,
237
+ "Term must contain no more than one variable",
238
+ )
239
+
240
+ def _lint_comma(self, token: Token) -> None:
241
+ """Lint the comma Token.
242
+
243
+ Args:
244
+ token (Token): The comma Token.
245
+
246
+ Raises:
247
+ LinterException: Unexpected comma at the beginning of the
248
+ equation.
249
+ LinterException: Unexpected comma, equation missed.
250
+ LinterException: Unexpected comma at the end of the
251
+ equation.
252
+ """
253
+ if token.prev_token.kind == TokenKind.SOF:
254
+ raise LinterException(
255
+ self._source,
256
+ token.location,
257
+ "Unexpected comma at the beginning of the equation",
258
+ )
259
+ if not self._relation_provided:
260
+ raise LinterException(
261
+ self._source,
262
+ token.location,
263
+ "Equation must contain a relational operator",
264
+ )
265
+ if is_binary_operator(token.prev_token):
266
+ raise LinterException(
267
+ self._source,
268
+ token.prev_token,
269
+ "Unexpected binary operator at the end of the equation",
270
+ )
271
+ if token.prev_token.kind == TokenKind.MUL:
272
+ raise LinterException(
273
+ self._source,
274
+ token.location,
275
+ "Unexpected comma, multiplier missed",
276
+ )
277
+ if is_relational_operator(token.prev_token):
278
+ raise LinterException(
279
+ self._source,
280
+ token.location,
281
+ "Unexpected comma, right side of the equation is missed",
282
+ )
283
+ if token.prev_token.kind == TokenKind.COMMA:
284
+ raise LinterException(
285
+ self._source,
286
+ token.location,
287
+ "Unexpected comma, equation missed",
288
+ )
289
+
290
+
291
+ __all__ = ("Linter",)
ast_parser/parser.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+
6
+ from ast_parser.lexer import Lexer
7
+ from ast_parser.linter import Linter
8
+ from ast_parser.token import Token, TokenKind
9
+
10
+
11
+ class EquationKind(str, Enum):
12
+ """Kind of relationship between the left and right parts of the
13
+ Equation."""
14
+
15
+ EQ = "="
16
+ """Equality operator."""
17
+ LEQ = "<="
18
+ """Less than or equal to operator."""
19
+ GEQ = ">="
20
+ """Greater than or equal to operator."""
21
+
22
+
23
+ @dataclass
24
+ class Equation:
25
+ """Equation definition."""
26
+
27
+ kind: EquationKind
28
+ """Kind of relationship between the left and right parts of the
29
+ Equation.
30
+ """
31
+ variables: dict[str, float]
32
+ """Equation variables. The names are the keys, the coefficients are
33
+ the values.
34
+ """
35
+ bound: float
36
+ """Bound of the Equation. Must be non-negative."""
37
+
38
+
39
+ @dataclass
40
+ class EquationAccumulator:
41
+ """The EquationAccumulator collects equation Tokens: variables,
42
+ coefficients, and bounds.
43
+ """
44
+
45
+ kind: EquationKind | None = field(default=None)
46
+ """Kind of relationship between the left and right parts of the
47
+ Equation.
48
+ """
49
+ variables: dict[str, float] = field(default_factory=dict)
50
+ """Equation variables. The names are the keys, the coefficients are
51
+ the values.
52
+ """
53
+ bound: float = field(default=0.0)
54
+ """Bound of the Equation. Must be non-negative."""
55
+
56
+ coefficient: float | None = field(default=None)
57
+ """Coefficient of the current variable."""
58
+ variable: str | None = field(default=None)
59
+ """Name of the current variable."""
60
+
61
+
62
+ class Parser:
63
+ """Source Parser. It parses the source and returns a list of
64
+ Equation objects found in the source.
65
+
66
+ Args:
67
+ source (str): The source to parse.
68
+ """
69
+
70
+ _lexer: Lexer
71
+ """Lexer of the source."""
72
+ _linter: Linter
73
+ """Linter of the source."""
74
+
75
+ _accumulator: EquationAccumulator
76
+ """Equation accumulator."""
77
+
78
+ def __init__(self, source: str) -> None:
79
+ self._lexer = Lexer(source)
80
+ self._linter = Linter(source)
81
+
82
+ self._accumulator = EquationAccumulator()
83
+
84
+ def __iter__(self) -> Parser:
85
+ """Gets an iterator over the Equations in the source.
86
+
87
+ Returns:
88
+ Parser: _description_
89
+ """
90
+ return self
91
+
92
+ def __next__(self) -> Equation:
93
+ """Gets the next Equation in the source.
94
+
95
+ Accumulation, processing and validation of Equations in
96
+ real-time. The source is parsed sequentially, token by token.
97
+
98
+ Raises:
99
+ StopIteration: The end of the source has been reached.
100
+ LexerException: Unexpected character, less than operator is
101
+ not allowed.
102
+ LexerException: Unexpected character, greater than operator
103
+ is not allowed.
104
+ LexerException: Invalid character: <code>.
105
+ LexerException: Invalid coefficient, unexpected digit after
106
+ 0: <code>.
107
+ LexerException: Invalid coefficient, expected digit but
108
+ got: <code>.
109
+ LinterException: Unexpected binary operator, term missed.
110
+ LinterException: Equation must contain only one relational
111
+ operator.
112
+ LinterException: Term must contain no more than one
113
+ variable.
114
+ LinterException: Unexpected comma at the beginning of the
115
+ equation.
116
+ LinterException: Unexpected comma, equation missed.
117
+ LinterException: Unexpected comma at the end of the
118
+ equation.
119
+ LinterException: Equation must contain a relational
120
+ operator.
121
+
122
+ Returns:
123
+ Equation: The next Equation from the source.
124
+ """
125
+ while True:
126
+ token = next(self._lexer)
127
+
128
+ self._linter.lint(token)
129
+
130
+ match token.kind:
131
+ case TokenKind.SOF | TokenKind.MUL:
132
+ continue
133
+ case TokenKind.EOF:
134
+ if token.prev_token.kind == TokenKind.SOF:
135
+ raise StopIteration
136
+
137
+ return self._derive_equation()
138
+ case TokenKind.ADD:
139
+ self._parse_addition_operator()
140
+
141
+ continue
142
+ case TokenKind.SUB:
143
+ self._parse_subtraction_operator()
144
+
145
+ continue
146
+ case TokenKind.EQ | TokenKind.LEQ | TokenKind.GEQ:
147
+ self._parse_relational_operator(token)
148
+
149
+ continue
150
+ case TokenKind.COEFFICIENT:
151
+ self._parse_coefficient(token)
152
+
153
+ continue
154
+ case TokenKind.VARIABLE:
155
+ self._parse_variable(token)
156
+
157
+ continue
158
+ case TokenKind.COMMA:
159
+ return self._derive_equation()
160
+
161
+ def _extend_variables(self) -> None:
162
+ """Extend the variables with the current variable and
163
+ coefficient.
164
+ """
165
+ if self._accumulator.coefficient:
166
+ if self._accumulator.kind:
167
+ self._accumulator.coefficient *= -1.0
168
+ if self._accumulator.variable:
169
+ if self._accumulator.variable in self._accumulator.variables:
170
+ self._accumulator.variables[
171
+ self._accumulator.variable
172
+ ] += self._accumulator.coefficient
173
+ else:
174
+ self._accumulator.variables[
175
+ self._accumulator.variable
176
+ ] = self._accumulator.coefficient
177
+ else:
178
+ self._accumulator.bound -= self._accumulator.coefficient
179
+
180
+ def _derive_equation(self) -> Equation:
181
+ """Derive the Equation from the EquationAccumulator.
182
+
183
+ Returns:
184
+ Equation: The derived Equation.
185
+ """
186
+ self._extend_variables()
187
+
188
+ equation = Equation(
189
+ self._accumulator.kind, self._accumulator.variables, self._accumulator.bound
190
+ )
191
+
192
+ self._accumulator = EquationAccumulator()
193
+
194
+ return equation
195
+
196
+ def _parse_addition_operator(self) -> None:
197
+ """Parse the addition operator Token."""
198
+ self._extend_variables()
199
+
200
+ self._accumulator.variable = None
201
+ self._accumulator.coefficient = 1.0
202
+
203
+ def _parse_subtraction_operator(self) -> None:
204
+ """Parse the subtraction operator Token."""
205
+ self._extend_variables()
206
+
207
+ self._accumulator.variable = None
208
+ self._accumulator.coefficient = -1.0
209
+
210
+ def _parse_relational_operator(self, token: Token) -> None:
211
+ """Parse the relational operator Token.
212
+
213
+ Args:
214
+ token (Token): The relational operator Token.
215
+ """
216
+ self._extend_variables()
217
+
218
+ self._accumulator.kind = EquationKind(token.kind.value)
219
+ self._accumulator.variable = None
220
+ self._accumulator.coefficient = None
221
+
222
+ def _parse_coefficient(self, token: Token) -> None:
223
+ """Parse the coefficient Token.
224
+
225
+ Args:
226
+ token (Token): The coefficient Token.
227
+ """
228
+ if not self._accumulator.coefficient:
229
+ self._accumulator.coefficient = 1.0
230
+
231
+ self._accumulator.coefficient *= float(token.value)
232
+
233
+ def _parse_variable(self, token: Token) -> None:
234
+ """Parse the variable Token.
235
+
236
+ Args:
237
+ token (Token): The variable Token.
238
+ """
239
+ if not self._accumulator.coefficient:
240
+ self._accumulator.coefficient = 1.0
241
+
242
+ self._accumulator.variable = token.value
243
+
244
+
245
+ __all__ = ("EquationKind", "Equation", "Parser")
ast_parser/token.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+
6
+
7
+ class TokenKind(str, Enum):
8
+ """Kind of the Token."""
9
+
10
+ SOF = "<SOF>"
11
+ """Start of file."""
12
+ EOF = "<EOF>"
13
+ """End of file."""
14
+
15
+ ADD = "+"
16
+ """Addition operator."""
17
+ SUB = "-"
18
+ """Subtraction operator."""
19
+ MUL = "*"
20
+ """Multiplication operator."""
21
+
22
+ EQ = "="
23
+ """Equality operator."""
24
+ LEQ = "<="
25
+ """Less than or equal to operator."""
26
+ GEQ = ">="
27
+ """Greater than or equal to operator."""
28
+
29
+ COEFFICIENT = "Coefficient"
30
+ """Coefficient of a variable."""
31
+ VARIABLE = "Variable"
32
+ """Variable name."""
33
+
34
+ COMMA = ","
35
+ """Comma."""
36
+
37
+
38
+ @dataclass
39
+ class Location:
40
+ """Location of the Token in the source.
41
+
42
+ The Location is more user-friendly than the start and end indices.
43
+ """
44
+
45
+ line: int
46
+ """Line number of the token in the source. Starts at 1."""
47
+ column: int
48
+ """Column number of the token in the source. Starts at 1."""
49
+
50
+
51
+ @dataclass
52
+ class Token:
53
+ """Token of the source code."""
54
+
55
+ kind: TokenKind
56
+ """Kind of the token."""
57
+
58
+ start: int = field(repr=False)
59
+ """The index of the first character of the token."""
60
+ end: int = field(repr=False)
61
+ """The index of the first character after the token."""
62
+ location: Location
63
+ """The Location of the token in the source."""
64
+
65
+ value: str
66
+ """The value of the token."""
67
+
68
+ prev_token: Token | None = field(repr=False, default=None)
69
+ """The previous Token in the source."""
70
+ next_token: Token | None = field(repr=False, default=None)
71
+ """The next Token in the source."""
72
+
73
+
74
+ def is_binary_operator(token: Token) -> bool:
75
+ """Check if the Token is an algebraic binary operator.
76
+
77
+ Args:
78
+ token (Token): The Token to check.
79
+
80
+ Returns:
81
+ bool: True if the Token is an algebraic binary operator, False
82
+ otherwise.
83
+ """
84
+ return token.kind in (TokenKind.ADD, TokenKind.SUB)
85
+
86
+
87
+ def is_relational_operator(token: Token) -> bool:
88
+ """Check if the Token is a relational operator.
89
+
90
+ Args:
91
+ token (Token): The Token to check.
92
+
93
+ Returns:
94
+ bool: True if the Token is a relational operator, False
95
+ otherwise.
96
+ """
97
+ return token.kind in (TokenKind.EQ, TokenKind.LEQ, TokenKind.GEQ)
98
+
99
+
100
+ __all__ = (
101
+ "TokenKind",
102
+ "Location",
103
+ "Token",
104
+ "is_binary_operator",
105
+ "is_relational_operator",
106
+ )
main.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ast_parser import Parser, EquationKind
2
+ from algorithm import solver
3
+
4
+ print("Enter all equations, following each with a comma. The last equation should be without a comma")
5
+ print("As an example 'Z = 5x_1 + 4x_2', '6x_1 + 4x_2 <= 24'")
6
+ input_equation = input()
7
+
8
+ parser = Parser(input_equation)
9
+
10
+ equations = tuple(parser)
11
+
12
+ for equation in equations:
13
+ if equation.kind == EquationKind.GEQ and equation.bound < 0.0:
14
+ for variable, coefficient in equation.variables.items():
15
+ equation.variables[variable] *= -1.0
16
+
17
+ equation.bound *= -1.0
18
+ equation.kind = EquationKind.LEQ
19
+
20
+ continue
21
+
22
+ if equation.kind == EquationKind.GEQ:
23
+ raise ValueError("Equation kind must be either EQ or LEQ")
24
+ if equation.bound < 0.0:
25
+ raise ValueError("Equation bound must be non-negative")
26
+
27
+ demand: dict[str, list[int]] = {}
28
+ for i, equation in enumerate(equations):
29
+ for variable, coefficient in equation.variables.items():
30
+ if coefficient == 0.0:
31
+ continue
32
+
33
+ if variable not in demand:
34
+ demand[variable] = []
35
+
36
+ demand[variable].append(i)
37
+
38
+ objective_variables = tuple(
39
+ filter(lambda variable: len(demand[variable]) == 1, demand.keys())
40
+ )
41
+
42
+ objective_functions = tuple(
43
+ filter(
44
+ lambda function: function.kind == EquationKind.EQ,
45
+ map(lambda variable: equations[demand[variable][0]], objective_variables),
46
+ )
47
+ )
48
+
49
+ if len(objective_variables) != len(objective_functions):
50
+ raise ValueError("Objective functions must be equalities")
51
+
52
+ constraints = tuple(
53
+ filter(
54
+ lambda function: function not in objective_functions,
55
+ equations,
56
+ )
57
+ )
58
+
59
+ solver.Solver(objective_functions, constraints)