asp-studio 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- asp_studio-0.1.0/LICENSE +21 -0
- asp_studio-0.1.0/PKG-INFO +110 -0
- asp_studio-0.1.0/README.md +81 -0
- asp_studio-0.1.0/pyproject.toml +45 -0
- asp_studio-0.1.0/setup.cfg +4 -0
- asp_studio-0.1.0/src/asp_studio/__init__.py +18 -0
- asp_studio-0.1.0/src/asp_studio/__main__.py +65 -0
- asp_studio-0.1.0/src/asp_studio/magic.py +48 -0
- asp_studio-0.1.0/src/asp_studio/render.py +61 -0
- asp_studio-0.1.0/src/asp_studio/solver.py +214 -0
- asp_studio-0.1.0/src/asp_studio/static/bootstrap.js +64 -0
- asp_studio-0.1.0/src/asp_studio/static/engine.js +187 -0
- asp_studio-0.1.0/src/asp_studio/static/themes.css +100 -0
- asp_studio-0.1.0/src/asp_studio/static/webview.css +168 -0
- asp_studio-0.1.0/src/asp_studio/static/webview.js +427 -0
- asp_studio-0.1.0/src/asp_studio.egg-info/PKG-INFO +110 -0
- asp_studio-0.1.0/src/asp_studio.egg-info/SOURCES.txt +24 -0
- asp_studio-0.1.0/src/asp_studio.egg-info/dependency_links.txt +1 -0
- asp_studio-0.1.0/src/asp_studio.egg-info/entry_points.txt +2 -0
- asp_studio-0.1.0/src/asp_studio.egg-info/requires.txt +8 -0
- asp_studio-0.1.0/src/asp_studio.egg-info/top_level.txt +1 -0
- asp_studio-0.1.0/tests/test_cli.py +56 -0
- asp_studio-0.1.0/tests/test_magic.py +31 -0
- asp_studio-0.1.0/tests/test_render.py +44 -0
- asp_studio-0.1.0/tests/test_solver.py +89 -0
- asp_studio-0.1.0/tests/test_static_sync.py +13 -0
asp_studio-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 yakupbulbul
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: asp-studio
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Answer sets, a graph and a step-by-step animator for Answer Set Programming, in Jupyter, VS Code notebooks and the command line.
|
|
5
|
+
Author: yakupbulbul
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://yakupbulbul.github.io/asp-vscode/
|
|
8
|
+
Project-URL: Documentation, https://yakupbulbul.github.io/asp-vscode/docs.html
|
|
9
|
+
Project-URL: Source, https://github.com/yakupbulbul/asp-vscode
|
|
10
|
+
Project-URL: Issues, https://github.com/yakupbulbul/asp-vscode/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/yakupbulbul/asp-vscode/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: asp,answer set programming,clingo,jupyter,visualization,potassco
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Framework :: Jupyter
|
|
15
|
+
Classifier: Intended Audience :: Education
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: clingo>=5.5
|
|
23
|
+
Provides-Extra: notebook
|
|
24
|
+
Requires-Dist: ipython; extra == "notebook"
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pytest; extra == "test"
|
|
27
|
+
Requires-Dist: ipython; extra == "test"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# ASP Studio for Python
|
|
31
|
+
|
|
32
|
+
ASP Studio shows the answer sets of an [Answer Set Programming](https://potassco.org/) program, draws them as a graph, and steps through how a recursive program reaches its result. It works in Jupyter, in VS Code notebooks and from the command line. It uses the clingo Python API and shares its visualizer with the [ASP Studio VS Code extension](https://github.com/yakupbulbul/asp-vscode).
|
|
33
|
+
|
|
34
|
+
Website and live demo: <https://yakupbulbul.github.io/asp-vscode/>
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install "asp-studio[notebook]" # Python 3.9 or newer
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The `notebook` extra adds IPython for the `%%asp` magic. Without it you still get `solve()` and the command line.
|
|
41
|
+
|
|
42
|
+
## In a notebook
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
%load_ext asp_studio
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
%%asp
|
|
50
|
+
parent(anna,bob). parent(bob,carl). parent(carl,eve).
|
|
51
|
+
ancestor(X,Y) :- parent(X,Y).
|
|
52
|
+
ancestor(X,Z) :- parent(X,Y), ancestor(Y,Z).
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The output has four tabs. Animator steps through the rounds and shows which rule fired with which variable bindings. Answer Sets lists every model. Graph draws the atoms. Console shows clingo's messages and the position of any error.
|
|
56
|
+
|
|
57
|
+
Cell options:
|
|
58
|
+
|
|
59
|
+
| Option | Meaning |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `-n 5`, `--models 5` | At most 5 answer sets (0 means all). |
|
|
62
|
+
| `-o "--opt-mode=optN -c n=6"` | Extra clingo options. |
|
|
63
|
+
| `-t 20` | Timeout in seconds. The default is 10. |
|
|
64
|
+
| `--tab answers` | First tab to show: `animator`, `answers`, `graph` or `console`. |
|
|
65
|
+
| `--theme dark` | `auto` (default), `light` or `dark`. Inside VS Code the editor theme is used. |
|
|
66
|
+
| `--height 500` | Starting height in pixels. The output grows with its content. |
|
|
67
|
+
| `--out res` | Keep the result in a variable called `res`. |
|
|
68
|
+
|
|
69
|
+
## From Python
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import asp_studio as asp
|
|
73
|
+
|
|
74
|
+
res = asp.solve("a :- not b. b :- not a.", models=0, timeout=10)
|
|
75
|
+
res.result # 'SATISFIABLE'
|
|
76
|
+
list(res) # [['b'], ['a']]
|
|
77
|
+
res.diagnostics # positions of errors and warnings from clingo
|
|
78
|
+
res # as the last line of a cell: shows the visualizer
|
|
79
|
+
asp.show(res, tab="graph")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`solve` takes ASP text, a `pathlib.Path`, or a list of those, which are joined in order. A broken program does not raise: check `res.ok` and `res.error`. With `#minimize` or `#maximize` and a finished search, only the optimal models are kept, and their costs are in `res.costs`.
|
|
83
|
+
|
|
84
|
+
## Command line
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
asp-studio program.lp --open # writes program.html and opens it
|
|
88
|
+
asp-studio instance.lp encoding.lp -n 3 # several files are joined in order
|
|
89
|
+
python -m asp_studio program.lp # the same, without the script on PATH
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The exit code is 0 when the program ran, 1 when it has an error (positions print as `file:line:col`) and 2 for bad arguments or unreadable input.
|
|
93
|
+
|
|
94
|
+
## Notes
|
|
95
|
+
|
|
96
|
+
- The animator follows positive programs only: no `not`, choice rules, constraints or ranges. For other programs it names the unsupported line, and the other three tabs still work.
|
|
97
|
+
- The visualizer runs in an isolated `<iframe>`. Jupyter shows such output only in trusted notebooks.
|
|
98
|
+
- Outside notebooks and VS Code, the color theme follows the operating system.
|
|
99
|
+
|
|
100
|
+
## Development
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pip install -e ".[test]"
|
|
104
|
+
pytest
|
|
105
|
+
python sync_static.py # copy ../media/{engine.js,webview.js,webview.css,themes.css} into the package
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The visualizer files live in `../media` and are copied into `src/asp_studio/static`. A test fails when the copies drift apart.
|
|
109
|
+
|
|
110
|
+
MIT license. Not affiliated with the Potassco team.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# ASP Studio for Python
|
|
2
|
+
|
|
3
|
+
ASP Studio shows the answer sets of an [Answer Set Programming](https://potassco.org/) program, draws them as a graph, and steps through how a recursive program reaches its result. It works in Jupyter, in VS Code notebooks and from the command line. It uses the clingo Python API and shares its visualizer with the [ASP Studio VS Code extension](https://github.com/yakupbulbul/asp-vscode).
|
|
4
|
+
|
|
5
|
+
Website and live demo: <https://yakupbulbul.github.io/asp-vscode/>
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install "asp-studio[notebook]" # Python 3.9 or newer
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The `notebook` extra adds IPython for the `%%asp` magic. Without it you still get `solve()` and the command line.
|
|
12
|
+
|
|
13
|
+
## In a notebook
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
%load_ext asp_studio
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
%%asp
|
|
21
|
+
parent(anna,bob). parent(bob,carl). parent(carl,eve).
|
|
22
|
+
ancestor(X,Y) :- parent(X,Y).
|
|
23
|
+
ancestor(X,Z) :- parent(X,Y), ancestor(Y,Z).
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The output has four tabs. Animator steps through the rounds and shows which rule fired with which variable bindings. Answer Sets lists every model. Graph draws the atoms. Console shows clingo's messages and the position of any error.
|
|
27
|
+
|
|
28
|
+
Cell options:
|
|
29
|
+
|
|
30
|
+
| Option | Meaning |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `-n 5`, `--models 5` | At most 5 answer sets (0 means all). |
|
|
33
|
+
| `-o "--opt-mode=optN -c n=6"` | Extra clingo options. |
|
|
34
|
+
| `-t 20` | Timeout in seconds. The default is 10. |
|
|
35
|
+
| `--tab answers` | First tab to show: `animator`, `answers`, `graph` or `console`. |
|
|
36
|
+
| `--theme dark` | `auto` (default), `light` or `dark`. Inside VS Code the editor theme is used. |
|
|
37
|
+
| `--height 500` | Starting height in pixels. The output grows with its content. |
|
|
38
|
+
| `--out res` | Keep the result in a variable called `res`. |
|
|
39
|
+
|
|
40
|
+
## From Python
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import asp_studio as asp
|
|
44
|
+
|
|
45
|
+
res = asp.solve("a :- not b. b :- not a.", models=0, timeout=10)
|
|
46
|
+
res.result # 'SATISFIABLE'
|
|
47
|
+
list(res) # [['b'], ['a']]
|
|
48
|
+
res.diagnostics # positions of errors and warnings from clingo
|
|
49
|
+
res # as the last line of a cell: shows the visualizer
|
|
50
|
+
asp.show(res, tab="graph")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`solve` takes ASP text, a `pathlib.Path`, or a list of those, which are joined in order. A broken program does not raise: check `res.ok` and `res.error`. With `#minimize` or `#maximize` and a finished search, only the optimal models are kept, and their costs are in `res.costs`.
|
|
54
|
+
|
|
55
|
+
## Command line
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
asp-studio program.lp --open # writes program.html and opens it
|
|
59
|
+
asp-studio instance.lp encoding.lp -n 3 # several files are joined in order
|
|
60
|
+
python -m asp_studio program.lp # the same, without the script on PATH
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The exit code is 0 when the program ran, 1 when it has an error (positions print as `file:line:col`) and 2 for bad arguments or unreadable input.
|
|
64
|
+
|
|
65
|
+
## Notes
|
|
66
|
+
|
|
67
|
+
- The animator follows positive programs only: no `not`, choice rules, constraints or ranges. For other programs it names the unsupported line, and the other three tabs still work.
|
|
68
|
+
- The visualizer runs in an isolated `<iframe>`. Jupyter shows such output only in trusted notebooks.
|
|
69
|
+
- Outside notebooks and VS Code, the color theme follows the operating system.
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install -e ".[test]"
|
|
75
|
+
pytest
|
|
76
|
+
python sync_static.py # copy ../media/{engine.js,webview.js,webview.css,themes.css} into the package
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The visualizer files live in `../media` and are copied into `src/asp_studio/static`. A test fails when the copies drift apart.
|
|
80
|
+
|
|
81
|
+
MIT license. Not affiliated with the Potassco team.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "asp-studio"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Answer sets, a graph and a step-by-step animator for Answer Set Programming, in Jupyter, VS Code notebooks and the command line."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "yakupbulbul" }]
|
|
13
|
+
keywords = ["asp", "answer set programming", "clingo", "jupyter", "visualization", "potassco"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Framework :: Jupyter",
|
|
17
|
+
"Intended Audience :: Education",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Visualization",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["clingo>=5.5"]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
notebook = ["ipython"]
|
|
26
|
+
test = ["pytest", "ipython"]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
asp-studio = "asp_studio.__main__:main"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://yakupbulbul.github.io/asp-vscode/"
|
|
33
|
+
Documentation = "https://yakupbulbul.github.io/asp-vscode/docs.html"
|
|
34
|
+
Source = "https://github.com/yakupbulbul/asp-vscode"
|
|
35
|
+
Issues = "https://github.com/yakupbulbul/asp-vscode/issues"
|
|
36
|
+
Changelog = "https://github.com/yakupbulbul/asp-vscode/blob/main/CHANGELOG.md"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.package-data]
|
|
42
|
+
asp_studio = ["static/*"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""ASP Studio: answer sets, graph and a step-by-step fixpoint animator for Answer Set Programming in Python.
|
|
2
|
+
|
|
3
|
+
import asp_studio as asp
|
|
4
|
+
res = asp.solve("a :- not b. b :- not a.") # clingo Python API under the hood
|
|
5
|
+
asp.show(res) # visualizer in a Jupyter / VS Code notebook
|
|
6
|
+
|
|
7
|
+
In a notebook: ``%load_ext asp_studio`` and start a cell with ``%%asp``.
|
|
8
|
+
"""
|
|
9
|
+
from .solver import Diagnostic, Result, solve
|
|
10
|
+
from .render import iframe, page, show
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
__all__ = ["solve", "show", "Result", "Diagnostic", "page", "iframe", "load_ipython_extension", "__version__"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_ipython_extension(ipython): # `%load_ext asp_studio`
|
|
17
|
+
from .magic import load_ipython_extension as _load
|
|
18
|
+
_load(ipython)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""``python -m asp_studio program.lp`` (or ``asp-studio program.lp``): solve files and write a standalone HTML report."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import pathlib
|
|
6
|
+
import sys
|
|
7
|
+
import webbrowser
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from .render import page
|
|
11
|
+
from .solver import solve
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _fail(message: str, code: int = 2) -> int:
|
|
15
|
+
print(f"error: {message}", file=sys.stderr)
|
|
16
|
+
return code
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
20
|
+
ap = argparse.ArgumentParser(prog="asp-studio", description="Solve ASP files and write an interactive HTML report.")
|
|
21
|
+
ap.add_argument("files", nargs="+", type=pathlib.Path, help=".lp files (joined in order)")
|
|
22
|
+
ap.add_argument("-n", "--models", type=int, default=0, help="max answer sets (0 = all)")
|
|
23
|
+
ap.add_argument("-o", "--options", default="", help='extra clingo options, e.g. "--opt-mode=optN"')
|
|
24
|
+
ap.add_argument("-t", "--timeout", type=float, default=10.0, help="seconds (default 10)")
|
|
25
|
+
ap.add_argument("--out", type=pathlib.Path, help="output file (default: <first file>.html)")
|
|
26
|
+
ap.add_argument("--tab", default="animator", choices=["animator", "answers", "graph", "console"])
|
|
27
|
+
ap.add_argument("--theme", default="auto", choices=["auto", "light", "dark"])
|
|
28
|
+
ap.add_argument("--open", action="store_true", help="open the report in the browser")
|
|
29
|
+
a = ap.parse_args(argv)
|
|
30
|
+
|
|
31
|
+
if a.models < 0:
|
|
32
|
+
return _fail("--models must be 0 or greater")
|
|
33
|
+
if a.timeout <= 0:
|
|
34
|
+
return _fail("--timeout must be greater than 0")
|
|
35
|
+
for f in a.files:
|
|
36
|
+
if f.is_dir():
|
|
37
|
+
return _fail(f"{f} is a directory, not a file")
|
|
38
|
+
if not f.is_file():
|
|
39
|
+
return _fail(f"{f} does not exist")
|
|
40
|
+
try:
|
|
41
|
+
f.read_text(encoding="utf-8")
|
|
42
|
+
except UnicodeDecodeError as e:
|
|
43
|
+
return _fail(f"{f} is not UTF-8 text ({e.reason} at byte {e.start}); is it an ASP file?")
|
|
44
|
+
except OSError as e:
|
|
45
|
+
return _fail(f"cannot read {f}: {e.strerror or e}")
|
|
46
|
+
res = solve(list(a.files), models=a.models, options=a.options, timeout=a.timeout)
|
|
47
|
+
|
|
48
|
+
out = a.out or a.files[0].with_suffix(".html")
|
|
49
|
+
try:
|
|
50
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
out.write_text(page(res, tab=a.tab, theme=a.theme, max_height=100000, title=a.files[0].name), encoding="utf-8")
|
|
52
|
+
except OSError as e:
|
|
53
|
+
return _fail(f"cannot write {out}: {e.strerror or e}", 1)
|
|
54
|
+
|
|
55
|
+
for d in res.diagnostics:
|
|
56
|
+
print(f"{d.file}:{d.line}:{d.col}: {d.severity}: {d.message.splitlines()[0]}", file=sys.stderr)
|
|
57
|
+
status = res.error or f"{res.result}, {len(res)} model(s), {res.ms} ms"
|
|
58
|
+
print(f"{status} -> {out}")
|
|
59
|
+
if a.open:
|
|
60
|
+
webbrowser.open(out.resolve().as_uri())
|
|
61
|
+
return 1 if res.error else 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""IPython cell magic: ``%%asp`` solves the cell and shows the visualizer."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import shlex
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from IPython.core.magic import Magics, cell_magic, magics_class
|
|
9
|
+
|
|
10
|
+
from .render import show
|
|
11
|
+
from .solver import solve
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _parser() -> argparse.ArgumentParser:
|
|
15
|
+
p = argparse.ArgumentParser(prog="%%asp", add_help=False)
|
|
16
|
+
p.add_argument("-n", "--models", type=int, default=0, help="max answer sets (0 = all)")
|
|
17
|
+
p.add_argument("-o", "--options", default="", help="extra clingo options, quoted")
|
|
18
|
+
p.add_argument("-t", "--timeout", type=float, default=10.0)
|
|
19
|
+
p.add_argument("--tab", default="animator", choices=["animator", "answers", "graph", "console"])
|
|
20
|
+
p.add_argument("--theme", default="auto", choices=["auto", "light", "dark"])
|
|
21
|
+
p.add_argument("--height", type=int, default=None)
|
|
22
|
+
p.add_argument("--out", default=None, help="store the Result in this variable")
|
|
23
|
+
return p
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@magics_class
|
|
27
|
+
class AspMagics(Magics):
|
|
28
|
+
@cell_magic
|
|
29
|
+
def asp(self, line: str, cell: str) -> Any:
|
|
30
|
+
"""Solve the cell with clingo and show answer sets, graph and animator.
|
|
31
|
+
|
|
32
|
+
%%asp [-n N] [-o "--opt-mode=optN"] [-t SECONDS] [--tab animator|answers|graph|console]
|
|
33
|
+
[--theme auto|light|dark] [--height PX] [--out NAME]
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
a = _parser().parse_args(shlex.split(line))
|
|
37
|
+
except SystemExit:
|
|
38
|
+
print(self.asp.__doc__)
|
|
39
|
+
return None
|
|
40
|
+
res = solve(cell, models=a.models, options=a.options, timeout=a.timeout)
|
|
41
|
+
show(res, tab=a.tab, theme=a.theme, height=a.height)
|
|
42
|
+
if a.out:
|
|
43
|
+
self.shell.user_ns[a.out] = res
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def load_ipython_extension(ipython: Any) -> None:
|
|
48
|
+
ipython.register_magics(AspMagics)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Build the visualizer page (HTML) for a :class:`~asp_studio.solver.Result`."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import html as _html
|
|
5
|
+
import json
|
|
6
|
+
from importlib import resources
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
_STATIC = ("themes.css", "webview.css")
|
|
10
|
+
_cache: dict = {}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _asset(name: str) -> str:
|
|
14
|
+
if name not in _cache:
|
|
15
|
+
_cache[name] = resources.files("asp_studio").joinpath("static", name).read_text(encoding="utf-8")
|
|
16
|
+
return _cache[name]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _js_json(value: Any) -> str:
|
|
20
|
+
"""JSON that is safe to place inside a <script> element."""
|
|
21
|
+
return json.dumps(value).replace("</", "<\\/").replace("<!--", "<\\!--")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def page(result, *, tab: str = "animator", theme: str = "auto", max_height: int = 760, title: str = "program") -> str:
|
|
25
|
+
"""A complete, self-contained HTML document showing the visualizer."""
|
|
26
|
+
init = {"state": result.to_state(), "program": result.program, "tab": tab, "theme": theme,
|
|
27
|
+
"maxHeight": max_height, "title": title}
|
|
28
|
+
css = "\n".join(_asset(n) for n in _STATIC)
|
|
29
|
+
return (
|
|
30
|
+
'<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">'
|
|
31
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">'
|
|
32
|
+
f"<title>ASP Studio: {_html.escape(title)}</title><style>{css}</style></head>"
|
|
33
|
+
'<body><div id="app"></div>'
|
|
34
|
+
f"<script>window.__ASP_INIT__ = {_js_json(init)};</script>"
|
|
35
|
+
f"<script>{_asset('engine.js')}</script>"
|
|
36
|
+
f"<script>{_asset('bootstrap.js')}</script>"
|
|
37
|
+
f"<script>{_asset('webview.js')}</script>"
|
|
38
|
+
"</body></html>"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def iframe(result, *, tab: str = "animator", theme: str = "auto", height: Optional[int] = None, max_height: int = 760) -> str:
|
|
43
|
+
"""The page wrapped in an isolated <iframe>, ready to display in a notebook output."""
|
|
44
|
+
doc = page(result, tab=tab, theme=theme, max_height=max_height)
|
|
45
|
+
start = height if height else 420
|
|
46
|
+
return (
|
|
47
|
+
'<iframe class="asp-studio" sandbox="allow-scripts allow-same-origin" '
|
|
48
|
+
f'style="width:100%;height:{start}px;border:0;display:block" srcdoc="{_html.escape(doc, quote=True)}"></iframe>'
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def show(program_or_result, *, tab: str = "animator", theme: str = "auto", height: Optional[int] = None,
|
|
53
|
+
max_height: int = 760, **solve_kwargs: Any) -> None:
|
|
54
|
+
"""Display the visualizer in the current notebook cell. ``program_or_result`` may be ASP text or a Result."""
|
|
55
|
+
try:
|
|
56
|
+
from IPython.display import display
|
|
57
|
+
except ImportError as e: # pragma: no cover
|
|
58
|
+
raise ImportError("asp_studio.show needs IPython: pip install 'asp-studio[notebook]'") from e
|
|
59
|
+
from .solver import Result, solve
|
|
60
|
+
result = program_or_result if isinstance(program_or_result, Result) else solve(program_or_result, **solve_kwargs)
|
|
61
|
+
display({"text/html": iframe(result, tab=tab, theme=theme, height=height, max_height=max_height)}, raw=True)
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Solve an ASP program with the clingo Python API and keep everything the visualizer needs."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union
|
|
9
|
+
|
|
10
|
+
import clingo
|
|
11
|
+
|
|
12
|
+
Program = Union[str, Path]
|
|
13
|
+
|
|
14
|
+
_DIAG = re.compile(
|
|
15
|
+
r"^(?:<block>|<string>|-):(\d+):(\d+)(?:-(\d+))?(?::(\d+)(?:-(\d+))?)?: (error|warning|info): (.*)$"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Diagnostic:
|
|
21
|
+
line: int
|
|
22
|
+
col: int
|
|
23
|
+
end_col: int
|
|
24
|
+
severity: str
|
|
25
|
+
message: str
|
|
26
|
+
file: str = "program"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(repr=False)
|
|
30
|
+
class Result:
|
|
31
|
+
"""Outcome of :func:`solve`. Iterating yields the answer sets (lists of atom strings)."""
|
|
32
|
+
|
|
33
|
+
program: str
|
|
34
|
+
version: str = clingo.__version__
|
|
35
|
+
command: str = ""
|
|
36
|
+
result: str = "UNKNOWN"
|
|
37
|
+
models: List[List[str]] = field(default_factory=list)
|
|
38
|
+
costs: List[Optional[List[int]]] = field(default_factory=list)
|
|
39
|
+
more: bool = False
|
|
40
|
+
ms: int = 0
|
|
41
|
+
time: Optional[float] = None
|
|
42
|
+
stderr: str = ""
|
|
43
|
+
diagnostics: List[Diagnostic] = field(default_factory=list)
|
|
44
|
+
error: Optional[str] = None
|
|
45
|
+
timed_out: bool = False
|
|
46
|
+
|
|
47
|
+
def __repr__(self) -> str:
|
|
48
|
+
if self.error:
|
|
49
|
+
return f"Result(error={self.error!r})"
|
|
50
|
+
return f"Result({self.result}, {len(self.models)} model{'s' if len(self.models) != 1 else ''}, {self.ms} ms)"
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def ok(self) -> bool:
|
|
54
|
+
"""True if the program ran without error (it may still be unsatisfiable)."""
|
|
55
|
+
return self.error is None
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def satisfiable(self) -> bool:
|
|
59
|
+
return self.ok and bool(self.models)
|
|
60
|
+
|
|
61
|
+
def __iter__(self):
|
|
62
|
+
return iter(self.models)
|
|
63
|
+
|
|
64
|
+
def __len__(self) -> int:
|
|
65
|
+
return len(self.models)
|
|
66
|
+
|
|
67
|
+
def __getitem__(self, i: int) -> List[str]:
|
|
68
|
+
return self.models[i]
|
|
69
|
+
|
|
70
|
+
def to_state(self) -> Dict[str, Any]:
|
|
71
|
+
"""The ``solve`` object the visualizer front end renders."""
|
|
72
|
+
state: Dict[str, Any] = {
|
|
73
|
+
"engine": "python",
|
|
74
|
+
"version": self.version,
|
|
75
|
+
"command": self.command,
|
|
76
|
+
"result": self.result,
|
|
77
|
+
"models": [{"atoms": a, "costs": c} for a, c in zip(self.models, self.costs)],
|
|
78
|
+
"totalModels": len(self.models),
|
|
79
|
+
"more": self.more,
|
|
80
|
+
"ms": self.ms,
|
|
81
|
+
"time": self.time,
|
|
82
|
+
"stderr": self.stderr,
|
|
83
|
+
"capped": 0,
|
|
84
|
+
"diagnostics": [
|
|
85
|
+
{"sourceIndex": 0, "file": d.file, "line": d.line, "col": d.col, "endCol": d.end_col,
|
|
86
|
+
"severity": d.severity, "message": d.message}
|
|
87
|
+
for d in self.diagnostics
|
|
88
|
+
],
|
|
89
|
+
}
|
|
90
|
+
if self.error:
|
|
91
|
+
state["error"] = self.error
|
|
92
|
+
state["timedOut"] = self.timed_out
|
|
93
|
+
return state
|
|
94
|
+
|
|
95
|
+
def _repr_html_(self) -> str: # picked up by Jupyter when a Result is the last expression of a cell
|
|
96
|
+
from .render import iframe
|
|
97
|
+
return iframe(self)
|
|
98
|
+
|
|
99
|
+
def show(self, **kwargs: Any) -> None:
|
|
100
|
+
from .render import show
|
|
101
|
+
show(self, **kwargs)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _read(programs: Union[Program, Sequence[Program]]) -> Tuple[str, List[Tuple[str, int]]]:
|
|
105
|
+
"""Join the programs; also return (name, line count) per part so messages can be mapped back to their file."""
|
|
106
|
+
items: Iterable[Program] = [programs] if isinstance(programs, (str, Path)) else programs
|
|
107
|
+
parts: List[str] = []
|
|
108
|
+
sources: List[Tuple[str, int]] = []
|
|
109
|
+
for p in items:
|
|
110
|
+
text = p.read_text(encoding="utf-8") if isinstance(p, Path) else p
|
|
111
|
+
parts.append(text)
|
|
112
|
+
sources.append((p.name if isinstance(p, Path) else "program", len(text.split("\n"))))
|
|
113
|
+
return "\n".join(parts) + "\n", sources
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _locate(sources: Sequence[Tuple[str, int]], line: int) -> Tuple[str, int]:
|
|
117
|
+
"""Map a 1-based line of the joined program to (file name, line in that file)."""
|
|
118
|
+
start = 0
|
|
119
|
+
for name, count in sources:
|
|
120
|
+
if line - 1 < start + count:
|
|
121
|
+
return name, line - start
|
|
122
|
+
start += count
|
|
123
|
+
return (sources[-1][0], 1) if sources else ("program", line)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _parse_messages(messages: Sequence[str], sources: Sequence[Tuple[str, int]]) -> List[Diagnostic]:
|
|
127
|
+
out: List[Diagnostic] = []
|
|
128
|
+
for msg in messages:
|
|
129
|
+
lines = msg.split("\n")
|
|
130
|
+
m = _DIAG.match(lines[0])
|
|
131
|
+
if not m:
|
|
132
|
+
continue
|
|
133
|
+
col = int(m.group(2))
|
|
134
|
+
# `L:C1-C2` marks a range on one line; `L:C1-L2:C2` spans lines, then only the start is used
|
|
135
|
+
end = int(m.group(3)) if m.group(3) and m.group(4) is None else col + 1
|
|
136
|
+
text = m.group(7)
|
|
137
|
+
extra = [l.strip() for l in lines[1:] if l.strip() and not l.startswith("***")]
|
|
138
|
+
if extra:
|
|
139
|
+
text += "\n" + "\n".join(extra)
|
|
140
|
+
sev = {"error": "error", "info": "info"}.get(m.group(6), "warning")
|
|
141
|
+
name, line = _locate(sources, int(m.group(1)))
|
|
142
|
+
out.append(Diagnostic(line, col, end, sev, text, name))
|
|
143
|
+
return out
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def solve(
|
|
147
|
+
program: Union[Program, Sequence[Program]],
|
|
148
|
+
*,
|
|
149
|
+
models: int = 0,
|
|
150
|
+
options: Union[str, Sequence[str]] = (),
|
|
151
|
+
timeout: float = 10.0,
|
|
152
|
+
) -> Result:
|
|
153
|
+
"""Solve ``program`` and return a :class:`Result`.
|
|
154
|
+
|
|
155
|
+
``program`` is ASP text, a :class:`pathlib.Path`, or a list of those (joined in order).
|
|
156
|
+
``models`` limits the number of answer sets (0 = all). ``options`` are extra clingo options,
|
|
157
|
+
either a list or one string such as ``"--opt-mode=optN -c n=6"``. ``timeout`` is in seconds.
|
|
158
|
+
With optimisation statements and a finished search only the optimal models are kept.
|
|
159
|
+
"""
|
|
160
|
+
text, sources = _read(program)
|
|
161
|
+
opts = options.split() if isinstance(options, str) else list(options)
|
|
162
|
+
args = [str(max(0, int(models)))] + opts
|
|
163
|
+
res = Result(program=text, command="clingo.Control(" + repr(args) + ")")
|
|
164
|
+
messages: List[str] = []
|
|
165
|
+
found: List[Any] = []
|
|
166
|
+
exhausted = False
|
|
167
|
+
t0 = time.perf_counter()
|
|
168
|
+
|
|
169
|
+
def on_model(m: clingo.Model) -> None:
|
|
170
|
+
found.append(([str(s) for s in m.symbols(shown=True)], list(m.cost) if m.cost else None, m.optimality_proven))
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
ctl = clingo.Control(args, lambda code, msg: messages.append(msg), 50)
|
|
174
|
+
ctl.add("base", [], text)
|
|
175
|
+
ctl.ground([("base", [])])
|
|
176
|
+
handle = ctl.solve(on_model=on_model, async_=True)
|
|
177
|
+
if not handle.wait(timeout):
|
|
178
|
+
handle.cancel()
|
|
179
|
+
handle.get()
|
|
180
|
+
res.timed_out = True
|
|
181
|
+
res.error = f"Timeout after {timeout:g} s."
|
|
182
|
+
else:
|
|
183
|
+
sr = handle.get()
|
|
184
|
+
res.result = "SATISFIABLE" if sr.satisfiable else "UNSATISFIABLE" if sr.unsatisfiable else "UNKNOWN"
|
|
185
|
+
exhausted = bool(sr.exhausted)
|
|
186
|
+
res.more = bool(models) and len(found) >= models and not exhausted
|
|
187
|
+
try:
|
|
188
|
+
res.time = float(ctl.statistics["summary"]["times"]["total"])
|
|
189
|
+
except Exception: # statistics are optional
|
|
190
|
+
res.time = None
|
|
191
|
+
except RuntimeError as e:
|
|
192
|
+
res.error = str(e)
|
|
193
|
+
res.diagnostics = _parse_messages(messages, sources)
|
|
194
|
+
if res.error and not res.timed_out and any(d.severity == "error" for d in res.diagnostics):
|
|
195
|
+
res.error = "The program has a syntax or semantic error."
|
|
196
|
+
res.stderr = "".join(m if m.endswith("\n") else m + "\n" for m in messages)
|
|
197
|
+
res.ms = round((time.perf_counter() - t0) * 1000)
|
|
198
|
+
|
|
199
|
+
if res.error is None:
|
|
200
|
+
optimising = any(f[1] for f in found)
|
|
201
|
+
keep = found
|
|
202
|
+
if optimising and exhausted: # search finished: only the best cost is optimal
|
|
203
|
+
best = min(tuple(f[1] or ()) for f in found)
|
|
204
|
+
keep = [f for f in found if tuple(f[1] or ()) == best]
|
|
205
|
+
res.result = "OPTIMUM FOUND"
|
|
206
|
+
seen = set()
|
|
207
|
+
for atoms, cost, _ in keep:
|
|
208
|
+
key = tuple(atoms)
|
|
209
|
+
if key in seen:
|
|
210
|
+
continue
|
|
211
|
+
seen.add(key)
|
|
212
|
+
res.models.append(atoms)
|
|
213
|
+
res.costs.append(cost)
|
|
214
|
+
return res
|