Spaces:
Running
Running
Deploy optimisation from main (6727a1a)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +18 -0
- README.md +54 -5
- backend/.python-version +1 -0
- backend/README 2.md +10 -0
- backend/README.md +10 -0
- backend/contract/gradient_descent_quadratic 2.json +11 -0
- backend/contract/gradient_descent_quadratic.json +11 -0
- backend/contract/invalid_function 2.json +6 -0
- backend/contract/invalid_function.json +6 -0
- backend/contract/newton_singular_hessian 2.json +20 -0
- backend/contract/newton_singular_hessian.json +20 -0
- backend/contract/past_the_end 2.json +6 -0
- backend/contract/past_the_end.json +6 -0
- backend/pyproject.toml +18 -0
- backend/src/algorithms 2.json +60 -0
- backend/src/algorithms.json +60 -0
- backend/src/logic.py +231 -0
- backend/src/manager 2.py +118 -0
- backend/src/manager.py +118 -0
- backend/tests/test_contract.py +53 -0
- backend/tests/test_logic 2.py +175 -0
- backend/tests/test_logic.py +175 -0
- backend/uv.lock +169 -0
- dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J 2.woff2 +0 -0
- dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ 2.woff +0 -0
- dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv 2.woff +0 -0
- dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag 2.woff2 +0 -0
- dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv 2.woff +0 -0
- dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f 2.woff2 +0 -0
- dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT 2.woff +0 -0
- dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T 2.woff2 +0 -0
- dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP 2.woff +0 -0
- dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y 2.woff2 +0 -0
- dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo 2.woff +0 -0
- dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd 2.woff2 +0 -0
- dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV 2.woff2 +0 -0
- dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC 2.woff +0 -0
.dockerignore
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dependency folders (The biggest space eaters)
|
| 2 |
+
node_modules/
|
| 3 |
+
__pycache__/
|
| 4 |
+
*.pyc
|
| 5 |
+
venv/
|
| 6 |
+
.env
|
| 7 |
+
|
| 8 |
+
# Git history (Not needed inside the container)
|
| 9 |
+
.git
|
| 10 |
+
.gitignore
|
| 11 |
+
|
| 12 |
+
# Build outputs from your local machine
|
| 13 |
+
dist/
|
| 14 |
+
build/
|
| 15 |
+
|
| 16 |
+
# OS trash
|
| 17 |
+
.DS_Store
|
| 18 |
+
Thumbs.db
|
README.md
CHANGED
|
@@ -1,10 +1,59 @@
|
|
| 1 |
---
|
| 2 |
-
title: Optimization
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Optimization
|
| 3 |
+
emoji: 🚀
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: static
|
| 7 |
+
app_file: dist/index.html
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# Developer's Guide for React frontend
|
| 12 |
+
|
| 13 |
+
## Prerequisites
|
| 14 |
+
|
| 15 |
+
Install **Node.js** if not done yet. The following commands should run if you have it installed.
|
| 16 |
+
|
| 17 |
+
```bash
|
| 18 |
+
node -v
|
| 19 |
+
npm -v
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
## Local Development
|
| 23 |
+
|
| 24 |
+
First go to the react frontend folder:
|
| 25 |
+
```bash
|
| 26 |
+
cd frontends/react
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
Run the following commands to install required packages when you run the code the first time or after you make changes to `package.json`.
|
| 30 |
+
```bash
|
| 31 |
+
npm install
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
Now you should be able to start the local development server:
|
| 35 |
+
```bash
|
| 36 |
+
npm run dev
|
| 37 |
+
```
|
| 38 |
+
Open the link printed out on the terminal. The webpage automatically reloads when files are edited.
|
| 39 |
+
|
| 40 |
+
## Deploy to Hugging Face
|
| 41 |
+
|
| 42 |
+
### Build the production version:
|
| 43 |
+
|
| 44 |
+
```bash
|
| 45 |
+
npm run build
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
This generates the production build inside the `dist/` directory.
|
| 49 |
+
|
| 50 |
+
### Commit and push:
|
| 51 |
+
|
| 52 |
+
```bash
|
| 53 |
+
git add <files>
|
| 54 |
+
git commit
|
| 55 |
+
git push
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
⚠️ Make sure the `dist/` directory is included when adding to git.
|
| 59 |
+
⚠️ Even if you only changed the backend, you must still run `npm run build` when deploying or it will not update.
|
backend/.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.12
|
backend/README 2.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Optimisation backend
|
| 2 |
+
|
| 3 |
+
Python run in the browser by Pyodide (see `frontends/react/src/pyodide.worker.ts`).
|
| 4 |
+
|
| 5 |
+
- `src/logic.py`: parsing the function, its derivatives, the optimisers and runs
|
| 6 |
+
- `src/manager.py`: the `Manager` methods the UI calls
|
| 7 |
+
- `src/algorithms.json`: each optimiser's settings, shared with the UI
|
| 8 |
+
- `contract/`: example calls, checked by `tests/test_contract.py` and the frontend's `contract.check.ts`
|
| 9 |
+
|
| 10 |
+
Run the tests with `uv run pytest`.
|
backend/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Optimisation backend
|
| 2 |
+
|
| 3 |
+
Python run in the browser by Pyodide (see `frontends/react/src/pyodide.worker.ts`).
|
| 4 |
+
|
| 5 |
+
- `src/logic.py`: parsing the function, its derivatives, the optimisers and runs
|
| 6 |
+
- `src/manager.py`: the `Manager` methods the UI calls
|
| 7 |
+
- `src/algorithms.json`: each optimiser's settings, shared with the UI
|
| 8 |
+
- `contract/`: example calls, checked by `tests/test_contract.py` and the frontend's `contract.check.ts`
|
| 9 |
+
|
| 10 |
+
Run the tests with `uv run pytest`.
|
backend/contract/gradient_descent_quadratic 2.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"description": "Gradient descent on x^2 from x = 1 with learning rate 0.25 halves x each iteration",
|
| 3 |
+
"calls": {
|
| 4 |
+
"handle_set_function": { "args": { "variables": 1, "expression": "x^2" }, "result": null },
|
| 5 |
+
"handle_set_runs": { "args": { "runs": [{ "algorithm": "gradient_descent", "start": [1.0], "params": { "learning_rate": 0.25 } }] }, "result": null },
|
| 6 |
+
"handle_state": {
|
| 7 |
+
"args": { "iteration": 2 },
|
| 8 |
+
"result": { "runs": [{ "point": { "position": [0.25], "value": 0.0625, "gradient": [0.5], "hessian": [[2.0]] }, "stopped": null }] }
|
| 9 |
+
}
|
| 10 |
+
}
|
| 11 |
+
}
|
backend/contract/gradient_descent_quadratic.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"description": "Gradient descent on x^2 from x = 1 with learning rate 0.25 halves x each iteration",
|
| 3 |
+
"calls": {
|
| 4 |
+
"handle_set_function": { "args": { "variables": 1, "expression": "x^2" }, "result": null },
|
| 5 |
+
"handle_set_runs": { "args": { "runs": [{ "algorithm": "gradient_descent", "start": [1.0], "params": { "learning_rate": 0.25 } }] }, "result": null },
|
| 6 |
+
"handle_state": {
|
| 7 |
+
"args": { "iteration": 2 },
|
| 8 |
+
"result": { "runs": [{ "point": { "position": [0.25], "value": 0.0625, "gradient": [0.5], "hessian": [[2.0]] }, "stopped": null }] }
|
| 9 |
+
}
|
| 10 |
+
}
|
| 11 |
+
}
|
backend/contract/invalid_function 2.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"description": "A function of one variable can't use y",
|
| 3 |
+
"calls": {
|
| 4 |
+
"handle_set_function": { "args": { "variables": 1, "expression": "x^2 + y" }, "error": { "kind": "user", "message": "use only x" } }
|
| 5 |
+
}
|
| 6 |
+
}
|
backend/contract/invalid_function.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"description": "A function of one variable can't use y",
|
| 3 |
+
"calls": {
|
| 4 |
+
"handle_set_function": { "args": { "variables": 1, "expression": "x^2 + y" }, "error": { "kind": "user", "message": "use only x" } }
|
| 5 |
+
}
|
| 6 |
+
}
|
backend/contract/newton_singular_hessian 2.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"description": "Two runs stepped together on x^2 + 0y: Newton stops at once (singular Hessian), gradient descent (default learning rate 0.1) carries on",
|
| 3 |
+
"calls": {
|
| 4 |
+
"handle_set_function": { "args": { "variables": 2, "expression": "x^2 + 0y" }, "result": null },
|
| 5 |
+
"handle_set_runs": {
|
| 6 |
+
"args": { "runs": [
|
| 7 |
+
{ "algorithm": "gradient_descent", "start": [1.0, 1.0], "params": {} },
|
| 8 |
+
{ "algorithm": "newton", "start": [1.0, 1.0], "params": {} }
|
| 9 |
+
] },
|
| 10 |
+
"result": null
|
| 11 |
+
},
|
| 12 |
+
"handle_state": {
|
| 13 |
+
"args": { "iteration": 1 },
|
| 14 |
+
"result": { "runs": [
|
| 15 |
+
{ "point": { "position": [0.8, 1.0], "value": 0.64, "gradient": [1.6, 0.0], "hessian": [[2.0, 0.0], [0.0, 0.0]] }, "stopped": null },
|
| 16 |
+
{ "point": null, "stopped": "The Hessian is singular here, so Newton's method can't take a step." }
|
| 17 |
+
] }
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
}
|
backend/contract/newton_singular_hessian.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"description": "Two runs stepped together on x^2 + 0y: Newton stops at once (singular Hessian), gradient descent (default learning rate 0.1) carries on",
|
| 3 |
+
"calls": {
|
| 4 |
+
"handle_set_function": { "args": { "variables": 2, "expression": "x^2 + 0y" }, "result": null },
|
| 5 |
+
"handle_set_runs": {
|
| 6 |
+
"args": { "runs": [
|
| 7 |
+
{ "algorithm": "gradient_descent", "start": [1.0, 1.0], "params": {} },
|
| 8 |
+
{ "algorithm": "newton", "start": [1.0, 1.0], "params": {} }
|
| 9 |
+
] },
|
| 10 |
+
"result": null
|
| 11 |
+
},
|
| 12 |
+
"handle_state": {
|
| 13 |
+
"args": { "iteration": 1 },
|
| 14 |
+
"result": { "runs": [
|
| 15 |
+
{ "point": { "position": [0.8, 1.0], "value": 0.64, "gradient": [1.6, 0.0], "hessian": [[2.0, 0.0], [0.0, 0.0]] }, "stopped": null },
|
| 16 |
+
{ "point": null, "stopped": "The Hessian is singular here, so Newton's method can't take a step." }
|
| 17 |
+
] }
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
}
|
backend/contract/past_the_end 2.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"description": "Before any runs are set there is no state, so the stepper finds the end at iteration 0",
|
| 3 |
+
"calls": {
|
| 4 |
+
"handle_state": { "args": { "iteration": 0 }, "result": null }
|
| 5 |
+
}
|
| 6 |
+
}
|
backend/contract/past_the_end.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"description": "Before any runs are set there is no state, so the stepper finds the end at iteration 0",
|
| 3 |
+
"calls": {
|
| 4 |
+
"handle_state": { "args": { "iteration": 0 }, "result": null }
|
| 5 |
+
}
|
| 6 |
+
}
|
backend/pyproject.toml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "backend"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Optimisation visualisation: objective functions and optimisers, run in the browser by Pyodide"
|
| 5 |
+
requires-python = ">=3.12"
|
| 6 |
+
dependencies = [
|
| 7 |
+
"numpy>=2.4.0",
|
| 8 |
+
"sympy>=1.14.0",
|
| 9 |
+
]
|
| 10 |
+
|
| 11 |
+
[dependency-groups]
|
| 12 |
+
dev = [
|
| 13 |
+
"pytest>=9.2.0",
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
[tool.pytest.ini_options]
|
| 17 |
+
pythonpath = ["src", "."]
|
| 18 |
+
testpaths = ["tests"]
|
backend/src/algorithms 2.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"algorithms": [
|
| 3 |
+
{
|
| 4 |
+
"id": "gradient_descent",
|
| 5 |
+
"label": "Gradient descent",
|
| 6 |
+
"params": [
|
| 7 |
+
{ "name": "learning_rate", "label": "Learning rate η", "symbol": "η", "kind": "number", "min": 0, "minExclusive": true, "default": 0.1, "description": "How far each step moves along the negative gradient." },
|
| 8 |
+
{ "name": "momentum", "label": "Momentum μ", "symbol": "μ", "kind": "number", "min": 0, "max": 1, "maxExclusive": true, "default": 0.0, "description": "How much of the previous step carries over into the next one." }
|
| 9 |
+
]
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
"id": "nesterov",
|
| 13 |
+
"label": "Nesterov momentum",
|
| 14 |
+
"params": [
|
| 15 |
+
{ "name": "learning_rate", "label": "Learning rate η", "symbol": "η", "kind": "number", "min": 0, "minExclusive": true, "default": 0.1, "description": "How far each step moves along the negative gradient." },
|
| 16 |
+
{ "name": "momentum", "label": "Momentum μ", "symbol": "μ", "kind": "number", "min": 0, "max": 1, "maxExclusive": true, "default": 0.1, "description": "How much of the previous step carries over; the gradient is taken at the look-ahead point." }
|
| 17 |
+
]
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"id": "newton",
|
| 21 |
+
"label": "Newton's method",
|
| 22 |
+
"params": []
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"id": "adam",
|
| 26 |
+
"label": "Adam",
|
| 27 |
+
"params": [
|
| 28 |
+
{ "name": "learning_rate", "label": "Learning rate η", "symbol": "η", "kind": "number", "min": 0, "minExclusive": true, "default": 0.1, "description": "The largest step size, roughly." },
|
| 29 |
+
{ "name": "beta1", "label": "Decay β₁", "symbol": "β₁", "kind": "number", "min": 0, "max": 1, "maxExclusive": true, "default": 0.9, "description": "Decay rate of the moving average of gradients (first moment)." },
|
| 30 |
+
{ "name": "beta2", "label": "Decay β₂", "symbol": "β₂", "kind": "number", "min": 0, "max": 1, "maxExclusive": true, "default": 0.999, "description": "Decay rate of the moving average of squared gradients (second moment)." },
|
| 31 |
+
{ "name": "epsilon", "label": "Epsilon ε", "symbol": "ε", "kind": "number", "min": 0, "minExclusive": true, "default": 1e-8, "description": "Added to the denominator to avoid dividing by zero." }
|
| 32 |
+
]
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"id": "adagrad",
|
| 36 |
+
"label": "Adagrad",
|
| 37 |
+
"params": [
|
| 38 |
+
{ "name": "learning_rate", "label": "Learning rate η", "symbol": "η", "kind": "number", "min": 0, "minExclusive": true, "default": 0.1, "description": "Divided by the root of the summed squared gradients." },
|
| 39 |
+
{ "name": "epsilon", "label": "Epsilon ε", "symbol": "ε", "kind": "number", "min": 0, "minExclusive": true, "default": 1e-8, "description": "Added to the denominator to avoid dividing by zero." }
|
| 40 |
+
]
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"id": "rmsprop",
|
| 44 |
+
"label": "RMSProp",
|
| 45 |
+
"params": [
|
| 46 |
+
{ "name": "learning_rate", "label": "Learning rate η", "symbol": "η", "kind": "number", "min": 0, "minExclusive": true, "default": 0.1, "description": "Divided by the root of the moving average of squared gradients." },
|
| 47 |
+
{ "name": "beta", "label": "Decay β", "symbol": "β", "kind": "number", "min": 0, "max": 1, "maxExclusive": true, "default": 0.9, "description": "Decay rate of the moving average of squared gradients." },
|
| 48 |
+
{ "name": "epsilon", "label": "Epsilon ε", "symbol": "ε", "kind": "number", "min": 0, "minExclusive": true, "default": 1e-8, "description": "Added to the denominator to avoid dividing by zero." }
|
| 49 |
+
]
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"id": "adadelta",
|
| 53 |
+
"label": "Adadelta",
|
| 54 |
+
"params": [
|
| 55 |
+
{ "name": "beta", "label": "Decay β", "symbol": "β", "kind": "number", "min": 0, "max": 1, "maxExclusive": true, "default": 0.9, "description": "Decay rate of the moving averages of squared gradients and squared updates." },
|
| 56 |
+
{ "name": "epsilon", "label": "Epsilon ε", "symbol": "ε", "kind": "number", "min": 0, "minExclusive": true, "default": 0.001, "description": "Sets the first step size, and avoids dividing by zero." }
|
| 57 |
+
]
|
| 58 |
+
}
|
| 59 |
+
]
|
| 60 |
+
}
|
backend/src/algorithms.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"algorithms": [
|
| 3 |
+
{
|
| 4 |
+
"id": "gradient_descent",
|
| 5 |
+
"label": "Gradient descent",
|
| 6 |
+
"params": [
|
| 7 |
+
{ "name": "learning_rate", "label": "Learning rate η", "symbol": "η", "kind": "number", "min": 0, "minExclusive": true, "default": 0.1, "description": "How far each step moves along the negative gradient." },
|
| 8 |
+
{ "name": "momentum", "label": "Momentum μ", "symbol": "μ", "kind": "number", "min": 0, "max": 1, "maxExclusive": true, "default": 0.0, "description": "How much of the previous step carries over into the next one." }
|
| 9 |
+
]
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
"id": "nesterov",
|
| 13 |
+
"label": "Nesterov momentum",
|
| 14 |
+
"params": [
|
| 15 |
+
{ "name": "learning_rate", "label": "Learning rate η", "symbol": "η", "kind": "number", "min": 0, "minExclusive": true, "default": 0.1, "description": "How far each step moves along the negative gradient." },
|
| 16 |
+
{ "name": "momentum", "label": "Momentum μ", "symbol": "μ", "kind": "number", "min": 0, "max": 1, "maxExclusive": true, "default": 0.1, "description": "How much of the previous step carries over; the gradient is taken at the look-ahead point." }
|
| 17 |
+
]
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"id": "newton",
|
| 21 |
+
"label": "Newton's method",
|
| 22 |
+
"params": []
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"id": "adam",
|
| 26 |
+
"label": "Adam",
|
| 27 |
+
"params": [
|
| 28 |
+
{ "name": "learning_rate", "label": "Learning rate η", "symbol": "η", "kind": "number", "min": 0, "minExclusive": true, "default": 0.1, "description": "The largest step size, roughly." },
|
| 29 |
+
{ "name": "beta1", "label": "Decay β₁", "symbol": "β₁", "kind": "number", "min": 0, "max": 1, "maxExclusive": true, "default": 0.9, "description": "Decay rate of the moving average of gradients (first moment)." },
|
| 30 |
+
{ "name": "beta2", "label": "Decay β₂", "symbol": "β₂", "kind": "number", "min": 0, "max": 1, "maxExclusive": true, "default": 0.999, "description": "Decay rate of the moving average of squared gradients (second moment)." },
|
| 31 |
+
{ "name": "epsilon", "label": "Epsilon ε", "symbol": "ε", "kind": "number", "min": 0, "minExclusive": true, "default": 1e-8, "description": "Added to the denominator to avoid dividing by zero." }
|
| 32 |
+
]
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"id": "adagrad",
|
| 36 |
+
"label": "Adagrad",
|
| 37 |
+
"params": [
|
| 38 |
+
{ "name": "learning_rate", "label": "Learning rate η", "symbol": "η", "kind": "number", "min": 0, "minExclusive": true, "default": 0.1, "description": "Divided by the root of the summed squared gradients." },
|
| 39 |
+
{ "name": "epsilon", "label": "Epsilon ε", "symbol": "ε", "kind": "number", "min": 0, "minExclusive": true, "default": 1e-8, "description": "Added to the denominator to avoid dividing by zero." }
|
| 40 |
+
]
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"id": "rmsprop",
|
| 44 |
+
"label": "RMSProp",
|
| 45 |
+
"params": [
|
| 46 |
+
{ "name": "learning_rate", "label": "Learning rate η", "symbol": "η", "kind": "number", "min": 0, "minExclusive": true, "default": 0.1, "description": "Divided by the root of the moving average of squared gradients." },
|
| 47 |
+
{ "name": "beta", "label": "Decay β", "symbol": "β", "kind": "number", "min": 0, "max": 1, "maxExclusive": true, "default": 0.9, "description": "Decay rate of the moving average of squared gradients." },
|
| 48 |
+
{ "name": "epsilon", "label": "Epsilon ε", "symbol": "ε", "kind": "number", "min": 0, "minExclusive": true, "default": 1e-8, "description": "Added to the denominator to avoid dividing by zero." }
|
| 49 |
+
]
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"id": "adadelta",
|
| 53 |
+
"label": "Adadelta",
|
| 54 |
+
"params": [
|
| 55 |
+
{ "name": "beta", "label": "Decay β", "symbol": "β", "kind": "number", "min": 0, "max": 1, "maxExclusive": true, "default": 0.9, "description": "Decay rate of the moving averages of squared gradients and squared updates." },
|
| 56 |
+
{ "name": "epsilon", "label": "Epsilon ε", "symbol": "ε", "kind": "number", "min": 0, "minExclusive": true, "default": 0.001, "description": "Sets the first step size, and avoids dividing by zero." }
|
| 57 |
+
]
|
| 58 |
+
}
|
| 59 |
+
]
|
| 60 |
+
}
|
backend/src/logic.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Objective functions and optimisers, in one or two variables.
|
| 2 |
+
|
| 3 |
+
Positions, gradients and Hessians are NumPy arrays of length 1 or 2, so each
|
| 4 |
+
optimiser is written once for both.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
from sympy import Abs, Expr, acos, asin, atan, cos, exp, lambdify, log, pi, sin, sqrt, symbols, tan
|
| 9 |
+
from sympy.parsing.sympy_parser import (
|
| 10 |
+
convert_xor,
|
| 11 |
+
implicit_multiplication_application,
|
| 12 |
+
parse_expr,
|
| 13 |
+
standard_transformations,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
# Real, so that e.g. d/dx |x| is sign(x)
|
| 17 |
+
VARIABLES = symbols("x y", real=True)
|
| 18 |
+
|
| 19 |
+
ALLOWED_NAMES = {
|
| 20 |
+
"sin": sin, "cos": cos, "tan": tan, "asin": asin, "acos": acos, "atan": atan,
|
| 21 |
+
"log": log, "ln": log, "sqrt": sqrt, "abs": Abs, "exp": exp,
|
| 22 |
+
"e": exp(1), "pi": pi, "π": pi,
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def parse_function(expression: str, n_variables: int) -> Expr:
|
| 27 |
+
"""Parse e.g. "x^2 + 3y^2"; raises ValueError with a message for students."""
|
| 28 |
+
if not expression.strip():
|
| 29 |
+
raise ValueError("Enter a function.")
|
| 30 |
+
variables = VARIABLES[:n_variables]
|
| 31 |
+
names = {str(v): v for v in VARIABLES} | ALLOWED_NAMES
|
| 32 |
+
try:
|
| 33 |
+
expr = parse_expr(
|
| 34 |
+
expression,
|
| 35 |
+
local_dict=names,
|
| 36 |
+
transformations=standard_transformations + (implicit_multiplication_application, convert_xor),
|
| 37 |
+
)
|
| 38 |
+
except Exception:
|
| 39 |
+
raise ValueError("This isn't a function sympy can read. Check the brackets and operators.")
|
| 40 |
+
if not isinstance(expr, Expr):
|
| 41 |
+
raise ValueError("This isn't a function sympy can read. Check the brackets and operators.")
|
| 42 |
+
unknown = expr.free_symbols - set(variables)
|
| 43 |
+
if unknown:
|
| 44 |
+
allowed = " and ".join(str(v) for v in variables)
|
| 45 |
+
raise ValueError(f"Unknown name {', '.join(sorted(map(str, unknown)))}: use only {allowed}.")
|
| 46 |
+
return expr
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class Objective:
|
| 50 |
+
"""f with its gradient and Hessian, evaluated at a point or on a grid."""
|
| 51 |
+
|
| 52 |
+
def __init__(self, expr: Expr, n_variables: int):
|
| 53 |
+
variables = VARIABLES[:n_variables]
|
| 54 |
+
self.n = n_variables
|
| 55 |
+
self._f = lambdify(variables, expr, modules="numpy")
|
| 56 |
+
self._gradient = [lambdify(variables, expr.diff(v), modules="numpy") for v in variables]
|
| 57 |
+
self._hessian = [[lambdify(variables, expr.diff(a, b), modules="numpy") for b in variables] for a in variables]
|
| 58 |
+
|
| 59 |
+
def value(self, p: np.ndarray) -> float:
|
| 60 |
+
return float(self._f(*p))
|
| 61 |
+
|
| 62 |
+
def gradient(self, p: np.ndarray) -> np.ndarray:
|
| 63 |
+
return np.array([float(g(*p)) for g in self._gradient])
|
| 64 |
+
|
| 65 |
+
def hessian(self, p: np.ndarray) -> np.ndarray:
|
| 66 |
+
return np.array([[float(h(*p)) for h in row] for row in self._hessian])
|
| 67 |
+
|
| 68 |
+
def values_on(self, *grid: np.ndarray) -> np.ndarray:
|
| 69 |
+
# Constant expressions give a scalar; spread it over the grid
|
| 70 |
+
return np.broadcast_to(np.asarray(self._f(*grid), dtype=float), grid[0].shape)
|
| 71 |
+
|
| 72 |
+
def gradient_on(self, *grid: np.ndarray) -> list[np.ndarray]:
|
| 73 |
+
return [np.broadcast_to(np.asarray(g(*grid), dtype=float), grid[0].shape) for g in self._gradient]
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class SingularHessian(Exception):
|
| 77 |
+
pass
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
# Each optimiser keeps its own state between steps; step(p) returns the next position.
|
| 81 |
+
|
| 82 |
+
class GradientDescent:
|
| 83 |
+
"""Heavy-ball momentum: v ← μv − η∇f(x), x ← x + v."""
|
| 84 |
+
|
| 85 |
+
def __init__(self, objective: Objective, learning_rate: float, momentum: float):
|
| 86 |
+
self.f, self.lr, self.momentum = objective, learning_rate, momentum
|
| 87 |
+
self.v = 0.0
|
| 88 |
+
|
| 89 |
+
def step(self, p: np.ndarray) -> np.ndarray:
|
| 90 |
+
self.v = self.momentum * self.v - self.lr * self.f.gradient(p)
|
| 91 |
+
return p + self.v
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class Nesterov:
|
| 95 |
+
"""Gradient taken at the look-ahead point x + μv."""
|
| 96 |
+
|
| 97 |
+
def __init__(self, objective: Objective, learning_rate: float, momentum: float):
|
| 98 |
+
self.f, self.lr, self.momentum = objective, learning_rate, momentum
|
| 99 |
+
self.v = 0.0
|
| 100 |
+
|
| 101 |
+
def step(self, p: np.ndarray) -> np.ndarray:
|
| 102 |
+
self.v = self.momentum * self.v - self.lr * self.f.gradient(p + self.momentum * self.v)
|
| 103 |
+
return p + self.v
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
class Newton:
|
| 107 |
+
"""x ← x − H⁻¹∇f(x)."""
|
| 108 |
+
|
| 109 |
+
def __init__(self, objective: Objective):
|
| 110 |
+
self.f = objective
|
| 111 |
+
|
| 112 |
+
def step(self, p: np.ndarray) -> np.ndarray:
|
| 113 |
+
h, g = self.f.hessian(p), self.f.gradient(p)
|
| 114 |
+
try:
|
| 115 |
+
delta = np.linalg.solve(h, g)
|
| 116 |
+
except np.linalg.LinAlgError:
|
| 117 |
+
raise SingularHessian()
|
| 118 |
+
# Pyodide's NumPy can't see floating-point errors, so solve returns inf or nan instead of raising
|
| 119 |
+
if np.all(np.isfinite(h)) and np.all(np.isfinite(g)) and not np.all(np.isfinite(delta)):
|
| 120 |
+
raise SingularHessian()
|
| 121 |
+
return p - delta
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
class Adagrad:
|
| 125 |
+
def __init__(self, objective: Objective, learning_rate: float, epsilon: float):
|
| 126 |
+
self.f, self.lr, self.eps = objective, learning_rate, epsilon
|
| 127 |
+
self.v = 0.0 # sum of squared gradients
|
| 128 |
+
|
| 129 |
+
def step(self, p: np.ndarray) -> np.ndarray:
|
| 130 |
+
g = self.f.gradient(p)
|
| 131 |
+
self.v = self.v + g**2
|
| 132 |
+
return p - self.lr / np.sqrt(self.v + self.eps) * g
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
class RMSProp:
|
| 136 |
+
def __init__(self, objective: Objective, learning_rate: float, beta: float, epsilon: float):
|
| 137 |
+
self.f, self.lr, self.beta, self.eps = objective, learning_rate, beta, epsilon
|
| 138 |
+
self.v = 0.0 # moving average of squared gradients
|
| 139 |
+
|
| 140 |
+
def step(self, p: np.ndarray) -> np.ndarray:
|
| 141 |
+
g = self.f.gradient(p)
|
| 142 |
+
self.v = self.beta * self.v + (1 - self.beta) * g**2
|
| 143 |
+
return p - self.lr / np.sqrt(self.v + self.eps) * g
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
class Adadelta:
|
| 147 |
+
def __init__(self, objective: Objective, beta: float, epsilon: float):
|
| 148 |
+
self.f, self.beta, self.eps = objective, beta, epsilon
|
| 149 |
+
self.v = 0.0 # moving average of squared gradients
|
| 150 |
+
self.s = 0.0 # moving average of squared updates
|
| 151 |
+
|
| 152 |
+
def step(self, p: np.ndarray) -> np.ndarray:
|
| 153 |
+
g = self.f.gradient(p)
|
| 154 |
+
self.v = self.beta * self.v + (1 - self.beta) * g**2
|
| 155 |
+
delta = -np.sqrt(self.s + self.eps) / np.sqrt(self.v + self.eps) * g
|
| 156 |
+
self.s = self.beta * self.s + (1 - self.beta) * delta**2
|
| 157 |
+
return p + delta
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
class Adam:
|
| 161 |
+
def __init__(self, objective: Objective, learning_rate: float, beta1: float, beta2: float, epsilon: float):
|
| 162 |
+
self.f, self.lr, self.beta1, self.beta2, self.eps = objective, learning_rate, beta1, beta2, epsilon
|
| 163 |
+
self.m = 0.0 # first moment
|
| 164 |
+
self.v = 0.0 # second moment
|
| 165 |
+
self.t = 0
|
| 166 |
+
|
| 167 |
+
def step(self, p: np.ndarray) -> np.ndarray:
|
| 168 |
+
g = self.f.gradient(p)
|
| 169 |
+
self.t += 1
|
| 170 |
+
self.m = self.beta1 * self.m + (1 - self.beta1) * g
|
| 171 |
+
self.v = self.beta2 * self.v + (1 - self.beta2) * g**2
|
| 172 |
+
m_hat = self.m / (1 - self.beta1**self.t)
|
| 173 |
+
v_hat = self.v / (1 - self.beta2**self.t)
|
| 174 |
+
return p - self.lr / (np.sqrt(v_hat) + self.eps) * m_hat
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
# Ids match backend/src/algorithms.json
|
| 178 |
+
OPTIMISERS = {
|
| 179 |
+
"gradient_descent": GradientDescent,
|
| 180 |
+
"nesterov": Nesterov,
|
| 181 |
+
"newton": Newton,
|
| 182 |
+
"adam": Adam,
|
| 183 |
+
"adagrad": Adagrad,
|
| 184 |
+
"rmsprop": RMSProp,
|
| 185 |
+
"adadelta": Adadelta,
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
# A run that gets further than this from the origin has diverged
|
| 189 |
+
DIVERGENCE_LIMIT = 1e6
|
| 190 |
+
|
| 191 |
+
DIVERGED = "The next point has no finite value or is beyond 1,000,000: the run diverged or left the function's domain."
|
| 192 |
+
SINGULAR = "The Hessian is singular here, so Newton's method can't take a step."
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
class Run:
|
| 196 |
+
"""One optimiser's trajectory, computed as far as it has been asked for."""
|
| 197 |
+
|
| 198 |
+
def __init__(self, objective: Objective, optimiser, start: np.ndarray):
|
| 199 |
+
self.f = objective
|
| 200 |
+
self.optimiser = optimiser
|
| 201 |
+
self.points = [self._describe(start)]
|
| 202 |
+
self.position = start
|
| 203 |
+
# Why the run ended, once it has
|
| 204 |
+
self.stopped: str | None = None
|
| 205 |
+
|
| 206 |
+
def point(self, k: int) -> dict | None:
|
| 207 |
+
"""The point after k iterations, or None if the run stopped before then."""
|
| 208 |
+
with np.errstate(all="ignore"):
|
| 209 |
+
while len(self.points) <= k and self.stopped is None:
|
| 210 |
+
self._advance()
|
| 211 |
+
return self.points[k] if k < len(self.points) else None
|
| 212 |
+
|
| 213 |
+
def _advance(self) -> None:
|
| 214 |
+
try:
|
| 215 |
+
p = self.optimiser.step(self.position)
|
| 216 |
+
except SingularHessian:
|
| 217 |
+
self.stopped = SINGULAR
|
| 218 |
+
return
|
| 219 |
+
if not np.all(np.abs(p) <= DIVERGENCE_LIMIT) or not np.isfinite(self.f.value(p)):
|
| 220 |
+
self.stopped = DIVERGED
|
| 221 |
+
return
|
| 222 |
+
self.position = p
|
| 223 |
+
self.points.append(self._describe(p))
|
| 224 |
+
|
| 225 |
+
def _describe(self, p: np.ndarray) -> dict:
|
| 226 |
+
return {
|
| 227 |
+
"position": p.tolist(),
|
| 228 |
+
"value": self.f.value(p),
|
| 229 |
+
"gradient": self.f.gradient(p).tolist(),
|
| 230 |
+
"hessian": self.f.hessian(p).tolist(),
|
| 231 |
+
}
|
backend/src/manager 2.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
from logic import OPTIMISERS, Objective, Run, parse_function
|
| 7 |
+
|
| 8 |
+
ALGORITHMS = {
|
| 9 |
+
spec["id"]: spec
|
| 10 |
+
for spec in json.loads((Path(__file__).parent / "algorithms.json").read_text())["algorithms"]
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
# Iterations a run can take before stepping stops
|
| 14 |
+
MAX_ITERATIONS = 1000
|
| 15 |
+
# Sample points for the plotted curve (one variable) or surface (two, per side)
|
| 16 |
+
CURVE_POINTS = 401
|
| 17 |
+
GRID_POINTS = 101
|
| 18 |
+
# Gradient arrows per side
|
| 19 |
+
ARROW_POINTS = 15
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class UserError(Exception):
|
| 23 |
+
"""A mistake the student can fix; its message is shown in the UI."""
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _json_values(values: np.ndarray) -> list:
|
| 27 |
+
"""Values as JSON numbers, with null where f has no finite value."""
|
| 28 |
+
return [float(v) if np.isfinite(v) else None for v in values]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class Manager:
|
| 32 |
+
def __init__(self):
|
| 33 |
+
self._objective: Objective | None = None
|
| 34 |
+
self._runs: list[Run] = []
|
| 35 |
+
|
| 36 |
+
def handle_set_function(self, args: dict) -> None:
|
| 37 |
+
"""{variables: 1 | 2, expression}; clears the runs."""
|
| 38 |
+
self._objective = None
|
| 39 |
+
self._runs = []
|
| 40 |
+
n = int(args["variables"])
|
| 41 |
+
try:
|
| 42 |
+
expr = parse_function(str(args["expression"]), n)
|
| 43 |
+
except ValueError as e:
|
| 44 |
+
raise UserError(str(e))
|
| 45 |
+
try:
|
| 46 |
+
self._objective = Objective(expr, n)
|
| 47 |
+
except Exception:
|
| 48 |
+
raise UserError("This function can't be differentiated here. Try writing it another way.")
|
| 49 |
+
|
| 50 |
+
def handle_function_values(self, args: dict) -> dict | None:
|
| 51 |
+
"""f sampled over the plot's {x_range, y_range}; None with no function."""
|
| 52 |
+
f = self._objective
|
| 53 |
+
if f is None:
|
| 54 |
+
return None
|
| 55 |
+
with np.errstate(all="ignore"):
|
| 56 |
+
if f.n == 1:
|
| 57 |
+
x = np.linspace(*args["x_range"], CURVE_POINTS)
|
| 58 |
+
return {"x": x.tolist(), "f": _json_values(f.values_on(x))}
|
| 59 |
+
|
| 60 |
+
x = np.linspace(*args["x_range"], GRID_POINTS)
|
| 61 |
+
y = np.linspace(*args["y_range"], GRID_POINTS)
|
| 62 |
+
z = f.values_on(*np.meshgrid(x, y))
|
| 63 |
+
ax, ay = np.meshgrid(np.linspace(*args["x_range"], ARROW_POINTS), np.linspace(*args["y_range"], ARROW_POINTS))
|
| 64 |
+
u, v = f.gradient_on(ax, ay)
|
| 65 |
+
return {
|
| 66 |
+
"x": x.tolist(),
|
| 67 |
+
"y": y.tolist(),
|
| 68 |
+
# Rows follow y, columns follow x
|
| 69 |
+
"f": [_json_values(row) for row in z],
|
| 70 |
+
"arrows": {
|
| 71 |
+
"x": ax.ravel().tolist(),
|
| 72 |
+
"y": ay.ravel().tolist(),
|
| 73 |
+
"u": _json_values(u.ravel()),
|
| 74 |
+
"v": _json_values(v.ravel()),
|
| 75 |
+
},
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
def handle_set_runs(self, args: dict) -> None:
|
| 79 |
+
"""{runs: [{algorithm, start, params}]}: the optimisers to step together."""
|
| 80 |
+
self._runs = []
|
| 81 |
+
f = self._objective
|
| 82 |
+
if f is None:
|
| 83 |
+
raise ValueError("Function not set")
|
| 84 |
+
runs = []
|
| 85 |
+
for run in args["runs"]:
|
| 86 |
+
spec = ALGORITHMS[run["algorithm"]]
|
| 87 |
+
given = dict(run.get("params", {}))
|
| 88 |
+
params = {p["name"]: float(given.get(p["name"], p["default"])) for p in spec["params"]}
|
| 89 |
+
start = np.array(run["start"], dtype=float)
|
| 90 |
+
if start.shape != (f.n,):
|
| 91 |
+
raise ValueError(f"Start point needs {f.n} coordinates, got {start.tolist()}")
|
| 92 |
+
with np.errstate(all="ignore"):
|
| 93 |
+
if not np.isfinite(f.value(start)):
|
| 94 |
+
point = ", ".join(f"{c:g}" for c in start)
|
| 95 |
+
raise UserError(f"The function has no finite value at the initial point ({point}).")
|
| 96 |
+
runs.append(Run(f, OPTIMISERS[spec["id"]](f, **params), start))
|
| 97 |
+
self._runs = runs
|
| 98 |
+
|
| 99 |
+
def handle_state(self, args: dict) -> dict | None:
|
| 100 |
+
"""Every run after {iteration} iterations; None once all have stopped.
|
| 101 |
+
|
| 102 |
+
"stopped" says why a run ended, at its last point and after it (where
|
| 103 |
+
point is None). It is None while the run carries on.
|
| 104 |
+
"""
|
| 105 |
+
k = int(args["iteration"])
|
| 106 |
+
if not self._runs or k > MAX_ITERATIONS:
|
| 107 |
+
return None
|
| 108 |
+
points = [run.point(k) for run in self._runs]
|
| 109 |
+
if all(p is None for p in points):
|
| 110 |
+
return None
|
| 111 |
+
# Look one iteration ahead to know whether this is a run's last point
|
| 112 |
+
ended = [p is None or run.point(k + 1) is None for run, p in zip(self._runs, points)]
|
| 113 |
+
return {
|
| 114 |
+
"runs": [
|
| 115 |
+
{"point": p, "stopped": run.stopped if end else None}
|
| 116 |
+
for run, p, end in zip(self._runs, points, ended)
|
| 117 |
+
]
|
| 118 |
+
}
|
backend/src/manager.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
from logic import OPTIMISERS, Objective, Run, parse_function
|
| 7 |
+
|
| 8 |
+
ALGORITHMS = {
|
| 9 |
+
spec["id"]: spec
|
| 10 |
+
for spec in json.loads((Path(__file__).parent / "algorithms.json").read_text())["algorithms"]
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
# Iterations a run can take before stepping stops
|
| 14 |
+
MAX_ITERATIONS = 1000
|
| 15 |
+
# Sample points for the plotted curve (one variable) or surface (two, per side)
|
| 16 |
+
CURVE_POINTS = 401
|
| 17 |
+
GRID_POINTS = 101
|
| 18 |
+
# Gradient arrows per side
|
| 19 |
+
ARROW_POINTS = 15
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class UserError(Exception):
|
| 23 |
+
"""A mistake the student can fix; its message is shown in the UI."""
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _json_values(values: np.ndarray) -> list:
|
| 27 |
+
"""Values as JSON numbers, with null where f has no finite value."""
|
| 28 |
+
return [float(v) if np.isfinite(v) else None for v in values]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class Manager:
|
| 32 |
+
def __init__(self):
|
| 33 |
+
self._objective: Objective | None = None
|
| 34 |
+
self._runs: list[Run] = []
|
| 35 |
+
|
| 36 |
+
def handle_set_function(self, args: dict) -> None:
|
| 37 |
+
"""{variables: 1 | 2, expression}; clears the runs."""
|
| 38 |
+
self._objective = None
|
| 39 |
+
self._runs = []
|
| 40 |
+
n = int(args["variables"])
|
| 41 |
+
try:
|
| 42 |
+
expr = parse_function(str(args["expression"]), n)
|
| 43 |
+
except ValueError as e:
|
| 44 |
+
raise UserError(str(e))
|
| 45 |
+
try:
|
| 46 |
+
self._objective = Objective(expr, n)
|
| 47 |
+
except Exception:
|
| 48 |
+
raise UserError("This function can't be differentiated here. Try writing it another way.")
|
| 49 |
+
|
| 50 |
+
def handle_function_values(self, args: dict) -> dict | None:
|
| 51 |
+
"""f sampled over the plot's {x_range, y_range}; None with no function."""
|
| 52 |
+
f = self._objective
|
| 53 |
+
if f is None:
|
| 54 |
+
return None
|
| 55 |
+
with np.errstate(all="ignore"):
|
| 56 |
+
if f.n == 1:
|
| 57 |
+
x = np.linspace(*args["x_range"], CURVE_POINTS)
|
| 58 |
+
return {"x": x.tolist(), "f": _json_values(f.values_on(x))}
|
| 59 |
+
|
| 60 |
+
x = np.linspace(*args["x_range"], GRID_POINTS)
|
| 61 |
+
y = np.linspace(*args["y_range"], GRID_POINTS)
|
| 62 |
+
z = f.values_on(*np.meshgrid(x, y))
|
| 63 |
+
ax, ay = np.meshgrid(np.linspace(*args["x_range"], ARROW_POINTS), np.linspace(*args["y_range"], ARROW_POINTS))
|
| 64 |
+
u, v = f.gradient_on(ax, ay)
|
| 65 |
+
return {
|
| 66 |
+
"x": x.tolist(),
|
| 67 |
+
"y": y.tolist(),
|
| 68 |
+
# Rows follow y, columns follow x
|
| 69 |
+
"f": [_json_values(row) for row in z],
|
| 70 |
+
"arrows": {
|
| 71 |
+
"x": ax.ravel().tolist(),
|
| 72 |
+
"y": ay.ravel().tolist(),
|
| 73 |
+
"u": _json_values(u.ravel()),
|
| 74 |
+
"v": _json_values(v.ravel()),
|
| 75 |
+
},
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
def handle_set_runs(self, args: dict) -> None:
|
| 79 |
+
"""{runs: [{algorithm, start, params}]}: the optimisers to step together."""
|
| 80 |
+
self._runs = []
|
| 81 |
+
f = self._objective
|
| 82 |
+
if f is None:
|
| 83 |
+
raise ValueError("Function not set")
|
| 84 |
+
runs = []
|
| 85 |
+
for run in args["runs"]:
|
| 86 |
+
spec = ALGORITHMS[run["algorithm"]]
|
| 87 |
+
given = dict(run.get("params", {}))
|
| 88 |
+
params = {p["name"]: float(given.get(p["name"], p["default"])) for p in spec["params"]}
|
| 89 |
+
start = np.array(run["start"], dtype=float)
|
| 90 |
+
if start.shape != (f.n,):
|
| 91 |
+
raise ValueError(f"Start point needs {f.n} coordinates, got {start.tolist()}")
|
| 92 |
+
with np.errstate(all="ignore"):
|
| 93 |
+
if not np.isfinite(f.value(start)):
|
| 94 |
+
point = ", ".join(f"{c:g}" for c in start)
|
| 95 |
+
raise UserError(f"The function has no finite value at the initial point ({point}).")
|
| 96 |
+
runs.append(Run(f, OPTIMISERS[spec["id"]](f, **params), start))
|
| 97 |
+
self._runs = runs
|
| 98 |
+
|
| 99 |
+
def handle_state(self, args: dict) -> dict | None:
|
| 100 |
+
"""Every run after {iteration} iterations; None once all have stopped.
|
| 101 |
+
|
| 102 |
+
"stopped" says why a run ended, at its last point and after it (where
|
| 103 |
+
point is None). It is None while the run carries on.
|
| 104 |
+
"""
|
| 105 |
+
k = int(args["iteration"])
|
| 106 |
+
if not self._runs or k > MAX_ITERATIONS:
|
| 107 |
+
return None
|
| 108 |
+
points = [run.point(k) for run in self._runs]
|
| 109 |
+
if all(p is None for p in points):
|
| 110 |
+
return None
|
| 111 |
+
# Look one iteration ahead to know whether this is a run's last point
|
| 112 |
+
ended = [p is None or run.point(k + 1) is None for run, p in zip(self._runs, points)]
|
| 113 |
+
return {
|
| 114 |
+
"runs": [
|
| 115 |
+
{"point": p, "stopped": run.stopped if end else None}
|
| 116 |
+
for run, p, end in zip(self._runs, points, ended)
|
| 117 |
+
]
|
| 118 |
+
}
|
backend/tests/test_contract.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Replay each contract example in ../contract against a fresh Manager.
|
| 2 |
+
|
| 3 |
+
Each example lists calls in order, keyed by method name. A call has "args"
|
| 4 |
+
(omitted for methods that take none) and either "result" (the expected return
|
| 5 |
+
value as JSON; floats are compared to 6 significant figures) or "error" (a
|
| 6 |
+
UserError of "kind": "user", whose text contains "message" if given). This
|
| 7 |
+
mirrors what @elvis/pyodide's worker sends to the UI.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import json
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
import pytest
|
| 14 |
+
|
| 15 |
+
from manager import Manager
|
| 16 |
+
|
| 17 |
+
CONTRACT_DIR = Path(__file__).parent.parent / "contract"
|
| 18 |
+
EXAMPLES = sorted(CONTRACT_DIR.glob("*.json"))
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def same(actual, expected) -> bool:
|
| 22 |
+
"""Equal JSON values, with floats compared approximately."""
|
| 23 |
+
if isinstance(expected, float) or isinstance(actual, float):
|
| 24 |
+
return isinstance(actual, (int, float)) and actual == pytest.approx(expected, rel=1e-6, abs=1e-12)
|
| 25 |
+
if isinstance(expected, dict):
|
| 26 |
+
return isinstance(actual, dict) and actual.keys() == expected.keys() and all(same(actual[k], expected[k]) for k in expected)
|
| 27 |
+
if isinstance(expected, list):
|
| 28 |
+
return isinstance(actual, list) and len(actual) == len(expected) and all(same(a, e) for a, e in zip(actual, expected))
|
| 29 |
+
return actual == expected
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@pytest.mark.parametrize("path", EXAMPLES, ids=[p.stem for p in EXAMPLES])
|
| 33 |
+
def test_contract_example(path: Path) -> None:
|
| 34 |
+
example = json.loads(path.read_text())
|
| 35 |
+
manager = Manager()
|
| 36 |
+
|
| 37 |
+
for method, call in example["calls"].items():
|
| 38 |
+
fn = getattr(manager, method)
|
| 39 |
+
try:
|
| 40 |
+
data = fn(call["args"]) if "args" in call else fn()
|
| 41 |
+
except Exception as e:
|
| 42 |
+
# The worker treats only UserError as a user error; anything else is a bug
|
| 43 |
+
if type(e).__name__ != "UserError":
|
| 44 |
+
raise
|
| 45 |
+
assert "error" in call, f"{method} raised UserError: {e}"
|
| 46 |
+
assert call["error"]["kind"] == "user"
|
| 47 |
+
assert call["error"].get("message", "") in str(e)
|
| 48 |
+
continue
|
| 49 |
+
|
| 50 |
+
assert "error" not in call, f"{method} returned {data!r} instead of raising UserError"
|
| 51 |
+
# Results must be plain JSON values (no NumPy types) to reach the UI unchanged
|
| 52 |
+
actual = json.loads(json.dumps(data))
|
| 53 |
+
assert same(actual, call["result"]), f"{method} returned {actual!r}"
|
backend/tests/test_logic 2.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import inspect
|
| 2 |
+
import json
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import pytest
|
| 7 |
+
|
| 8 |
+
from logic import DIVERGED, OPTIMISERS, SINGULAR, Objective, Run, parse_function
|
| 9 |
+
from manager import MAX_ITERATIONS, Manager, UserError
|
| 10 |
+
|
| 11 |
+
SPEC = json.loads((Path(__file__).parent.parent / "src" / "algorithms.json").read_text())["algorithms"]
|
| 12 |
+
DEFAULTS = {spec["id"]: {p["name"]: p["default"] for p in spec["params"]} for spec in SPEC}
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def run(expression: str, algorithm: str, start: list[float], **params) -> Run:
|
| 16 |
+
f = Objective(parse_function(expression, len(start)), len(start))
|
| 17 |
+
return Run(f, OPTIMISERS[algorithm](f, **(DEFAULTS[algorithm] | params)), np.array(start, dtype=float))
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def test_spec_matches_optimisers():
|
| 21 |
+
assert [spec["id"] for spec in SPEC] == list(OPTIMISERS)
|
| 22 |
+
for spec in SPEC:
|
| 23 |
+
names = list(inspect.signature(OPTIMISERS[spec["id"]]).parameters)[1:]
|
| 24 |
+
assert names == [p["name"] for p in spec["params"]], spec["id"]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@pytest.mark.parametrize("expression, n, expected", [
|
| 28 |
+
("x^2 - 0.1cos(20x)", 1, 0.81 - 0.1 * np.cos(18)),
|
| 29 |
+
("2x y + e", 2, 2 * 0.9 * -0.5 + np.e),
|
| 30 |
+
("sqrt(abs(x)) + ln(exp(y))", 2, np.sqrt(0.9) - 0.5),
|
| 31 |
+
])
|
| 32 |
+
def test_parse_function(expression, n, expected):
|
| 33 |
+
f = Objective(parse_function(expression, n), n)
|
| 34 |
+
point = np.array([0.9, -0.5][:n])
|
| 35 |
+
assert f.value(point) == pytest.approx(expected)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@pytest.mark.parametrize("expression, message", [
|
| 39 |
+
("", "Enter a function"),
|
| 40 |
+
("x^2 + (", "can read"),
|
| 41 |
+
("x + z", "Unknown name z"),
|
| 42 |
+
])
|
| 43 |
+
def test_parse_function_errors(expression, message):
|
| 44 |
+
with pytest.raises(ValueError, match=message):
|
| 45 |
+
parse_function(expression, 2)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def test_gradient_and_hessian():
|
| 49 |
+
f = Objective(parse_function("x^2 y + y^3", 2), 2)
|
| 50 |
+
p = np.array([1.0, 2.0])
|
| 51 |
+
assert f.gradient(p).tolist() == [4.0, 13.0]
|
| 52 |
+
assert f.hessian(p).tolist() == [[4.0, 2.0], [2.0, 12.0]]
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def test_gradient_descent_on_a_quadratic():
|
| 56 |
+
# x_k = (1 - 2η)^k x_0
|
| 57 |
+
r = run("x^2", "gradient_descent", [1.0], learning_rate=0.1)
|
| 58 |
+
assert r.point(10)["position"][0] == pytest.approx(0.8**10)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def test_momentum_overshoots_and_nesterov_damps_it():
|
| 62 |
+
plain = run("x^2", "gradient_descent", [1.0], learning_rate=0.1, momentum=0.9)
|
| 63 |
+
nesterov = run("x^2", "nesterov", [1.0], learning_rate=0.1, momentum=0.9)
|
| 64 |
+
assert min(plain.point(k)["position"][0] for k in range(20)) < -0.5
|
| 65 |
+
assert abs(nesterov.point(20)["position"][0]) < abs(plain.point(20)["position"][0])
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def test_newton_solves_a_quadratic_in_one_step():
|
| 69 |
+
r = run("(x - 1)^2 + 3(y + 2)^2 + x y", "newton", [5.0, 5.0])
|
| 70 |
+
assert r.point(1)["gradient"] == pytest.approx([0, 0], abs=1e-12)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@pytest.mark.parametrize("algorithm", [spec["id"] for spec in SPEC])
|
| 74 |
+
def test_every_optimiser_goes_downhill_on_a_bowl(algorithm):
|
| 75 |
+
r = run("x^2 + 3y^2", algorithm, [0.8, -0.6])
|
| 76 |
+
assert r.point(50)["value"] < r.point(0)["value"]
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def test_divergence_stops_the_run():
|
| 80 |
+
r = run("x^2", "gradient_descent", [1.0], learning_rate=1.5)
|
| 81 |
+
# |x| doubles each step, so it passes 1e6 after 20 steps
|
| 82 |
+
assert r.point(20) is None and r.point(19) is not None
|
| 83 |
+
assert r.stopped == DIVERGED
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def test_leaving_the_domain_stops_the_run():
|
| 87 |
+
r = run("sqrt(x)", "gradient_descent", [0.5], learning_rate=1.0)
|
| 88 |
+
assert r.point(3) is None
|
| 89 |
+
assert r.stopped == DIVERGED
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def test_singular_hessian_stops_newton():
|
| 93 |
+
r = run("x^3", "newton", [0.0])
|
| 94 |
+
assert r.point(1) is None
|
| 95 |
+
assert r.stopped == SINGULAR
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def test_singular_hessian_stops_newton_in_pyodide(monkeypatch):
|
| 99 |
+
# Pyodide's solve returns inf rather than raising LinAlgError
|
| 100 |
+
monkeypatch.setattr(np.linalg, "solve", lambda a, b: np.full_like(b, np.inf))
|
| 101 |
+
r = run("x^2 + 0y", "newton", [1.0, 1.0])
|
| 102 |
+
assert r.point(1) is None
|
| 103 |
+
assert r.stopped == SINGULAR
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def test_points_are_cached_and_asked_for_in_any_order():
|
| 107 |
+
r = run("x^2 + y^2", "adam", [1.0, 1.0])
|
| 108 |
+
late = r.point(30)
|
| 109 |
+
assert r.point(3) == r.points[3]
|
| 110 |
+
assert r.point(30) == late
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def manager_with(expression: str, *runs: dict) -> Manager:
|
| 114 |
+
m = Manager()
|
| 115 |
+
m.handle_set_function({"variables": len(runs[0]["start"]), "expression": expression})
|
| 116 |
+
m.handle_set_runs({"runs": list(runs)})
|
| 117 |
+
return m
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def test_state_past_the_last_iteration():
|
| 121 |
+
m = manager_with("x^2", {"algorithm": "gradient_descent", "start": [1.0], "params": {}})
|
| 122 |
+
assert m.handle_state({"iteration": MAX_ITERATIONS}) is not None
|
| 123 |
+
assert m.handle_state({"iteration": MAX_ITERATIONS + 1}) is None
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def test_state_ends_when_every_run_has_stopped():
|
| 127 |
+
m = manager_with(
|
| 128 |
+
"x^3 + x^2",
|
| 129 |
+
{"algorithm": "gradient_descent", "start": [1.0], "params": {"learning_rate": 1.5}},
|
| 130 |
+
{"algorithm": "newton", "start": [-1/3], "params": {}}, # f'' = 0 here
|
| 131 |
+
)
|
| 132 |
+
states = [m.handle_state({"iteration": k}) for k in range(MAX_ITERATIONS)]
|
| 133 |
+
end = states.index(None)
|
| 134 |
+
assert 1 < end and all(s is None for s in states[end:])
|
| 135 |
+
# Newton stops first and says why while gradient descent carries on
|
| 136 |
+
assert states[1]["runs"][1] == {"point": None, "stopped": SINGULAR}
|
| 137 |
+
assert states[1]["runs"][0]["point"] is not None
|
| 138 |
+
# Each run says why at its last point
|
| 139 |
+
assert states[0]["runs"][1]["stopped"] == SINGULAR
|
| 140 |
+
assert states[end - 2]["runs"][0]["stopped"] is None
|
| 141 |
+
assert states[end - 1]["runs"][0]["stopped"] == DIVERGED
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def test_start_point_outside_the_domain():
|
| 145 |
+
m = Manager()
|
| 146 |
+
m.handle_set_function({"variables": 1, "expression": "log(x)"})
|
| 147 |
+
with pytest.raises(UserError, match="no finite value at the initial point"):
|
| 148 |
+
m.handle_set_runs({"runs": [{"algorithm": "adam", "start": [-1.0], "params": {}}]})
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def test_failed_runs_replace_the_old_ones():
|
| 152 |
+
m = manager_with("log(x)", {"algorithm": "adam", "start": [1.0], "params": {}})
|
| 153 |
+
with pytest.raises(UserError):
|
| 154 |
+
m.handle_set_runs({"runs": [{"algorithm": "adam", "start": [-1.0], "params": {}}]})
|
| 155 |
+
assert m.handle_state({"iteration": 0}) is None
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def test_function_values_one_variable():
|
| 159 |
+
m = Manager()
|
| 160 |
+
m.handle_set_function({"variables": 1, "expression": "log(x)"})
|
| 161 |
+
values = m.handle_function_values({"x_range": [-1, 1], "y_range": [0, 1]})
|
| 162 |
+
assert len(values["x"]) == len(values["f"])
|
| 163 |
+
assert values["f"][0] is None and values["f"][-1] == 0.0
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def test_function_values_two_variables():
|
| 167 |
+
m = Manager()
|
| 168 |
+
m.handle_set_function({"variables": 2, "expression": "x + 2y"})
|
| 169 |
+
values = m.handle_function_values({"x_range": [0, 1], "y_range": [0, 2]})
|
| 170 |
+
assert values["f"][-1][0] == 4.0 # rows follow y
|
| 171 |
+
assert set(values["arrows"]["u"]) == {1.0} and set(values["arrows"]["v"]) == {2.0}
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def test_no_function_values_without_a_function():
|
| 175 |
+
assert Manager().handle_function_values({"x_range": [0, 1], "y_range": [0, 1]}) is None
|
backend/tests/test_logic.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import inspect
|
| 2 |
+
import json
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import pytest
|
| 7 |
+
|
| 8 |
+
from logic import DIVERGED, OPTIMISERS, SINGULAR, Objective, Run, parse_function
|
| 9 |
+
from manager import MAX_ITERATIONS, Manager, UserError
|
| 10 |
+
|
| 11 |
+
SPEC = json.loads((Path(__file__).parent.parent / "src" / "algorithms.json").read_text())["algorithms"]
|
| 12 |
+
DEFAULTS = {spec["id"]: {p["name"]: p["default"] for p in spec["params"]} for spec in SPEC}
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def run(expression: str, algorithm: str, start: list[float], **params) -> Run:
|
| 16 |
+
f = Objective(parse_function(expression, len(start)), len(start))
|
| 17 |
+
return Run(f, OPTIMISERS[algorithm](f, **(DEFAULTS[algorithm] | params)), np.array(start, dtype=float))
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def test_spec_matches_optimisers():
|
| 21 |
+
assert [spec["id"] for spec in SPEC] == list(OPTIMISERS)
|
| 22 |
+
for spec in SPEC:
|
| 23 |
+
names = list(inspect.signature(OPTIMISERS[spec["id"]]).parameters)[1:]
|
| 24 |
+
assert names == [p["name"] for p in spec["params"]], spec["id"]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@pytest.mark.parametrize("expression, n, expected", [
|
| 28 |
+
("x^2 - 0.1cos(20x)", 1, 0.81 - 0.1 * np.cos(18)),
|
| 29 |
+
("2x y + e", 2, 2 * 0.9 * -0.5 + np.e),
|
| 30 |
+
("sqrt(abs(x)) + ln(exp(y))", 2, np.sqrt(0.9) - 0.5),
|
| 31 |
+
])
|
| 32 |
+
def test_parse_function(expression, n, expected):
|
| 33 |
+
f = Objective(parse_function(expression, n), n)
|
| 34 |
+
point = np.array([0.9, -0.5][:n])
|
| 35 |
+
assert f.value(point) == pytest.approx(expected)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@pytest.mark.parametrize("expression, message", [
|
| 39 |
+
("", "Enter a function"),
|
| 40 |
+
("x^2 + (", "can read"),
|
| 41 |
+
("x + z", "Unknown name z"),
|
| 42 |
+
])
|
| 43 |
+
def test_parse_function_errors(expression, message):
|
| 44 |
+
with pytest.raises(ValueError, match=message):
|
| 45 |
+
parse_function(expression, 2)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def test_gradient_and_hessian():
|
| 49 |
+
f = Objective(parse_function("x^2 y + y^3", 2), 2)
|
| 50 |
+
p = np.array([1.0, 2.0])
|
| 51 |
+
assert f.gradient(p).tolist() == [4.0, 13.0]
|
| 52 |
+
assert f.hessian(p).tolist() == [[4.0, 2.0], [2.0, 12.0]]
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def test_gradient_descent_on_a_quadratic():
|
| 56 |
+
# x_k = (1 - 2η)^k x_0
|
| 57 |
+
r = run("x^2", "gradient_descent", [1.0], learning_rate=0.1)
|
| 58 |
+
assert r.point(10)["position"][0] == pytest.approx(0.8**10)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def test_momentum_overshoots_and_nesterov_damps_it():
|
| 62 |
+
plain = run("x^2", "gradient_descent", [1.0], learning_rate=0.1, momentum=0.9)
|
| 63 |
+
nesterov = run("x^2", "nesterov", [1.0], learning_rate=0.1, momentum=0.9)
|
| 64 |
+
assert min(plain.point(k)["position"][0] for k in range(20)) < -0.5
|
| 65 |
+
assert abs(nesterov.point(20)["position"][0]) < abs(plain.point(20)["position"][0])
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def test_newton_solves_a_quadratic_in_one_step():
|
| 69 |
+
r = run("(x - 1)^2 + 3(y + 2)^2 + x y", "newton", [5.0, 5.0])
|
| 70 |
+
assert r.point(1)["gradient"] == pytest.approx([0, 0], abs=1e-12)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@pytest.mark.parametrize("algorithm", [spec["id"] for spec in SPEC])
|
| 74 |
+
def test_every_optimiser_goes_downhill_on_a_bowl(algorithm):
|
| 75 |
+
r = run("x^2 + 3y^2", algorithm, [0.8, -0.6])
|
| 76 |
+
assert r.point(50)["value"] < r.point(0)["value"]
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def test_divergence_stops_the_run():
|
| 80 |
+
r = run("x^2", "gradient_descent", [1.0], learning_rate=1.5)
|
| 81 |
+
# |x| doubles each step, so it passes 1e6 after 20 steps
|
| 82 |
+
assert r.point(20) is None and r.point(19) is not None
|
| 83 |
+
assert r.stopped == DIVERGED
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def test_leaving_the_domain_stops_the_run():
|
| 87 |
+
r = run("sqrt(x)", "gradient_descent", [0.5], learning_rate=1.0)
|
| 88 |
+
assert r.point(3) is None
|
| 89 |
+
assert r.stopped == DIVERGED
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def test_singular_hessian_stops_newton():
|
| 93 |
+
r = run("x^3", "newton", [0.0])
|
| 94 |
+
assert r.point(1) is None
|
| 95 |
+
assert r.stopped == SINGULAR
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def test_singular_hessian_stops_newton_in_pyodide(monkeypatch):
|
| 99 |
+
# Pyodide's solve returns inf rather than raising LinAlgError
|
| 100 |
+
monkeypatch.setattr(np.linalg, "solve", lambda a, b: np.full_like(b, np.inf))
|
| 101 |
+
r = run("x^2 + 0y", "newton", [1.0, 1.0])
|
| 102 |
+
assert r.point(1) is None
|
| 103 |
+
assert r.stopped == SINGULAR
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def test_points_are_cached_and_asked_for_in_any_order():
|
| 107 |
+
r = run("x^2 + y^2", "adam", [1.0, 1.0])
|
| 108 |
+
late = r.point(30)
|
| 109 |
+
assert r.point(3) == r.points[3]
|
| 110 |
+
assert r.point(30) == late
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def manager_with(expression: str, *runs: dict) -> Manager:
|
| 114 |
+
m = Manager()
|
| 115 |
+
m.handle_set_function({"variables": len(runs[0]["start"]), "expression": expression})
|
| 116 |
+
m.handle_set_runs({"runs": list(runs)})
|
| 117 |
+
return m
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def test_state_past_the_last_iteration():
|
| 121 |
+
m = manager_with("x^2", {"algorithm": "gradient_descent", "start": [1.0], "params": {}})
|
| 122 |
+
assert m.handle_state({"iteration": MAX_ITERATIONS}) is not None
|
| 123 |
+
assert m.handle_state({"iteration": MAX_ITERATIONS + 1}) is None
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def test_state_ends_when_every_run_has_stopped():
|
| 127 |
+
m = manager_with(
|
| 128 |
+
"x^3 + x^2",
|
| 129 |
+
{"algorithm": "gradient_descent", "start": [1.0], "params": {"learning_rate": 1.5}},
|
| 130 |
+
{"algorithm": "newton", "start": [-1/3], "params": {}}, # f'' = 0 here
|
| 131 |
+
)
|
| 132 |
+
states = [m.handle_state({"iteration": k}) for k in range(MAX_ITERATIONS)]
|
| 133 |
+
end = states.index(None)
|
| 134 |
+
assert 1 < end and all(s is None for s in states[end:])
|
| 135 |
+
# Newton stops first and says why while gradient descent carries on
|
| 136 |
+
assert states[1]["runs"][1] == {"point": None, "stopped": SINGULAR}
|
| 137 |
+
assert states[1]["runs"][0]["point"] is not None
|
| 138 |
+
# Each run says why at its last point
|
| 139 |
+
assert states[0]["runs"][1]["stopped"] == SINGULAR
|
| 140 |
+
assert states[end - 2]["runs"][0]["stopped"] is None
|
| 141 |
+
assert states[end - 1]["runs"][0]["stopped"] == DIVERGED
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def test_start_point_outside_the_domain():
|
| 145 |
+
m = Manager()
|
| 146 |
+
m.handle_set_function({"variables": 1, "expression": "log(x)"})
|
| 147 |
+
with pytest.raises(UserError, match="no finite value at the initial point"):
|
| 148 |
+
m.handle_set_runs({"runs": [{"algorithm": "adam", "start": [-1.0], "params": {}}]})
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def test_failed_runs_replace_the_old_ones():
|
| 152 |
+
m = manager_with("log(x)", {"algorithm": "adam", "start": [1.0], "params": {}})
|
| 153 |
+
with pytest.raises(UserError):
|
| 154 |
+
m.handle_set_runs({"runs": [{"algorithm": "adam", "start": [-1.0], "params": {}}]})
|
| 155 |
+
assert m.handle_state({"iteration": 0}) is None
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def test_function_values_one_variable():
|
| 159 |
+
m = Manager()
|
| 160 |
+
m.handle_set_function({"variables": 1, "expression": "log(x)"})
|
| 161 |
+
values = m.handle_function_values({"x_range": [-1, 1], "y_range": [0, 1]})
|
| 162 |
+
assert len(values["x"]) == len(values["f"])
|
| 163 |
+
assert values["f"][0] is None and values["f"][-1] == 0.0
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def test_function_values_two_variables():
|
| 167 |
+
m = Manager()
|
| 168 |
+
m.handle_set_function({"variables": 2, "expression": "x + 2y"})
|
| 169 |
+
values = m.handle_function_values({"x_range": [0, 1], "y_range": [0, 2]})
|
| 170 |
+
assert values["f"][-1][0] == 4.0 # rows follow y
|
| 171 |
+
assert set(values["arrows"]["u"]) == {1.0} and set(values["arrows"]["v"]) == {2.0}
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def test_no_function_values_without_a_function():
|
| 175 |
+
assert Manager().handle_function_values({"x_range": [0, 1], "y_range": [0, 1]}) is None
|
backend/uv.lock
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version = 1
|
| 2 |
+
revision = 5
|
| 3 |
+
requires-python = ">=3.12"
|
| 4 |
+
|
| 5 |
+
[[package]]
|
| 6 |
+
name = "backend"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
source = { virtual = "." }
|
| 9 |
+
dependencies = [
|
| 10 |
+
{ name = "numpy" },
|
| 11 |
+
{ name = "sympy" },
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
[package.dev-dependencies]
|
| 15 |
+
dev = [
|
| 16 |
+
{ name = "pytest" },
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
[package.metadata]
|
| 20 |
+
requires-dist = [
|
| 21 |
+
{ name = "numpy", specifier = ">=2.4.0" },
|
| 22 |
+
{ name = "sympy", specifier = ">=1.14.0" },
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
+
[package.metadata.requires-dev]
|
| 26 |
+
dev = [{ name = "pytest", specifier = ">=9.2.0" }]
|
| 27 |
+
|
| 28 |
+
[[package]]
|
| 29 |
+
name = "colorama"
|
| 30 |
+
version = "0.4.6"
|
| 31 |
+
source = { registry = "https://pypi.org/simple" }
|
| 32 |
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
| 33 |
+
wheels = [
|
| 34 |
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
[[package]]
|
| 38 |
+
name = "iniconfig"
|
| 39 |
+
version = "2.3.1"
|
| 40 |
+
source = { registry = "https://pypi.org/simple" }
|
| 41 |
+
sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" }
|
| 42 |
+
wheels = [
|
| 43 |
+
{ url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" },
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
[[package]]
|
| 47 |
+
name = "mpmath"
|
| 48 |
+
version = "1.3.0"
|
| 49 |
+
source = { registry = "https://pypi.org/simple" }
|
| 50 |
+
sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
|
| 51 |
+
wheels = [
|
| 52 |
+
{ url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
[[package]]
|
| 56 |
+
name = "numpy"
|
| 57 |
+
version = "2.4.0"
|
| 58 |
+
source = { registry = "https://pypi.org/simple" }
|
| 59 |
+
sdist = { url = "https://files.pythonhosted.org/packages/a4/7a/6a3d14e205d292b738db449d0de649b373a59edb0d0b4493821d0a3e8718/numpy-2.4.0.tar.gz", hash = "sha256:6e504f7b16118198f138ef31ba24d985b124c2c469fe8467007cf30fd992f934", size = 20685720, upload-time = "2025-12-20T16:18:19.023Z" }
|
| 60 |
+
wheels = [
|
| 61 |
+
{ url = "https://files.pythonhosted.org/packages/8b/ff/f6400ffec95de41c74b8e73df32e3fff1830633193a7b1e409be7fb1bb8c/numpy-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a8b6bb8369abefb8bd1801b054ad50e02b3275c8614dc6e5b0373c305291037", size = 16653117, upload-time = "2025-12-20T16:16:06.709Z" },
|
| 62 |
+
{ url = "https://files.pythonhosted.org/packages/fd/28/6c23e97450035072e8d830a3c411bf1abd1f42c611ff9d29e3d8f55c6252/numpy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e284ca13d5a8367e43734148622caf0b261b275673823593e3e3634a6490f83", size = 12369711, upload-time = "2025-12-20T16:16:08.758Z" },
|
| 63 |
+
{ url = "https://files.pythonhosted.org/packages/bc/af/acbef97b630ab1bb45e6a7d01d1452e4251aa88ce680ac36e56c272120ec/numpy-2.4.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:49ff32b09f5aa0cd30a20c2b39db3e669c845589f2b7fc910365210887e39344", size = 5198355, upload-time = "2025-12-20T16:16:10.902Z" },
|
| 64 |
+
{ url = "https://files.pythonhosted.org/packages/c1/c8/4e0d436b66b826f2e53330adaa6311f5cac9871a5b5c31ad773b27f25a74/numpy-2.4.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:36cbfb13c152b1c7c184ddac43765db8ad672567e7bafff2cc755a09917ed2e6", size = 6545298, upload-time = "2025-12-20T16:16:12.607Z" },
|
| 65 |
+
{ url = "https://files.pythonhosted.org/packages/ef/27/e1f5d144ab54eac34875e79037011d511ac57b21b220063310cb96c80fbc/numpy-2.4.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35ddc8f4914466e6fc954c76527aa91aa763682a4f6d73249ef20b418fe6effb", size = 14398387, upload-time = "2025-12-20T16:16:14.257Z" },
|
| 66 |
+
{ url = "https://files.pythonhosted.org/packages/67/64/4cb909dd5ab09a9a5d086eff9586e69e827b88a5585517386879474f4cf7/numpy-2.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc578891de1db95b2a35001b695451767b580bb45753717498213c5ff3c41d63", size = 16363091, upload-time = "2025-12-20T16:16:17.32Z" },
|
| 67 |
+
{ url = "https://files.pythonhosted.org/packages/9d/9c/8efe24577523ec6809261859737cf117b0eb6fdb655abdfdc81b2e468ce4/numpy-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98e81648e0b36e325ab67e46b5400a7a6d4a22b8a7c8e8bbfe20e7db7906bf95", size = 16176394, upload-time = "2025-12-20T16:16:19.524Z" },
|
| 68 |
+
{ url = "https://files.pythonhosted.org/packages/61/f0/1687441ece7b47a62e45a1f82015352c240765c707928edd8aef875d5951/numpy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d57b5046c120561ba8fa8e4030fbb8b822f3063910fa901ffadf16e2b7128ad6", size = 18287378, upload-time = "2025-12-20T16:16:22.866Z" },
|
| 69 |
+
{ url = "https://files.pythonhosted.org/packages/d3/6f/f868765d44e6fc466467ed810ba9d8d6db1add7d4a748abfa2a4c99a3194/numpy-2.4.0-cp312-cp312-win32.whl", hash = "sha256:92190db305a6f48734d3982f2c60fa30d6b5ee9bff10f2887b930d7b40119f4c", size = 5955432, upload-time = "2025-12-20T16:16:25.06Z" },
|
| 70 |
+
{ url = "https://files.pythonhosted.org/packages/d4/b5/94c1e79fcbab38d1ca15e13777477b2914dd2d559b410f96949d6637b085/numpy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:680060061adb2d74ce352628cb798cfdec399068aa7f07ba9fb818b2b3305f98", size = 12306201, upload-time = "2025-12-20T16:16:26.979Z" },
|
| 71 |
+
{ url = "https://files.pythonhosted.org/packages/70/09/c39dadf0b13bb0768cd29d6a3aaff1fb7c6905ac40e9aaeca26b1c086e06/numpy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:39699233bc72dd482da1415dcb06076e32f60eddc796a796c5fb6c5efce94667", size = 10308234, upload-time = "2025-12-20T16:16:29.417Z" },
|
| 72 |
+
{ url = "https://files.pythonhosted.org/packages/a7/0d/853fd96372eda07c824d24adf02e8bc92bb3731b43a9b2a39161c3667cc4/numpy-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a152d86a3ae00ba5f47b3acf3b827509fd0b6cb7d3259665e63dafbad22a75ea", size = 16649088, upload-time = "2025-12-20T16:16:31.421Z" },
|
| 73 |
+
{ url = "https://files.pythonhosted.org/packages/e3/37/cc636f1f2a9f585434e20a3e6e63422f70bfe4f7f6698e941db52ea1ac9a/numpy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39b19251dec4de8ff8496cd0806cbe27bf0684f765abb1f4809554de93785f2d", size = 12364065, upload-time = "2025-12-20T16:16:33.491Z" },
|
| 74 |
+
{ url = "https://files.pythonhosted.org/packages/ed/69/0b78f37ca3690969beee54103ce5f6021709134e8020767e93ba691a72f1/numpy-2.4.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:009bd0ea12d3c784b6639a8457537016ce5172109e585338e11334f6a7bb88ee", size = 5192640, upload-time = "2025-12-20T16:16:35.636Z" },
|
| 75 |
+
{ url = "https://files.pythonhosted.org/packages/1d/2a/08569f8252abf590294dbb09a430543ec8f8cc710383abfb3e75cc73aeda/numpy-2.4.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5fe44e277225fd3dff6882d86d3d447205d43532c3627313d17e754fb3905a0e", size = 6541556, upload-time = "2025-12-20T16:16:37.276Z" },
|
| 76 |
+
{ url = "https://files.pythonhosted.org/packages/93/e9/a949885a4e177493d61519377952186b6cbfdf1d6002764c664ba28349b5/numpy-2.4.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f935c4493eda9069851058fa0d9e39dbf6286be690066509305e52912714dbb2", size = 14396562, upload-time = "2025-12-20T16:16:38.953Z" },
|
| 77 |
+
{ url = "https://files.pythonhosted.org/packages/99/98/9d4ad53b0e9ef901c2ef1d550d2136f5ac42d3fd2988390a6def32e23e48/numpy-2.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cfa5f29a695cb7438965e6c3e8d06e0416060cf0d709c1b1c1653a939bf5c2a", size = 16351719, upload-time = "2025-12-20T16:16:41.503Z" },
|
| 78 |
+
{ url = "https://files.pythonhosted.org/packages/28/de/5f3711a38341d6e8dd619f6353251a0cdd07f3d6d101a8fd46f4ef87f895/numpy-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba0cb30acd3ef11c94dc27fbfba68940652492bc107075e7ffe23057f9425681", size = 16176053, upload-time = "2025-12-20T16:16:44.552Z" },
|
| 79 |
+
{ url = "https://files.pythonhosted.org/packages/2a/5b/2a3753dc43916501b4183532e7ace862e13211042bceafa253afb5c71272/numpy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60e8c196cd82cbbd4f130b5290007e13e6de3eca79f0d4d38014769d96a7c475", size = 18277859, upload-time = "2025-12-20T16:16:47.174Z" },
|
| 80 |
+
{ url = "https://files.pythonhosted.org/packages/2c/c5/a18bcdd07a941db3076ef489d036ab16d2bfc2eae0cf27e5a26e29189434/numpy-2.4.0-cp313-cp313-win32.whl", hash = "sha256:5f48cb3e88fbc294dc90e215d86fbaf1c852c63dbdb6c3a3e63f45c4b57f7344", size = 5953849, upload-time = "2025-12-20T16:16:49.554Z" },
|
| 81 |
+
{ url = "https://files.pythonhosted.org/packages/4f/f1/719010ff8061da6e8a26e1980cf090412d4f5f8060b31f0c45d77dd67a01/numpy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:a899699294f28f7be8992853c0c60741f16ff199205e2e6cdca155762cbaa59d", size = 12302840, upload-time = "2025-12-20T16:16:51.227Z" },
|
| 82 |
+
{ url = "https://files.pythonhosted.org/packages/f5/5a/b3d259083ed8b4d335270c76966cb6cf14a5d1b69e1a608994ac57a659e6/numpy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:9198f447e1dc5647d07c9a6bbe2063cc0132728cc7175b39dbc796da5b54920d", size = 10308509, upload-time = "2025-12-20T16:16:53.313Z" },
|
| 83 |
+
{ url = "https://files.pythonhosted.org/packages/31/01/95edcffd1bb6c0633df4e808130545c4f07383ab629ac7e316fb44fff677/numpy-2.4.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74623f2ab5cc3f7c886add4f735d1031a1d2be4a4ae63c0546cfd74e7a31ddf6", size = 12491815, upload-time = "2025-12-20T16:16:55.496Z" },
|
| 84 |
+
{ url = "https://files.pythonhosted.org/packages/59/ea/5644b8baa92cc1c7163b4b4458c8679852733fa74ca49c942cfa82ded4e0/numpy-2.4.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:0804a8e4ab070d1d35496e65ffd3cf8114c136a2b81f61dfab0de4b218aacfd5", size = 5320321, upload-time = "2025-12-20T16:16:57.468Z" },
|
| 85 |
+
{ url = "https://files.pythonhosted.org/packages/26/4e/e10938106d70bc21319bd6a86ae726da37edc802ce35a3a71ecdf1fdfe7f/numpy-2.4.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:02a2038eb27f9443a8b266a66911e926566b5a6ffd1a689b588f7f35b81e7dc3", size = 6641635, upload-time = "2025-12-20T16:16:59.379Z" },
|
| 86 |
+
{ url = "https://files.pythonhosted.org/packages/b3/8d/a8828e3eaf5c0b4ab116924df82f24ce3416fa38d0674d8f708ddc6c8aac/numpy-2.4.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1889b3a3f47a7b5bee16bc25a2145bd7cb91897f815ce3499db64c7458b6d91d", size = 14456053, upload-time = "2025-12-20T16:17:01.768Z" },
|
| 87 |
+
{ url = "https://files.pythonhosted.org/packages/68/a1/17d97609d87d4520aa5ae2dcfb32305654550ac6a35effb946d303e594ce/numpy-2.4.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85eef4cb5625c47ee6425c58a3502555e10f45ee973da878ac8248ad58c136f3", size = 16401702, upload-time = "2025-12-20T16:17:04.235Z" },
|
| 88 |
+
{ url = "https://files.pythonhosted.org/packages/18/32/0f13c1b2d22bea1118356b8b963195446f3af124ed7a5adfa8fdecb1b6ca/numpy-2.4.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6dc8b7e2f4eb184b37655195f421836cfae6f58197b67e3ffc501f1333d993fa", size = 16242493, upload-time = "2025-12-20T16:17:06.856Z" },
|
| 89 |
+
{ url = "https://files.pythonhosted.org/packages/ae/23/48f21e3d309fbc137c068a1475358cbd3a901b3987dcfc97a029ab3068e2/numpy-2.4.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:44aba2f0cafd287871a495fb3163408b0bd25bbce135c6f621534a07f4f7875c", size = 18324222, upload-time = "2025-12-20T16:17:09.392Z" },
|
| 90 |
+
{ url = "https://files.pythonhosted.org/packages/ac/52/41f3d71296a3dcaa4f456aaa3c6fc8e745b43d0552b6bde56571bb4b4a0f/numpy-2.4.0-cp313-cp313t-win32.whl", hash = "sha256:20c115517513831860c573996e395707aa9fb691eb179200125c250e895fcd93", size = 6076216, upload-time = "2025-12-20T16:17:11.437Z" },
|
| 91 |
+
{ url = "https://files.pythonhosted.org/packages/35/ff/46fbfe60ab0710d2a2b16995f708750307d30eccbb4c38371ea9e986866e/numpy-2.4.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b48e35f4ab6f6a7597c46e301126ceba4c44cd3280e3750f85db48b082624fa4", size = 12444263, upload-time = "2025-12-20T16:17:13.182Z" },
|
| 92 |
+
{ url = "https://files.pythonhosted.org/packages/a3/e3/9189ab319c01d2ed556c932ccf55064c5d75bb5850d1df7a482ce0badead/numpy-2.4.0-cp313-cp313t-win_arm64.whl", hash = "sha256:4d1cfce39e511069b11e67cd0bd78ceff31443b7c9e5c04db73c7a19f572967c", size = 10378265, upload-time = "2025-12-20T16:17:15.211Z" },
|
| 93 |
+
{ url = "https://files.pythonhosted.org/packages/ab/ed/52eac27de39d5e5a6c9aadabe672bc06f55e24a3d9010cd1183948055d76/numpy-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c95eb6db2884917d86cde0b4d4cf31adf485c8ec36bf8696dd66fa70de96f36b", size = 16647476, upload-time = "2025-12-20T16:17:17.671Z" },
|
| 94 |
+
{ url = "https://files.pythonhosted.org/packages/77/c0/990ce1b7fcd4e09aeaa574e2a0a839589e4b08b2ca68070f1acb1fea6736/numpy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:65167da969cd1ec3a1df31cb221ca3a19a8aaa25370ecb17d428415e93c1935e", size = 12374563, upload-time = "2025-12-20T16:17:20.216Z" },
|
| 95 |
+
{ url = "https://files.pythonhosted.org/packages/37/7c/8c5e389c6ae8f5fd2277a988600d79e9625db3fff011a2d87ac80b881a4c/numpy-2.4.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3de19cfecd1465d0dcf8a5b5ea8b3155b42ed0b639dba4b71e323d74f2a3be5e", size = 5203107, upload-time = "2025-12-20T16:17:22.47Z" },
|
| 96 |
+
{ url = "https://files.pythonhosted.org/packages/e6/94/ca5b3bd6a8a70a5eec9a0b8dd7f980c1eff4b8a54970a9a7fef248ef564f/numpy-2.4.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6c05483c3136ac4c91b4e81903cb53a8707d316f488124d0398499a4f8e8ef51", size = 6538067, upload-time = "2025-12-20T16:17:24.001Z" },
|
| 97 |
+
{ url = "https://files.pythonhosted.org/packages/79/43/993eb7bb5be6761dde2b3a3a594d689cec83398e3f58f4758010f3b85727/numpy-2.4.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36667db4d6c1cea79c8930ab72fadfb4060feb4bfe724141cd4bd064d2e5f8ce", size = 14411926, upload-time = "2025-12-20T16:17:25.822Z" },
|
| 98 |
+
{ url = "https://files.pythonhosted.org/packages/03/75/d4c43b61de473912496317a854dac54f1efec3eeb158438da6884b70bb90/numpy-2.4.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a818668b674047fd88c4cddada7ab8f1c298812783e8328e956b78dc4807f9f", size = 16354295, upload-time = "2025-12-20T16:17:28.308Z" },
|
| 99 |
+
{ url = "https://files.pythonhosted.org/packages/b8/0a/b54615b47ee8736a6461a4bb6749128dd3435c5a759d5663f11f0e9af4ac/numpy-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ee32359fb7543b7b7bd0b2f46294db27e29e7bbdf70541e81b190836cd83ded", size = 16190242, upload-time = "2025-12-20T16:17:30.993Z" },
|
| 100 |
+
{ url = "https://files.pythonhosted.org/packages/98/ce/ea207769aacad6246525ec6c6bbd66a2bf56c72443dc10e2f90feed29290/numpy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e493962256a38f58283de033d8af176c5c91c084ea30f15834f7545451c42059", size = 18280875, upload-time = "2025-12-20T16:17:33.327Z" },
|
| 101 |
+
{ url = "https://files.pythonhosted.org/packages/17/ef/ec409437aa962ea372ed601c519a2b141701683ff028f894b7466f0ab42b/numpy-2.4.0-cp314-cp314-win32.whl", hash = "sha256:6bbaebf0d11567fa8926215ae731e1d58e6ec28a8a25235b8a47405d301332db", size = 6002530, upload-time = "2025-12-20T16:17:35.729Z" },
|
| 102 |
+
{ url = "https://files.pythonhosted.org/packages/5f/4a/5cb94c787a3ed1ac65e1271b968686521169a7b3ec0b6544bb3ca32960b0/numpy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d857f55e7fdf7c38ab96c4558c95b97d1c685be6b05c249f5fdafcbd6f9899e", size = 12435890, upload-time = "2025-12-20T16:17:37.599Z" },
|
| 103 |
+
{ url = "https://files.pythonhosted.org/packages/48/a0/04b89db963af9de1104975e2544f30de89adbf75b9e75f7dd2599be12c79/numpy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:bb50ce5fb202a26fd5404620e7ef820ad1ab3558b444cb0b55beb7ef66cd2d63", size = 10591892, upload-time = "2025-12-20T16:17:39.649Z" },
|
| 104 |
+
{ url = "https://files.pythonhosted.org/packages/53/e5/d74b5ccf6712c06c7a545025a6a71bfa03bdc7e0568b405b0d655232fd92/numpy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:355354388cba60f2132df297e2d53053d4063f79077b67b481d21276d61fc4df", size = 12494312, upload-time = "2025-12-20T16:17:41.714Z" },
|
| 105 |
+
{ url = "https://files.pythonhosted.org/packages/c2/08/3ca9cc2ddf54dfee7ae9a6479c071092a228c68aef08252aa08dac2af002/numpy-2.4.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:1d8f9fde5f6dc1b6fc34df8162f3b3079365468703fee7f31d4e0cc8c63baed9", size = 5322862, upload-time = "2025-12-20T16:17:44.145Z" },
|
| 106 |
+
{ url = "https://files.pythonhosted.org/packages/87/74/0bb63a68394c0c1e52670cfff2e309afa41edbe11b3327d9af29e4383f34/numpy-2.4.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e0434aa22c821f44eeb4c650b81c7fbdd8c0122c6c4b5a576a76d5a35625ecd9", size = 6644986, upload-time = "2025-12-20T16:17:46.203Z" },
|
| 107 |
+
{ url = "https://files.pythonhosted.org/packages/06/8f/9264d9bdbcf8236af2823623fe2f3981d740fc3461e2787e231d97c38c28/numpy-2.4.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40483b2f2d3ba7aad426443767ff5632ec3156ef09742b96913787d13c336471", size = 14457958, upload-time = "2025-12-20T16:17:48.017Z" },
|
| 108 |
+
{ url = "https://files.pythonhosted.org/packages/8c/d9/f9a69ae564bbc7236a35aa883319364ef5fd41f72aa320cc1cbe66148fe2/numpy-2.4.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6a7664ddd9746e20b7325351fe1a8408d0a2bf9c63b5e898290ddc8f09544", size = 16398394, upload-time = "2025-12-20T16:17:50.409Z" },
|
| 109 |
+
{ url = "https://files.pythonhosted.org/packages/34/c7/39241501408dde7f885d241a98caba5421061a2c6d2b2197ac5e3aa842d8/numpy-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ecb0019d44f4cdb50b676c5d0cb4b1eae8e15d1ed3d3e6639f986fc92b2ec52c", size = 16241044, upload-time = "2025-12-20T16:17:52.661Z" },
|
| 110 |
+
{ url = "https://files.pythonhosted.org/packages/7c/95/cae7effd90e065a95e59fe710eeee05d7328ed169776dfdd9f789e032125/numpy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d0ffd9e2e4441c96a9c91ec1783285d80bf835b677853fc2770a89d50c1e48ac", size = 18321772, upload-time = "2025-12-20T16:17:54.947Z" },
|
| 111 |
+
{ url = "https://files.pythonhosted.org/packages/96/df/3c6c279accd2bfb968a76298e5b276310bd55d243df4fa8ac5816d79347d/numpy-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:77f0d13fa87036d7553bf81f0e1fe3ce68d14c9976c9851744e4d3e91127e95f", size = 6148320, upload-time = "2025-12-20T16:17:57.249Z" },
|
| 112 |
+
{ url = "https://files.pythonhosted.org/packages/92/8d/f23033cce252e7a75cae853d17f582e86534c46404dea1c8ee094a9d6d84/numpy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b1f5b45829ac1848893f0ddf5cb326110604d6df96cdc255b0bf9edd154104d4", size = 12623460, upload-time = "2025-12-20T16:17:58.963Z" },
|
| 113 |
+
{ url = "https://files.pythonhosted.org/packages/a4/4f/1f8475907d1a7c4ef9020edf7f39ea2422ec896849245f00688e4b268a71/numpy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:23a3e9d1a6f360267e8fbb38ba5db355a6a7e9be71d7fce7ab3125e88bb646c8", size = 10661799, upload-time = "2025-12-20T16:18:01.078Z" },
|
| 114 |
+
]
|
| 115 |
+
|
| 116 |
+
[[package]]
|
| 117 |
+
name = "packaging"
|
| 118 |
+
version = "26.3"
|
| 119 |
+
source = { registry = "https://pypi.org/simple" }
|
| 120 |
+
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
|
| 121 |
+
wheels = [
|
| 122 |
+
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
|
| 123 |
+
]
|
| 124 |
+
|
| 125 |
+
[[package]]
|
| 126 |
+
name = "pluggy"
|
| 127 |
+
version = "1.7.0"
|
| 128 |
+
source = { registry = "https://pypi.org/simple" }
|
| 129 |
+
sdist = { url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8", size = 123304, upload-time = "2026-10-15T09:50:58.343Z" }
|
| 130 |
+
wheels = [
|
| 131 |
+
{ url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec", size = 27082, upload-time = "2026-10-15T09:50:56.808Z" },
|
| 132 |
+
]
|
| 133 |
+
|
| 134 |
+
[[package]]
|
| 135 |
+
name = "pygments"
|
| 136 |
+
version = "2.21.0"
|
| 137 |
+
source = { registry = "https://pypi.org/simple" }
|
| 138 |
+
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
|
| 139 |
+
wheels = [
|
| 140 |
+
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
|
| 141 |
+
]
|
| 142 |
+
|
| 143 |
+
[[package]]
|
| 144 |
+
name = "pytest"
|
| 145 |
+
version = "9.2.0"
|
| 146 |
+
source = { registry = "https://pypi.org/simple" }
|
| 147 |
+
dependencies = [
|
| 148 |
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
| 149 |
+
{ name = "iniconfig" },
|
| 150 |
+
{ name = "packaging" },
|
| 151 |
+
{ name = "pluggy" },
|
| 152 |
+
{ name = "pygments" },
|
| 153 |
+
]
|
| 154 |
+
sdist = { url = "https://files.pythonhosted.org/packages/79/42/bb4641652e58dedeac2ec51abc1f8cae4fee4ec63899a0999016229a0f2a/pytest-9.2.0.tar.gz", hash = "sha256:97c4f3b1f3c7414aa36465e09b8220f1d03627993bd51b1282e193753c80557b", size = 1797555, upload-time = "2026-10-27T21:30:20.728Z" }
|
| 155 |
+
wheels = [
|
| 156 |
+
{ url = "https://files.pythonhosted.org/packages/51/39/1f5d8c8877a0fa327db959c19341d4be61660de1ab6b7391f9eb805bcd67/pytest-9.2.0-py3-none-any.whl", hash = "sha256:07acdcf88ec174f907dc01af70bf52d49b014bbab03faeeade31c96b18c2e588", size = 411776, upload-time = "2026-10-27T21:30:19.131Z" },
|
| 157 |
+
]
|
| 158 |
+
|
| 159 |
+
[[package]]
|
| 160 |
+
name = "sympy"
|
| 161 |
+
version = "1.14.0"
|
| 162 |
+
source = { registry = "https://pypi.org/simple" }
|
| 163 |
+
dependencies = [
|
| 164 |
+
{ name = "mpmath" },
|
| 165 |
+
]
|
| 166 |
+
sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
|
| 167 |
+
wheels = [
|
| 168 |
+
{ url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
|
| 169 |
+
]
|
dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J 2.woff2
ADDED
|
Binary file (8.36 kB). View file
|
|
|
dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2
ADDED
|
Binary file (8.36 kB). View file
|
|
|
dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ 2.woff
ADDED
|
Binary file (7.2 kB). View file
|
|
|
dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff
ADDED
|
Binary file (7.2 kB). View file
|
|
|
dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv 2.woff
ADDED
|
Binary file (7.21 kB). View file
|
|
|
dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff
ADDED
|
Binary file (7.21 kB). View file
|
|
|
dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag 2.woff2
ADDED
|
Binary file (8.46 kB). View file
|
|
|
dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2
ADDED
|
Binary file (8.46 kB). View file
|
|
|
dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv 2.woff
ADDED
|
Binary file (5.82 kB). View file
|
|
|
dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff
ADDED
|
Binary file (5.82 kB). View file
|
|
|
dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f 2.woff2
ADDED
|
Binary file (6.91 kB). View file
|
|
|
dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2
ADDED
|
Binary file (6.91 kB). View file
|
|
|
dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT 2.woff
ADDED
|
Binary file (5.77 kB). View file
|
|
|
dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff
ADDED
|
Binary file (5.77 kB). View file
|
|
|
dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T 2.woff2
ADDED
|
Binary file (6.97 kB). View file
|
|
|
dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2
ADDED
|
Binary file (6.97 kB). View file
|
|
|
dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP 2.woff
ADDED
|
Binary file (13.1 kB). View file
|
|
|
dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff
ADDED
|
Binary file (13.1 kB). View file
|
|
|
dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y 2.woff2
ADDED
|
Binary file (14.7 kB). View file
|
|
|
dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2
ADDED
|
Binary file (14.7 kB). View file
|
|
|
dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo 2.woff
ADDED
|
Binary file (13.2 kB). View file
|
|
|
dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff
ADDED
|
Binary file (13.2 kB). View file
|
|
|
dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd 2.woff2
ADDED
|
Binary file (14.9 kB). View file
|
|
|
dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2
ADDED
|
Binary file (14.9 kB). View file
|
|
|
dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV 2.woff2
ADDED
|
Binary file (13.3 kB). View file
|
|
|
dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2
ADDED
|
Binary file (13.3 kB). View file
|
|
|
dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC 2.woff
ADDED
|
Binary file (11.8 kB). View file
|
|
|