optimumai 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- optimumai/__init__.py +31 -0
- optimumai/algebra/__init__.py +6 -0
- optimumai/algebra/matrix.py +94 -0
- optimumai/algebra/vector.py +158 -0
- optimumai/cli/__init__.py +0 -0
- optimumai/cli/main.py +150 -0
- optimumai/config.py +41 -0
- optimumai/core/__init__.py +7 -0
- optimumai/core/_fmt.py +40 -0
- optimumai/core/base_op.py +58 -0
- optimumai/core/explain.py +46 -0
- optimumai/core/trace.py +97 -0
- optimumai/probability/__init__.py +5 -0
- optimumai/probability/softmax.py +83 -0
- optimumai/transformers/__init__.py +5 -0
- optimumai/transformers/attention.py +117 -0
- optimumai/utils/__init__.py +0 -0
- optimumai/visualization/__init__.py +5 -0
- optimumai/visualization/terminal.py +88 -0
- optimumai-0.1.0.dist-info/METADATA +181 -0
- optimumai-0.1.0.dist-info/RECORD +24 -0
- optimumai-0.1.0.dist-info/WHEEL +4 -0
- optimumai-0.1.0.dist-info/entry_points.txt +2 -0
- optimumai-0.1.0.dist-info/licenses/LICENSE +21 -0
optimumai/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""OptimumAI — unlock the math behind AI.
|
|
2
|
+
|
|
3
|
+
Every operation, from a dot product to a full attention block, can be run with
|
|
4
|
+
``explain=True`` to produce a step-by-step computation trace, a terminal
|
|
5
|
+
visualization, and the context for *why* AI uses it.
|
|
6
|
+
|
|
7
|
+
>>> from optimumai import Vector
|
|
8
|
+
>>> Vector([1, 2, 3]).dot(Vector([4, 5, 6]), explain=True)
|
|
9
|
+
32.0
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from optimumai.algebra.matrix import Matrix
|
|
13
|
+
from optimumai.algebra.vector import Vector
|
|
14
|
+
from optimumai.core.explain import ExplainLevel
|
|
15
|
+
from optimumai.core.trace import Step, Trace
|
|
16
|
+
from optimumai.probability.softmax import softmax, softmax_trace
|
|
17
|
+
from optimumai.transformers.attention import Attention
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Attention",
|
|
23
|
+
"ExplainLevel",
|
|
24
|
+
"Matrix",
|
|
25
|
+
"Step",
|
|
26
|
+
"Trace",
|
|
27
|
+
"Vector",
|
|
28
|
+
"softmax",
|
|
29
|
+
"softmax_trace",
|
|
30
|
+
"__version__",
|
|
31
|
+
]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Matrices that narrate their own multiplication.
|
|
2
|
+
|
|
3
|
+
``y = W·x + b`` is every dense layer. ``Q·Kᵀ`` is the attention score matrix.
|
|
4
|
+
Watching a matrix multiply fill in cell by cell is the fastest way to make the
|
|
5
|
+
shape rules (``(m,k) @ (k,n) → (m,n)``) click.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Iterable, Sequence
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from optimumai.core._fmt import num
|
|
15
|
+
from optimumai.core.explain import ExplainLevel
|
|
16
|
+
from optimumai.core.trace import Trace
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Matrix:
|
|
20
|
+
"""A 2-D array with an explainable matrix product."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, data: Sequence[Sequence[float]] | np.ndarray):
|
|
23
|
+
self.data = np.asarray(data, dtype=float)
|
|
24
|
+
if self.data.ndim != 2:
|
|
25
|
+
raise ValueError(f"Matrix expects 2-D data, got shape {self.data.shape}")
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def shape(self) -> tuple[int, int]:
|
|
29
|
+
return (int(self.data.shape[0]), int(self.data.shape[1]))
|
|
30
|
+
|
|
31
|
+
def __repr__(self) -> str:
|
|
32
|
+
return f"Matrix(shape={self.shape})"
|
|
33
|
+
|
|
34
|
+
def __eq__(self, other: object) -> bool:
|
|
35
|
+
return isinstance(other, Matrix) and np.array_equal(self.data, other.data)
|
|
36
|
+
|
|
37
|
+
# ------------------------------------------------------------ matrix product
|
|
38
|
+
def matmul_trace(self, other: Matrix | Iterable) -> Trace:
|
|
39
|
+
"""Trace ``C = A @ B``, one output cell at a time."""
|
|
40
|
+
b = other if isinstance(other, Matrix) else Matrix(np.atleast_2d(other))
|
|
41
|
+
A, B = self.data, b.data
|
|
42
|
+
if A.shape[1] != B.shape[0]:
|
|
43
|
+
raise ValueError(
|
|
44
|
+
f"cannot multiply {A.shape} by {B.shape}: "
|
|
45
|
+
f"inner dimensions {A.shape[1]} and {B.shape[0]} must match"
|
|
46
|
+
)
|
|
47
|
+
m, k = A.shape
|
|
48
|
+
_, n = B.shape
|
|
49
|
+
t = Trace(
|
|
50
|
+
op="matmul",
|
|
51
|
+
formula="C[i,j] = Σₖ A[i,k]·B[k,j]",
|
|
52
|
+
complexity=f"O(m·k·n) = O({m}·{k}·{n})",
|
|
53
|
+
why_ai=[
|
|
54
|
+
"Every dense/linear layer: y = W·x + b",
|
|
55
|
+
"Projecting inputs into Q, K, V inside attention",
|
|
56
|
+
"Composing linear transformations across network layers",
|
|
57
|
+
],
|
|
58
|
+
meta={"a_shape": A.shape, "b_shape": B.shape},
|
|
59
|
+
)
|
|
60
|
+
C = np.zeros((m, n))
|
|
61
|
+
for i in range(m):
|
|
62
|
+
for j in range(n):
|
|
63
|
+
terms = [f"{num(A[i, p])}×{num(B[p, j])}" for p in range(k)]
|
|
64
|
+
val = float(np.dot(A[i, :], B[:, j]))
|
|
65
|
+
C[i, j] = val
|
|
66
|
+
t.add(
|
|
67
|
+
f"Cell C[{i},{j}]",
|
|
68
|
+
f"{' + '.join(terms)} = {num(val)}",
|
|
69
|
+
val,
|
|
70
|
+
detail=f"row {i} of A · column {j} of B",
|
|
71
|
+
)
|
|
72
|
+
t.result = C
|
|
73
|
+
return t
|
|
74
|
+
|
|
75
|
+
def matmul(
|
|
76
|
+
self,
|
|
77
|
+
other: Matrix | Iterable,
|
|
78
|
+
explain: bool = False,
|
|
79
|
+
level: str | ExplainLevel = ExplainLevel.INTERMEDIATE,
|
|
80
|
+
) -> np.ndarray:
|
|
81
|
+
"""Matrix product. Set ``explain=True`` to print the cell-by-cell trace."""
|
|
82
|
+
t = self.matmul_trace(other)
|
|
83
|
+
return t.render(level) if explain else t.result
|
|
84
|
+
|
|
85
|
+
def __matmul__(self, other: Matrix) -> np.ndarray:
|
|
86
|
+
return self.matmul(other)
|
|
87
|
+
|
|
88
|
+
# ------------------------------------------------------------------ transpose
|
|
89
|
+
def transpose(self) -> Matrix:
|
|
90
|
+
return Matrix(self.data.T)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def T(self) -> Matrix: # noqa: N802 - mirror numpy's attribute name
|
|
94
|
+
return self.transpose()
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Vectors that can explain themselves.
|
|
2
|
+
|
|
3
|
+
The dot product is the atom of modern AI: it is the similarity between two
|
|
4
|
+
embeddings, the raw attention score ``q · k``, and the inner loop of every
|
|
5
|
+
matrix multiply. Teaching it well pays off everywhere downstream.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Iterable
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from optimumai.core._fmt import num
|
|
15
|
+
from optimumai.core.explain import ExplainLevel
|
|
16
|
+
from optimumai.core.trace import Trace
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Vector:
|
|
20
|
+
"""A 1-D array with step-by-step explanations of its operations."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, data: Iterable[float]):
|
|
23
|
+
raw = data if isinstance(data, np.ndarray) else list(data)
|
|
24
|
+
self.data = np.asarray(raw, dtype=float)
|
|
25
|
+
if self.data.ndim != 1:
|
|
26
|
+
raise ValueError(f"Vector expects 1-D data, got shape {self.data.shape}")
|
|
27
|
+
|
|
28
|
+
# ------------------------------------------------------------------ repr
|
|
29
|
+
def __len__(self) -> int:
|
|
30
|
+
return int(self.data.shape[0])
|
|
31
|
+
|
|
32
|
+
def __repr__(self) -> str:
|
|
33
|
+
return f"Vector({self.data.tolist()})"
|
|
34
|
+
|
|
35
|
+
def __eq__(self, other: object) -> bool:
|
|
36
|
+
return isinstance(other, Vector) and np.array_equal(self.data, other.data)
|
|
37
|
+
|
|
38
|
+
# ------------------------------------------------------------- dot product
|
|
39
|
+
def dot_trace(self, other: Vector) -> Trace:
|
|
40
|
+
"""Build the full trace of ``self · other``."""
|
|
41
|
+
a, b = self.data, self._as_vector(other).data
|
|
42
|
+
if a.shape != b.shape:
|
|
43
|
+
raise ValueError(
|
|
44
|
+
f"dot product needs equal-length vectors, got {a.shape} and {b.shape}"
|
|
45
|
+
)
|
|
46
|
+
t = Trace(
|
|
47
|
+
op="dot",
|
|
48
|
+
formula="a · b = Σᵢ aᵢ·bᵢ",
|
|
49
|
+
complexity="O(n)",
|
|
50
|
+
why_ai=[
|
|
51
|
+
"Similarity between two embedding vectors",
|
|
52
|
+
"The raw attention score q · k before scaling",
|
|
53
|
+
"The inner loop of every matrix multiply and linear layer",
|
|
54
|
+
],
|
|
55
|
+
)
|
|
56
|
+
products = []
|
|
57
|
+
for i, (x, y) in enumerate(zip(a, b, strict=True)):
|
|
58
|
+
p = float(x * y)
|
|
59
|
+
products.append(p)
|
|
60
|
+
t.add(f"Multiply component {i}", f"{num(x)} × {num(y)} = {num(p)}", p)
|
|
61
|
+
total = float(np.sum(products))
|
|
62
|
+
summation = " + ".join(num(p) for p in products) if products else "0"
|
|
63
|
+
t.add("Sum the products", f"{summation} = {num(total)}", total)
|
|
64
|
+
t.result = total
|
|
65
|
+
return t
|
|
66
|
+
|
|
67
|
+
def dot(
|
|
68
|
+
self,
|
|
69
|
+
other: Vector,
|
|
70
|
+
explain: bool = False,
|
|
71
|
+
level: str | ExplainLevel = ExplainLevel.INTERMEDIATE,
|
|
72
|
+
) -> float:
|
|
73
|
+
"""Inner product. Set ``explain=True`` to print the full trace."""
|
|
74
|
+
t = self.dot_trace(other)
|
|
75
|
+
return t.render(level) if explain else t.result
|
|
76
|
+
|
|
77
|
+
# -------------------------------------------------------------------- norm
|
|
78
|
+
def norm_trace(self) -> Trace:
|
|
79
|
+
"""Euclidean (L2) norm, ``||a|| = √Σ aᵢ²``."""
|
|
80
|
+
a = self.data
|
|
81
|
+
t = Trace(
|
|
82
|
+
op="norm",
|
|
83
|
+
formula="||a|| = √(Σᵢ aᵢ²)",
|
|
84
|
+
complexity="O(n)",
|
|
85
|
+
why_ai=[
|
|
86
|
+
"The length of an embedding vector",
|
|
87
|
+
"Denominator of cosine similarity",
|
|
88
|
+
"Weight/gradient magnitudes for clipping and regularization",
|
|
89
|
+
],
|
|
90
|
+
)
|
|
91
|
+
squares = [float(x * x) for x in a]
|
|
92
|
+
for i, (x, sq) in enumerate(zip(a, squares, strict=True)):
|
|
93
|
+
t.add(f"Square component {i}", f"{num(x)}² = {num(sq)}", sq)
|
|
94
|
+
ssum = float(np.sum(squares))
|
|
95
|
+
t.add("Sum of squares", f"{' + '.join(num(s) for s in squares) or '0'} = {num(ssum)}", ssum)
|
|
96
|
+
result = float(np.sqrt(ssum))
|
|
97
|
+
t.add("Square root", f"√{num(ssum)} = {num(result)}", result)
|
|
98
|
+
t.result = result
|
|
99
|
+
return t
|
|
100
|
+
|
|
101
|
+
def norm(
|
|
102
|
+
self,
|
|
103
|
+
explain: bool = False,
|
|
104
|
+
level: str | ExplainLevel = ExplainLevel.INTERMEDIATE,
|
|
105
|
+
) -> float:
|
|
106
|
+
t = self.norm_trace()
|
|
107
|
+
return t.render(level) if explain else t.result
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------- cosine similarity
|
|
110
|
+
def cosine_similarity_trace(self, other: Vector) -> Trace:
|
|
111
|
+
"""Cosine similarity ``(a · b) / (||a|| ||b||)`` — the RAG workhorse."""
|
|
112
|
+
b = self._as_vector(other)
|
|
113
|
+
if self.data.shape != b.data.shape:
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"cosine similarity needs equal-length vectors, "
|
|
116
|
+
f"got {self.data.shape} and {b.data.shape}"
|
|
117
|
+
)
|
|
118
|
+
t = Trace(
|
|
119
|
+
op="cosine_similarity",
|
|
120
|
+
formula="cos(θ) = (a · b) / (||a|| · ||b||)",
|
|
121
|
+
complexity="O(n)",
|
|
122
|
+
why_ai=[
|
|
123
|
+
"Ranking documents against a query in RAG / semantic search",
|
|
124
|
+
"Measuring how aligned two embeddings are, ignoring magnitude",
|
|
125
|
+
"Nearest-neighbour lookup in a vector database",
|
|
126
|
+
],
|
|
127
|
+
)
|
|
128
|
+
dot = float(np.dot(self.data, b.data))
|
|
129
|
+
na = float(np.linalg.norm(self.data))
|
|
130
|
+
nb = float(np.linalg.norm(b.data))
|
|
131
|
+
t.add("Dot product a · b", f"a · b = {num(dot)}", dot)
|
|
132
|
+
t.add("Norm of a", f"||a|| = {num(na)}", na)
|
|
133
|
+
t.add("Norm of b", f"||b|| = {num(nb)}", nb)
|
|
134
|
+
denom = na * nb
|
|
135
|
+
if denom == 0.0:
|
|
136
|
+
raise ValueError("cosine similarity is undefined for a zero vector")
|
|
137
|
+
result = dot / denom
|
|
138
|
+
t.add(
|
|
139
|
+
"Divide",
|
|
140
|
+
f"{num(dot)} / ({num(na)} × {num(nb)}) = {num(result)}",
|
|
141
|
+
result,
|
|
142
|
+
)
|
|
143
|
+
t.result = result
|
|
144
|
+
return t
|
|
145
|
+
|
|
146
|
+
def cosine_similarity(
|
|
147
|
+
self,
|
|
148
|
+
other: Vector,
|
|
149
|
+
explain: bool = False,
|
|
150
|
+
level: str | ExplainLevel = ExplainLevel.INTERMEDIATE,
|
|
151
|
+
) -> float:
|
|
152
|
+
t = self.cosine_similarity_trace(other)
|
|
153
|
+
return t.render(level) if explain else t.result
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------- internals
|
|
156
|
+
@staticmethod
|
|
157
|
+
def _as_vector(other: Vector | Iterable[float]) -> Vector:
|
|
158
|
+
return other if isinstance(other, Vector) else Vector(other)
|
|
File without changes
|
optimumai/cli/main.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""The ``optimumai`` command-line interface.
|
|
2
|
+
|
|
3
|
+
Examples:
|
|
4
|
+
optimumai algebra dot "[1,2,3]" "[4,5,6]"
|
|
5
|
+
optimumai algebra matmul "[[1,2],[3,4]]" "[[5,6],[7,8]]"
|
|
6
|
+
optimumai softmax "[2,1,0.1]" --temperature 0.5
|
|
7
|
+
optimumai attention --demo
|
|
8
|
+
optimumai learn
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import ast
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
|
|
17
|
+
from optimumai import __version__
|
|
18
|
+
from optimumai.algebra.matrix import Matrix
|
|
19
|
+
from optimumai.algebra.vector import Vector
|
|
20
|
+
from optimumai.core.explain import ExplainLevel
|
|
21
|
+
from optimumai.probability.softmax import softmax_trace
|
|
22
|
+
from optimumai.transformers.attention import Attention
|
|
23
|
+
|
|
24
|
+
_LEVEL_CHOICE = click.Choice([lvl.value for lvl in ExplainLevel], case_sensitive=False)
|
|
25
|
+
|
|
26
|
+
# topic -> (one-line description, zero-arg callable returning a Trace)
|
|
27
|
+
_TOPICS: dict[str, tuple[str, callable]] = {
|
|
28
|
+
"dot": (
|
|
29
|
+
"Dot product — similarity and the atom of matrix multiply",
|
|
30
|
+
lambda: Vector([1, 2, 3]).dot_trace(Vector([4, 5, 6])),
|
|
31
|
+
),
|
|
32
|
+
"cosine": (
|
|
33
|
+
"Cosine similarity — the RAG / semantic-search ranking score",
|
|
34
|
+
lambda: Vector([1, 2, 3]).cosine_similarity_trace(Vector([2, 4, 6])),
|
|
35
|
+
),
|
|
36
|
+
"matmul": (
|
|
37
|
+
"Matrix multiplication — every dense layer, cell by cell",
|
|
38
|
+
lambda: Matrix([[1, 2], [3, 4]]).matmul_trace(Matrix([[5, 6], [7, 8]])),
|
|
39
|
+
),
|
|
40
|
+
"softmax": (
|
|
41
|
+
"Softmax — logits into a probability distribution",
|
|
42
|
+
lambda: softmax_trace([2.0, 1.0, 0.1]),
|
|
43
|
+
),
|
|
44
|
+
"attention": (
|
|
45
|
+
"Scaled dot-product attention — the transformer core",
|
|
46
|
+
Attention.demo,
|
|
47
|
+
),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _parse_literal(text: str, what: str):
|
|
52
|
+
try:
|
|
53
|
+
return ast.literal_eval(text)
|
|
54
|
+
except (ValueError, SyntaxError) as exc:
|
|
55
|
+
raise click.BadParameter(
|
|
56
|
+
f"could not parse {what}: {text!r} (expected e.g. \"[1, 2, 3]\")"
|
|
57
|
+
) from exc
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
|
|
61
|
+
@click.version_option(__version__, prog_name="optimumai")
|
|
62
|
+
def cli() -> None:
|
|
63
|
+
"""OptimumAI — unlock the math behind AI, one explained step at a time."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# --------------------------------------------------------------------- algebra
|
|
67
|
+
@cli.group()
|
|
68
|
+
def algebra() -> None:
|
|
69
|
+
"""Vector and matrix operations."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@algebra.command("dot")
|
|
73
|
+
@click.argument("a")
|
|
74
|
+
@click.argument("b")
|
|
75
|
+
@click.option("--level", type=_LEVEL_CHOICE, default="intermediate", help="Detail level.")
|
|
76
|
+
def algebra_dot(a: str, b: str, level: str) -> None:
|
|
77
|
+
"""Dot product of two vectors, e.g. optimumai algebra dot "[1,2,3]" "[4,5,6]"."""
|
|
78
|
+
va = Vector(_parse_literal(a, "vector A"))
|
|
79
|
+
vb = Vector(_parse_literal(b, "vector B"))
|
|
80
|
+
va.dot(vb, explain=True, level=level)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@algebra.command("cosine")
|
|
84
|
+
@click.argument("a")
|
|
85
|
+
@click.argument("b")
|
|
86
|
+
@click.option("--level", type=_LEVEL_CHOICE, default="intermediate", help="Detail level.")
|
|
87
|
+
def algebra_cosine(a: str, b: str, level: str) -> None:
|
|
88
|
+
"""Cosine similarity of two vectors."""
|
|
89
|
+
va = Vector(_parse_literal(a, "vector A"))
|
|
90
|
+
vb = Vector(_parse_literal(b, "vector B"))
|
|
91
|
+
va.cosine_similarity(vb, explain=True, level=level)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@algebra.command("matmul")
|
|
95
|
+
@click.argument("a")
|
|
96
|
+
@click.argument("b")
|
|
97
|
+
@click.option("--level", type=_LEVEL_CHOICE, default="intermediate", help="Detail level.")
|
|
98
|
+
def algebra_matmul(a: str, b: str, level: str) -> None:
|
|
99
|
+
"""Matrix product, e.g. optimumai algebra matmul "[[1,2],[3,4]]" "[[5,6],[7,8]]"."""
|
|
100
|
+
ma = Matrix(_parse_literal(a, "matrix A"))
|
|
101
|
+
mb = Matrix(_parse_literal(b, "matrix B"))
|
|
102
|
+
ma.matmul(mb, explain=True, level=level)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# --------------------------------------------------------------------- softmax
|
|
106
|
+
@cli.command("softmax")
|
|
107
|
+
@click.argument("logits")
|
|
108
|
+
@click.option("--temperature", "-t", type=float, default=1.0, help="Sampling temperature (>0).")
|
|
109
|
+
@click.option("--level", type=_LEVEL_CHOICE, default="intermediate", help="Detail level.")
|
|
110
|
+
def softmax_cmd(logits: str, temperature: float, level: str) -> None:
|
|
111
|
+
"""Softmax of a logit vector, e.g. optimumai softmax "[2,1,0.1]"."""
|
|
112
|
+
values = _parse_literal(logits, "logits")
|
|
113
|
+
softmax_trace(values, temperature=temperature).render(level)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ------------------------------------------------------------------- attention
|
|
117
|
+
@cli.command("attention")
|
|
118
|
+
@click.option("--demo", "use_demo", is_flag=True, help="Run a built-in 3-token example.")
|
|
119
|
+
@click.option("--seed", type=int, default=0, help="Random seed for --demo.")
|
|
120
|
+
@click.option("--level", type=_LEVEL_CHOICE, default="engineer", help="Detail level.")
|
|
121
|
+
def attention_cmd(use_demo: bool, seed: int, level: str) -> None:
|
|
122
|
+
"""Scaled dot-product attention (currently via the built-in demo)."""
|
|
123
|
+
if not use_demo:
|
|
124
|
+
raise click.UsageError("pass --demo to run the built-in attention example")
|
|
125
|
+
Attention.demo(seed=seed).render(level)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ----------------------------------------------------------------------- learn
|
|
129
|
+
@cli.command("learn")
|
|
130
|
+
@click.argument("topic", required=False)
|
|
131
|
+
@click.option("--level", type=_LEVEL_CHOICE, default="intermediate", help="Detail level.")
|
|
132
|
+
def learn_cmd(topic: str | None, level: str) -> None:
|
|
133
|
+
"""Walk through a topic's math. Run without a topic to list them all."""
|
|
134
|
+
if topic is None:
|
|
135
|
+
click.echo("Available topics:\n")
|
|
136
|
+
for name, (desc, _) in _TOPICS.items():
|
|
137
|
+
click.echo(f" {name:<12} {desc}")
|
|
138
|
+
click.echo('\nTry: optimumai learn attention --level engineer')
|
|
139
|
+
return
|
|
140
|
+
key = topic.lower()
|
|
141
|
+
if key not in _TOPICS:
|
|
142
|
+
raise click.BadParameter(
|
|
143
|
+
f"unknown topic {topic!r}. Choose from: {', '.join(_TOPICS)}"
|
|
144
|
+
)
|
|
145
|
+
_, build = _TOPICS[key]
|
|
146
|
+
build().render(level)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__": # pragma: no cover
|
|
150
|
+
cli()
|
optimumai/config.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Runtime configuration via environment variables (``OPTIMUMAI_*``).
|
|
2
|
+
|
|
3
|
+
Follows the workspace convention of ``pydantic-settings`` + ``.env``. Nothing
|
|
4
|
+
here is required for the core math — every field has a sensible default and the
|
|
5
|
+
LLM fields are only consulted by the (optional) tutor layer.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from functools import lru_cache
|
|
11
|
+
|
|
12
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
13
|
+
|
|
14
|
+
from optimumai.core.explain import ExplainLevel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Settings(BaseSettings):
|
|
18
|
+
"""Central settings object, populated from the environment or a ``.env`` file."""
|
|
19
|
+
|
|
20
|
+
model_config = SettingsConfigDict(
|
|
21
|
+
env_prefix="OPTIMUMAI_",
|
|
22
|
+
env_file=".env",
|
|
23
|
+
env_file_encoding="utf-8",
|
|
24
|
+
extra="ignore",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Presentation defaults
|
|
28
|
+
explain_level: ExplainLevel = ExplainLevel.INTERMEDIATE
|
|
29
|
+
color: bool = True
|
|
30
|
+
|
|
31
|
+
# LLM tutor (optional; used only when the `[llm]` extra is installed)
|
|
32
|
+
llm_model: str = "gpt-4o-mini"
|
|
33
|
+
api_key: str | None = None
|
|
34
|
+
api_base: str | None = None
|
|
35
|
+
temperature: float = 0.3
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@lru_cache
|
|
39
|
+
def get_settings() -> Settings:
|
|
40
|
+
"""Return a cached :class:`Settings` instance."""
|
|
41
|
+
return Settings()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Core engine: explain levels, the trace model, and the base op contract."""
|
|
2
|
+
|
|
3
|
+
from optimumai.core.base_op import BaseOp
|
|
4
|
+
from optimumai.core.explain import ExplainLevel
|
|
5
|
+
from optimumai.core.trace import Step, Trace
|
|
6
|
+
|
|
7
|
+
__all__ = ["BaseOp", "ExplainLevel", "Step", "Trace"]
|
optimumai/core/_fmt.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Small, dependency-light number/array formatting helpers.
|
|
2
|
+
|
|
3
|
+
Kept in ``core`` so both the math ops (which build human-readable expression
|
|
4
|
+
strings) and the visualization layer can share one formatting convention
|
|
5
|
+
without importing each other.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def num(x: Any, precision: int = 4) -> str:
|
|
16
|
+
"""Format a scalar compactly: integers stay integers, floats get trimmed."""
|
|
17
|
+
val = float(x)
|
|
18
|
+
if val == int(val) and abs(val) < 1e15:
|
|
19
|
+
return str(int(val))
|
|
20
|
+
return f"{val:.{precision}g}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def arr(x: Any, precision: int = 4) -> str:
|
|
24
|
+
"""Format a scalar, vector, or matrix as a compact string."""
|
|
25
|
+
a = np.asarray(x, dtype=float)
|
|
26
|
+
if a.ndim == 0:
|
|
27
|
+
return num(float(a), precision)
|
|
28
|
+
return np.array2string(
|
|
29
|
+
a,
|
|
30
|
+
precision=precision,
|
|
31
|
+
suppress_small=True,
|
|
32
|
+
separator=", ",
|
|
33
|
+
floatmode="maxprec",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def shape_of(x: Any) -> str:
|
|
38
|
+
"""Return a short shape label like ``(2, 3)`` or ``scalar``."""
|
|
39
|
+
a = np.asarray(x)
|
|
40
|
+
return "scalar" if a.ndim == 0 else str(tuple(a.shape))
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Base class for composable, explainable operations.
|
|
2
|
+
|
|
3
|
+
Most simple ops live as methods on :class:`~optimumai.algebra.vector.Vector`
|
|
4
|
+
etc. and just return a :class:`~optimumai.core.trace.Trace`. Multi-stage ops
|
|
5
|
+
(attention, a transformer block, an optimizer step) are cleaner as objects that
|
|
6
|
+
carry configuration, so they subclass :class:`BaseOp`.
|
|
7
|
+
|
|
8
|
+
The contract is deliberately tiny: implement :meth:`trace` to build a
|
|
9
|
+
:class:`Trace`, and callers get :meth:`run` (fast path) and calling the object
|
|
10
|
+
directly (``op(x, explain=True)``) for free.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from abc import ABC, abstractmethod
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from optimumai.core.explain import ExplainLevel
|
|
19
|
+
from optimumai.core.trace import Trace
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BaseOp(ABC):
|
|
23
|
+
"""An operation that can compute quietly or explain itself step by step."""
|
|
24
|
+
|
|
25
|
+
name: str = "op"
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def trace(self, *args: Any, **kwargs: Any) -> Trace:
|
|
29
|
+
"""Build and return a full :class:`Trace` for the given inputs."""
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
def run(self, *args: Any, **kwargs: Any) -> Any:
|
|
33
|
+
"""Compute and return just the result (no rendering)."""
|
|
34
|
+
return self.trace(*args, **kwargs).result
|
|
35
|
+
|
|
36
|
+
def explain(
|
|
37
|
+
self,
|
|
38
|
+
*args: Any,
|
|
39
|
+
level: str | ExplainLevel = ExplainLevel.INTERMEDIATE,
|
|
40
|
+
console: Any = None,
|
|
41
|
+
**kwargs: Any,
|
|
42
|
+
) -> Any:
|
|
43
|
+
"""Compute, render the trace, and return the result."""
|
|
44
|
+
return self.trace(*args, **kwargs).render(level=level, console=console)
|
|
45
|
+
|
|
46
|
+
def __call__(
|
|
47
|
+
self,
|
|
48
|
+
*args: Any,
|
|
49
|
+
explain: bool = False,
|
|
50
|
+
level: str | ExplainLevel = ExplainLevel.INTERMEDIATE,
|
|
51
|
+
console: Any = None,
|
|
52
|
+
**kwargs: Any,
|
|
53
|
+
) -> Any:
|
|
54
|
+
"""``op(x)`` computes; ``op(x, explain=True)`` also prints the trace."""
|
|
55
|
+
t = self.trace(*args, **kwargs)
|
|
56
|
+
if explain:
|
|
57
|
+
return t.render(level=level, console=console)
|
|
58
|
+
return t.result
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Explanation verbosity levels.
|
|
2
|
+
|
|
3
|
+
The same computation can be explained for four audiences. Higher levels do not
|
|
4
|
+
*change* the math — they reveal more of it (formulas, complexity, edge cases).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ExplainLevel(str, Enum):
|
|
13
|
+
"""How much detail a rendered trace should surface."""
|
|
14
|
+
|
|
15
|
+
BEGINNER = "beginner"
|
|
16
|
+
INTERMEDIATE = "intermediate"
|
|
17
|
+
ENGINEER = "engineer"
|
|
18
|
+
RESEARCHER = "researcher"
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def parse(cls, value: str | ExplainLevel) -> ExplainLevel:
|
|
22
|
+
"""Coerce a string (case-insensitive) or enum into an ``ExplainLevel``."""
|
|
23
|
+
if isinstance(value, cls):
|
|
24
|
+
return value
|
|
25
|
+
try:
|
|
26
|
+
return cls(str(value).strip().lower())
|
|
27
|
+
except ValueError as exc: # pragma: no cover - defensive
|
|
28
|
+
valid = ", ".join(level.value for level in cls)
|
|
29
|
+
raise ValueError(
|
|
30
|
+
f"Unknown explain level {value!r}. Choose one of: {valid}."
|
|
31
|
+
) from exc
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def rank(self) -> int:
|
|
35
|
+
"""Ordinal used to gate optional sections (higher shows more)."""
|
|
36
|
+
order = [
|
|
37
|
+
ExplainLevel.BEGINNER,
|
|
38
|
+
ExplainLevel.INTERMEDIATE,
|
|
39
|
+
ExplainLevel.ENGINEER,
|
|
40
|
+
ExplainLevel.RESEARCHER,
|
|
41
|
+
]
|
|
42
|
+
return order.index(self)
|
|
43
|
+
|
|
44
|
+
def at_least(self, other: ExplainLevel) -> bool:
|
|
45
|
+
"""True when this level is as detailed as ``other`` (or more)."""
|
|
46
|
+
return self.rank >= other.rank
|
optimumai/core/trace.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""The computation trace — the beating heart of OptimumAI.
|
|
2
|
+
|
|
3
|
+
Every operation, whether a 3-element dot product or a full attention block,
|
|
4
|
+
produces a :class:`Trace`: an ordered list of :class:`Step` objects plus the
|
|
5
|
+
final result and the "why AI cares" context. A trace can be rendered to a
|
|
6
|
+
terminal, inspected programmatically, or asserted against in tests.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from optimumai.core.explain import ExplainLevel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Step:
|
|
19
|
+
"""One intermediate move in a computation.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
index: 1-based position in the trace.
|
|
23
|
+
title: Short label, e.g. ``"Multiply component 0"``.
|
|
24
|
+
expression: Human-readable computation, e.g. ``"1 × 4 = 4"``.
|
|
25
|
+
value: The numeric result of this step (scalar / vector / matrix).
|
|
26
|
+
detail: Optional longer note shown at higher explain levels.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
index: int
|
|
30
|
+
title: str
|
|
31
|
+
expression: str
|
|
32
|
+
value: Any = None
|
|
33
|
+
detail: str = ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Trace:
|
|
38
|
+
"""A complete, replayable record of an operation.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
op: Name of the operation, e.g. ``"dot"`` or ``"attention"``.
|
|
42
|
+
result: The final output value.
|
|
43
|
+
steps: Ordered intermediate steps.
|
|
44
|
+
formula: The closed-form formula, e.g. ``"a · b = Σ aᵢbᵢ"``.
|
|
45
|
+
why_ai: Bullet points on where this shows up in real AI systems.
|
|
46
|
+
complexity: Big-O note, surfaced at engineer/researcher levels.
|
|
47
|
+
meta: Free-form extra data (shapes, hyper-params, ...).
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
op: str
|
|
51
|
+
result: Any = None
|
|
52
|
+
steps: list[Step] = field(default_factory=list)
|
|
53
|
+
formula: str = ""
|
|
54
|
+
why_ai: list[str] = field(default_factory=list)
|
|
55
|
+
complexity: str = ""
|
|
56
|
+
meta: dict[str, Any] = field(default_factory=dict)
|
|
57
|
+
|
|
58
|
+
def add(
|
|
59
|
+
self, title: str, expression: str, value: Any = None, detail: str = ""
|
|
60
|
+
) -> Trace:
|
|
61
|
+
"""Append a step and return ``self`` for fluent chaining."""
|
|
62
|
+
self.steps.append(
|
|
63
|
+
Step(
|
|
64
|
+
index=len(self.steps) + 1,
|
|
65
|
+
title=title,
|
|
66
|
+
expression=expression,
|
|
67
|
+
value=value,
|
|
68
|
+
detail=detail,
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def render(
|
|
74
|
+
self,
|
|
75
|
+
level: str | ExplainLevel = ExplainLevel.INTERMEDIATE,
|
|
76
|
+
console: Any = None,
|
|
77
|
+
) -> Any:
|
|
78
|
+
"""Pretty-print this trace to the terminal and return the result.
|
|
79
|
+
|
|
80
|
+
Imported lazily so the math layer never hard-depends on the UI layer.
|
|
81
|
+
"""
|
|
82
|
+
from optimumai.visualization.terminal import render_trace
|
|
83
|
+
|
|
84
|
+
render_trace(self, level=ExplainLevel.parse(level), console=console)
|
|
85
|
+
return self.result
|
|
86
|
+
|
|
87
|
+
def last(self) -> Step:
|
|
88
|
+
"""Return the final recorded step (raises if the trace is empty)."""
|
|
89
|
+
if not self.steps:
|
|
90
|
+
raise IndexError("trace has no steps")
|
|
91
|
+
return self.steps[-1]
|
|
92
|
+
|
|
93
|
+
def __len__(self) -> int:
|
|
94
|
+
return len(self.steps)
|
|
95
|
+
|
|
96
|
+
def __iter__(self):
|
|
97
|
+
return iter(self.steps)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Softmax — the function that turns raw scores into a distribution.
|
|
2
|
+
|
|
3
|
+
It is the last layer of a classifier, the thing that makes attention weights sum
|
|
4
|
+
to one, and the knob (via temperature) behind "creative" vs "focused" sampling.
|
|
5
|
+
This implementation uses the standard max-subtraction trick for numerical
|
|
6
|
+
stability and shows *why* that trick is safe.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Iterable
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from optimumai.core._fmt import arr, num
|
|
16
|
+
from optimumai.core.explain import ExplainLevel
|
|
17
|
+
from optimumai.core.trace import Trace
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def softmax_trace(x: Iterable[float], temperature: float = 1.0) -> Trace:
|
|
21
|
+
"""Build the full trace of ``softmax(x)`` with an optional temperature."""
|
|
22
|
+
vec = np.asarray(list(x), dtype=float)
|
|
23
|
+
if vec.ndim != 1:
|
|
24
|
+
raise ValueError(f"softmax_trace expects a 1-D input, got shape {vec.shape}")
|
|
25
|
+
if temperature <= 0:
|
|
26
|
+
raise ValueError(f"temperature must be > 0, got {temperature}")
|
|
27
|
+
|
|
28
|
+
t = Trace(
|
|
29
|
+
op="softmax",
|
|
30
|
+
formula="softmax(xᵢ) = e^(xᵢ) / Σⱼ e^(xⱼ)",
|
|
31
|
+
complexity="O(n)",
|
|
32
|
+
why_ai=[
|
|
33
|
+
"Converts logits into a probability distribution that sums to 1",
|
|
34
|
+
"Produces attention weights from scaled QKᵀ scores",
|
|
35
|
+
"The final layer for classification and next-token prediction",
|
|
36
|
+
],
|
|
37
|
+
meta={"temperature": temperature},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if temperature != 1.0:
|
|
41
|
+
vec = vec / temperature
|
|
42
|
+
t.add(
|
|
43
|
+
"Apply temperature",
|
|
44
|
+
f"xᵢ / T with T = {num(temperature)} → {arr(vec)}",
|
|
45
|
+
vec.copy(),
|
|
46
|
+
detail="T < 1 sharpens the distribution; T > 1 flattens it.",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
x_max = float(np.max(vec))
|
|
50
|
+
shifted = vec - x_max
|
|
51
|
+
t.add(
|
|
52
|
+
"Subtract the max (stability)",
|
|
53
|
+
f"xᵢ − {num(x_max)} → {arr(shifted)}",
|
|
54
|
+
shifted,
|
|
55
|
+
detail="Shifting by a constant leaves softmax unchanged but avoids overflow in e^x.",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
exps = np.exp(shifted)
|
|
59
|
+
t.add("Exponentiate", f"e^(xᵢ − max) → {arr(exps)}", exps)
|
|
60
|
+
|
|
61
|
+
denom = float(np.sum(exps))
|
|
62
|
+
t.add("Sum the exponentials", f"Σ = {num(denom)}", denom)
|
|
63
|
+
|
|
64
|
+
probs = exps / denom
|
|
65
|
+
t.add(
|
|
66
|
+
"Normalize",
|
|
67
|
+
f"e^(xᵢ) / Σ → {arr(probs)}",
|
|
68
|
+
probs,
|
|
69
|
+
detail=f"Check: the outputs sum to {num(float(np.sum(probs)))}.",
|
|
70
|
+
)
|
|
71
|
+
t.result = probs
|
|
72
|
+
return t
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def softmax(
|
|
76
|
+
x: Iterable[float],
|
|
77
|
+
temperature: float = 1.0,
|
|
78
|
+
explain: bool = False,
|
|
79
|
+
level: str | ExplainLevel = ExplainLevel.INTERMEDIATE,
|
|
80
|
+
) -> np.ndarray:
|
|
81
|
+
"""Return the softmax of ``x``. Set ``explain=True`` to print the trace."""
|
|
82
|
+
t = softmax_trace(x, temperature=temperature)
|
|
83
|
+
return t.render(level) if explain else t.result
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Scaled dot-product attention — the operation that defines transformers.
|
|
2
|
+
|
|
3
|
+
Attention(Q, K, V) = softmax( Q·Kᵀ / √dₖ ) · V
|
|
4
|
+
|
|
5
|
+
This is where every earlier idea converges: the dot products become the score
|
|
6
|
+
matrix (algebra), ``/√dₖ`` keeps gradients healthy (calculus), softmax turns
|
|
7
|
+
scores into weights (probability), and the final matmul mixes the values. Run it
|
|
8
|
+
with ``explain=True`` to watch all four stages.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from optimumai.core._fmt import arr, num
|
|
16
|
+
from optimumai.core.base_op import BaseOp
|
|
17
|
+
from optimumai.core.trace import Trace
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _softmax_rows(x: np.ndarray) -> np.ndarray:
|
|
21
|
+
"""Numerically stable softmax over the last axis (per query row)."""
|
|
22
|
+
shifted = x - np.max(x, axis=-1, keepdims=True)
|
|
23
|
+
exps = np.exp(shifted)
|
|
24
|
+
return exps / np.sum(exps, axis=-1, keepdims=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Attention(BaseOp):
|
|
28
|
+
"""Single-head scaled dot-product attention.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
d_k: Key/query dimension used for the ``1/√dₖ`` scaling. If omitted it is
|
|
32
|
+
inferred from ``Q`` at call time.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
name = "attention"
|
|
36
|
+
|
|
37
|
+
def __init__(self, d_k: int | None = None):
|
|
38
|
+
self.d_k = d_k
|
|
39
|
+
|
|
40
|
+
def trace(self, Q, K, V) -> Trace: # noqa: N803 - Q/K/V are the standard names
|
|
41
|
+
Q = np.asarray(Q, dtype=float)
|
|
42
|
+
K = np.asarray(K, dtype=float)
|
|
43
|
+
V = np.asarray(V, dtype=float)
|
|
44
|
+
for name, mat in ("Q", Q), ("K", K), ("V", V):
|
|
45
|
+
if mat.ndim != 2:
|
|
46
|
+
raise ValueError(f"{name} must be 2-D (tokens × features), got shape {mat.shape}")
|
|
47
|
+
if Q.shape[1] != K.shape[1]:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"Q and K must share the feature dim; got {Q.shape[1]} and {K.shape[1]}"
|
|
50
|
+
)
|
|
51
|
+
if K.shape[0] != V.shape[0]:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"K and V must share the number of tokens; got {K.shape[0]} and {V.shape[0]}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
d_k = self.d_k or Q.shape[1]
|
|
57
|
+
t = Trace(
|
|
58
|
+
op="attention",
|
|
59
|
+
formula="Attention(Q,K,V) = softmax(Q·Kᵀ / √dₖ) · V",
|
|
60
|
+
complexity="O(n²·d) for n tokens of dimension d",
|
|
61
|
+
why_ai=[
|
|
62
|
+
"The core operation of every transformer (GPT, BERT, ViT, ...)",
|
|
63
|
+
"Lets each token gather information from the tokens it finds relevant",
|
|
64
|
+
"Q = what I'm looking for, K = what I offer, V = what I pass on",
|
|
65
|
+
],
|
|
66
|
+
meta={"d_k": d_k, "q_shape": Q.shape, "k_shape": K.shape, "v_shape": V.shape},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
scores = Q @ K.T
|
|
70
|
+
t.add(
|
|
71
|
+
"Score: Q · Kᵀ",
|
|
72
|
+
f"raw relevance of every query to every key\n{arr(scores)}",
|
|
73
|
+
scores,
|
|
74
|
+
detail="Entry [i,j] is the dot product of query i with key j.",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
scale = np.sqrt(d_k)
|
|
78
|
+
scaled = scores / scale
|
|
79
|
+
t.add(
|
|
80
|
+
f"Scale by 1/√dₖ = 1/√{d_k}",
|
|
81
|
+
f"scores / {num(scale)}\n{arr(scaled)}",
|
|
82
|
+
scaled,
|
|
83
|
+
detail="Scaling keeps the softmax out of its saturated, low-gradient region.",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
weights = _softmax_rows(scaled)
|
|
87
|
+
row_sums = np.sum(weights, axis=-1)
|
|
88
|
+
t.add(
|
|
89
|
+
"Softmax over each row",
|
|
90
|
+
f"attention weights (each row sums to 1)\n{arr(weights)}",
|
|
91
|
+
weights,
|
|
92
|
+
detail=f"Row sums = {arr(row_sums)} — every query distributes 100% of its attention.",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
output = weights @ V
|
|
96
|
+
t.add(
|
|
97
|
+
"Weighted sum: weights · V",
|
|
98
|
+
f"blend value vectors by attention weight\n{arr(output)}",
|
|
99
|
+
output,
|
|
100
|
+
detail="Each output row is a convex combination of the value vectors.",
|
|
101
|
+
)
|
|
102
|
+
t.result = output
|
|
103
|
+
return t
|
|
104
|
+
|
|
105
|
+
def forward(self, Q, K, V, explain: bool = False, level="intermediate"): # noqa: N803
|
|
106
|
+
"""Compute attention. Set ``explain=True`` to print the four-stage trace."""
|
|
107
|
+
t = self.trace(Q, K, V)
|
|
108
|
+
return t.render(level) if explain else t.result
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def demo(cls, seed: int = 0) -> Trace:
|
|
112
|
+
"""A tiny, reproducible 3-token / dₖ=4 example for docs and the CLI."""
|
|
113
|
+
rng = np.random.default_rng(seed)
|
|
114
|
+
Q = rng.normal(size=(3, 4)).round(2)
|
|
115
|
+
K = rng.normal(size=(3, 4)).round(2)
|
|
116
|
+
V = rng.normal(size=(3, 4)).round(2)
|
|
117
|
+
return cls(d_k=4).trace(Q, K, V)
|
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Render a :class:`~optimumai.core.trace.Trace` to the terminal with Rich.
|
|
2
|
+
|
|
3
|
+
The visual grammar is intentionally consistent across every operation:
|
|
4
|
+
|
|
5
|
+
┌ formula ┐ → step table → result → why AI uses this
|
|
6
|
+
|
|
7
|
+
so that a dot product and a full attention block feel like the same tool.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
from rich.text import Text
|
|
18
|
+
|
|
19
|
+
from optimumai.core._fmt import arr, shape_of
|
|
20
|
+
from optimumai.core.explain import ExplainLevel
|
|
21
|
+
from optimumai.core.trace import Trace
|
|
22
|
+
|
|
23
|
+
_default_console = Console()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _fmt_step_value(value: Any) -> str:
|
|
27
|
+
if value is None:
|
|
28
|
+
return ""
|
|
29
|
+
text = arr(value)
|
|
30
|
+
# Multi-line arrays already carry their own layout; keep scalars terse.
|
|
31
|
+
return text
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def render_trace(
|
|
35
|
+
trace: Trace,
|
|
36
|
+
level: str | ExplainLevel = ExplainLevel.INTERMEDIATE,
|
|
37
|
+
console: Console | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Pretty-print ``trace`` at the requested detail ``level``."""
|
|
40
|
+
level = ExplainLevel.parse(level)
|
|
41
|
+
console = console or _default_console
|
|
42
|
+
|
|
43
|
+
# ---- Header: name + formula ------------------------------------------
|
|
44
|
+
header = Text(trace.op.replace("_", " ").upper(), style="bold cyan")
|
|
45
|
+
if trace.formula:
|
|
46
|
+
header.append("\n")
|
|
47
|
+
header.append(trace.formula, style="italic")
|
|
48
|
+
console.print(Panel(header, border_style="cyan", title="[bold]OptimumAI[/bold]"))
|
|
49
|
+
|
|
50
|
+
# ---- Steps table ------------------------------------------------------
|
|
51
|
+
table = Table(show_lines=True, expand=False, border_style="grey42")
|
|
52
|
+
table.add_column("#", justify="right", style="dim", no_wrap=True)
|
|
53
|
+
table.add_column("Step", style="bold")
|
|
54
|
+
table.add_column("Computation", style="white")
|
|
55
|
+
if level.at_least(ExplainLevel.ENGINEER):
|
|
56
|
+
table.add_column("Result", style="green")
|
|
57
|
+
|
|
58
|
+
show_detail = level.at_least(ExplainLevel.INTERMEDIATE)
|
|
59
|
+
for step in trace.steps:
|
|
60
|
+
computation = step.expression
|
|
61
|
+
if show_detail and step.detail:
|
|
62
|
+
computation += f"\n[dim italic]{step.detail}[/dim italic]"
|
|
63
|
+
row = [str(step.index), step.title, computation]
|
|
64
|
+
if level.at_least(ExplainLevel.ENGINEER):
|
|
65
|
+
row.append(_fmt_step_value(step.value))
|
|
66
|
+
table.add_row(*row)
|
|
67
|
+
console.print(table)
|
|
68
|
+
|
|
69
|
+
# ---- Result -----------------------------------------------------------
|
|
70
|
+
result_text = arr(trace.result) if trace.result is not None else "—"
|
|
71
|
+
console.print(
|
|
72
|
+
Panel(
|
|
73
|
+
Text(result_text, style="bold green"),
|
|
74
|
+
title=f"Result · {shape_of(trace.result)}",
|
|
75
|
+
border_style="green",
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# ---- Why AI uses this -------------------------------------------------
|
|
80
|
+
if trace.why_ai:
|
|
81
|
+
bullets = "\n".join(f"• {reason}" for reason in trace.why_ai)
|
|
82
|
+
console.print(
|
|
83
|
+
Panel(bullets, title="Why AI uses this", border_style="magenta")
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# ---- Complexity (engineer+) ------------------------------------------
|
|
87
|
+
if trace.complexity and level.at_least(ExplainLevel.ENGINEER):
|
|
88
|
+
console.print(Text(f"Complexity: {trace.complexity}", style="dim yellow"))
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: optimumai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unlock the math behind AI — run any operation with explain=True for a step-by-step trace, terminal visualization, and the intuition of why AI uses it.
|
|
5
|
+
Project-URL: Homepage, https://github.com/muhammadyahiya/optimumai
|
|
6
|
+
Project-URL: Repository, https://github.com/muhammadyahiya/optimumai
|
|
7
|
+
Project-URL: Issues, https://github.com/muhammadyahiya/optimumai/issues
|
|
8
|
+
Author: Muhammad Yahiya
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,attention,deep-learning,education,explainable,machine-learning,mathematics,transformers,tutorial
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Education
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Education
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: click>=8.1
|
|
26
|
+
Requires-Dist: numpy>=1.26
|
|
27
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
28
|
+
Requires-Dist: python-dotenv>=1.0
|
|
29
|
+
Requires-Dist: rich>=13.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
34
|
+
Provides-Extra: llm
|
|
35
|
+
Requires-Dist: litellm>=1.0; extra == 'llm'
|
|
36
|
+
Provides-Extra: viz
|
|
37
|
+
Requires-Dist: matplotlib>=3.8; extra == 'viz'
|
|
38
|
+
Requires-Dist: plotext>=5.2; extra == 'viz'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# OptimumAI
|
|
42
|
+
|
|
43
|
+
**Unlock the math behind AI.**
|
|
44
|
+
|
|
45
|
+
Every mathematical operation in modern AI — from a dot product to a full
|
|
46
|
+
attention block — can be run with `explain=True` to produce a **step-by-step
|
|
47
|
+
computation trace**, a **terminal visualization**, and the intuition for **why
|
|
48
|
+
AI actually uses it**.
|
|
49
|
+
|
|
50
|
+
The same code runs fast in production *or* teaches you exactly what it's doing.
|
|
51
|
+
micrograd shows you scalar backprop; EpyNN walks you through MLPs — OptimumAI
|
|
52
|
+
gives you a single, traceable API that runs the whole way from `a · b` up to
|
|
53
|
+
`softmax(QKᵀ/√dₖ)·V`.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from optimumai import Vector
|
|
57
|
+
|
|
58
|
+
Vector([1, 2, 3]).dot(Vector([4, 5, 6]), explain=True)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
╭───────────────────────── OptimumAI ──────────────────────────╮
|
|
63
|
+
│ DOT │
|
|
64
|
+
│ a · b = Σᵢ aᵢ·bᵢ │
|
|
65
|
+
╰───────────────────────────────────────────────────────────────╯
|
|
66
|
+
# Step Computation
|
|
67
|
+
1 Multiply component 0 1 × 4 = 4
|
|
68
|
+
2 Multiply component 1 2 × 5 = 10
|
|
69
|
+
3 Multiply component 2 3 × 6 = 18
|
|
70
|
+
4 Sum the products 4 + 10 + 18 = 32
|
|
71
|
+
╭──────── Result · scalar ────────╮
|
|
72
|
+
│ 32 │
|
|
73
|
+
╰─────────────────────────────────╯
|
|
74
|
+
╭──────────── Why AI uses this ────────────╮
|
|
75
|
+
│ • Similarity between two embedding vectors │
|
|
76
|
+
│ • The raw attention score q · k │
|
|
77
|
+
│ • The inner loop of every matrix multiply │
|
|
78
|
+
╰────────────────────────────────────────────╯
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Install
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install optimumai
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Optional extras:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install "optimumai[llm]" # LLM tutor (Q&A over concepts)
|
|
93
|
+
pip install "optimumai[viz]" # extra plotting backends
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Quickstart — Python
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from optimumai import Vector, Matrix, softmax, Attention
|
|
100
|
+
|
|
101
|
+
# Linear algebra
|
|
102
|
+
Vector([1, 2, 3]).cosine_similarity(Vector([2, 4, 6]), explain=True) # → 1.0
|
|
103
|
+
Matrix([[1, 2], [3, 4]]).matmul(Matrix([[5, 6], [7, 8]]), explain=True)
|
|
104
|
+
|
|
105
|
+
# Probability
|
|
106
|
+
softmax([2.0, 1.0, 0.1], temperature=0.5, explain=True)
|
|
107
|
+
|
|
108
|
+
# Transformers — the headline
|
|
109
|
+
Attention(d_k=4).forward(Q, K, V, explain=True)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Every `explain=True` call returns the numeric result *and* prints the trace, so
|
|
113
|
+
it drops straight into notebooks, scripts, and tests. Prefer the data over the
|
|
114
|
+
print-out? Use the `*_trace` variants:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
trace = Vector([1, 2, 3]).dot_trace(Vector([4, 5, 6]))
|
|
118
|
+
trace.result # 32.0
|
|
119
|
+
trace.steps # [Step(...), Step(...), ...]
|
|
120
|
+
trace.why_ai # ['Similarity between two embedding vectors', ...]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Quickstart — CLI
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
optimumai algebra dot "[1,2,3]" "[4,5,6]"
|
|
127
|
+
optimumai algebra matmul "[[1,2],[3,4]]" "[[5,6],[7,8]]"
|
|
128
|
+
optimumai softmax "[2,1,0.1]" --temperature 0.5
|
|
129
|
+
optimumai attention --demo --level engineer
|
|
130
|
+
optimumai learn # list every topic
|
|
131
|
+
optimumai learn attention --level researcher
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Explain levels
|
|
135
|
+
|
|
136
|
+
The same math, revealed for four audiences (`--level` on the CLI, `level=` in
|
|
137
|
+
Python):
|
|
138
|
+
|
|
139
|
+
| Level | Adds |
|
|
140
|
+
| -------------- | ------------------------------------------------- |
|
|
141
|
+
| `beginner` | The steps and plain-English "why" |
|
|
142
|
+
| `intermediate` | Per-step detail notes (default) |
|
|
143
|
+
| `engineer` | Intermediate values + complexity |
|
|
144
|
+
| `researcher` | Everything |
|
|
145
|
+
|
|
146
|
+
## What's inside
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
optimumai/
|
|
150
|
+
├── core/ # Tracer, Step/Trace model, ExplainLevel, BaseOp
|
|
151
|
+
├── algebra/ # Vector (dot, norm, cosine), Matrix (matmul)
|
|
152
|
+
├── probability/ # softmax (with temperature + stability)
|
|
153
|
+
├── transformers/ # scaled dot-product Attention
|
|
154
|
+
├── visualization/ # Rich terminal renderer
|
|
155
|
+
└── cli/ # the `optimumai` command
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Roadmap
|
|
159
|
+
|
|
160
|
+
`v0.1` ships the spine — algebra → probability → attention — plus the tracer,
|
|
161
|
+
CLI, and terminal visualization. Next up:
|
|
162
|
+
|
|
163
|
+
- **Calculus & optimization** — derivatives, gradients, SGD/Adam convergence
|
|
164
|
+
- **Neural networks** — dense layers, activations, full backprop trace
|
|
165
|
+
- **Multi-head attention, positional encoding, a full transformer block**
|
|
166
|
+
- **Embeddings, RAG pipeline traces, diffusion schedules**
|
|
167
|
+
- **LLM tutor** — `Tutor().ask("Why is LayerNorm after attention?")`
|
|
168
|
+
- **Streamlit explorer** for visual, interactive pipelines
|
|
169
|
+
|
|
170
|
+
## Development
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
git clone https://github.com/muhammadyahiya/optimumai
|
|
174
|
+
cd optimumai
|
|
175
|
+
uv venv && uv pip install -e ".[dev]"
|
|
176
|
+
pytest
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT © 2026 Muhammad Yahiya
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
optimumai/__init__.py,sha256=RdLdgZi6-8nxDQw-Uz5ef9yAJy-g36aTunaLxq478xQ,849
|
|
2
|
+
optimumai/config.py,sha256=hjTbdBwGmU2ZTyqwOzxa9IVnO7a8bcxILFJsJVQpudA,1194
|
|
3
|
+
optimumai/algebra/__init__.py,sha256=7I-esC0-DM5eDt-39i9MAyz2fPNJnFJ-JSTbnjdNnbU,174
|
|
4
|
+
optimumai/algebra/matrix.py,sha256=AVmzBj1TnsAmmKHezv1WTY-tbob8PPzo9ZlRzNkPF6E,3412
|
|
5
|
+
optimumai/algebra/vector.py,sha256=rVUsVFBaehrmCjCRM_QEkEBb3UZDAekNjDYyULEJQgQ,6066
|
|
6
|
+
optimumai/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
optimumai/cli/main.py,sha256=WUD-q1LQNSREYbWLSQBM5Hyua2zPe7vwR-cqDVMcqzA,5641
|
|
8
|
+
optimumai/core/__init__.py,sha256=3PysOkzYJ_eBrmPOfaPOK3I5Q2fTGEbJTjz1i3UsVqA,269
|
|
9
|
+
optimumai/core/_fmt.py,sha256=bOEZ63Cii3uPv8U8dUbkVLpwh3zIW4YQ_h1PDYDCSSo,1122
|
|
10
|
+
optimumai/core/base_op.py,sha256=1V2usbw6y-Ut0AuEexEL3Hdd9aDCCKaG-dvwvPOQmQs,1967
|
|
11
|
+
optimumai/core/explain.py,sha256=QuVBoSbTAv6wqAOp59hO5ttmHQPI2TD0mqeJbNVGclQ,1499
|
|
12
|
+
optimumai/core/trace.py,sha256=gumIqCnCWYkUT_PZGboa7ogFqOVGx9R5gf9VvjLQE8Y,3052
|
|
13
|
+
optimumai/probability/__init__.py,sha256=5iY2gxZ0H045qCqBw9R_jdeFyxAm46RhuDHPoW64wxA,163
|
|
14
|
+
optimumai/probability/softmax.py,sha256=ynqZ_SxVEDcuMCkEbmedEfHHWNLvFrsjmUV8w28ZG3I,2756
|
|
15
|
+
optimumai/transformers/__init__.py,sha256=hl3wcjDiUOKnQQ7iDacLQAdNBiA8KHAYCERLRjAGtzU,136
|
|
16
|
+
optimumai/transformers/attention.py,sha256=vZeQAEGRKL6s2QdC_kwkE0jCYYXsJ8IOiDLgKvNjySg,4364
|
|
17
|
+
optimumai/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
optimumai/visualization/__init__.py,sha256=ph9x8U8lY53Q2WlXUz2TqoSTZd3_QOaRIEiOACUxyCY,136
|
|
19
|
+
optimumai/visualization/terminal.py,sha256=l1YfXuAk_9agKnGYZk6SZYPnWY_1sxTREANHpygT9HU,3192
|
|
20
|
+
optimumai-0.1.0.dist-info/METADATA,sha256=8ic742KPGdzIQ9Ph4Rjt1T-8vsAINgmbXIrRskoCu34,6773
|
|
21
|
+
optimumai-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
22
|
+
optimumai-0.1.0.dist-info/entry_points.txt,sha256=OFfCnMHQ7JfnW10bCbnMXSJYj7If3jDgu6IY1oGBHZE,53
|
|
23
|
+
optimumai-0.1.0.dist-info/licenses/LICENSE,sha256=e6mPmo0oVijvAFmm8-_9XTjH9xeyYJ1RqK_jYL7tiPo,1072
|
|
24
|
+
optimumai-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Muhammad Yahiya
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|