A mathematical workspace that grows with you.
Project description
MathSlate
A mathematical workspace that grows with you.
A mathematical workspace that orchestrates SymPy, NumPy and Plotly so that a learner can go from their first graph to real scientific computing without ever changing tools.
from mathslate import *
plot(sin(x)/x)
curve | x ∈ [-10, 10] | 411 samples | 1 discontinuity handled
· singularities at x = 0
That is the whole first lesson. No symbols, no lambdify, no linspace, no
figure, no show. When you are ready for those, ask:
plot(sin(x)/x).show_python()
and MathSlate prints the plain NumPy + SymPy + Plotly program that would have produced the same picture — including the parts it did quietly on your behalf.
Install
Pick whichever of these three matches you.
1. You already have a Python environment
pip install mathslate
Nothing frontend-specific comes with it. Add what your notebook needs:
pip install "mathslate[jupyter]" # pulls in ipywidgets + anywidget
pip install "mathslate[marimo]"
2. New to Python, or you'd rather not fiddle
One command — installs uv first if you don't have it, then creates the environment, installs mathslate, writes a starter notebook with the import already in it, and opens Jupyter Lab on it:
curl -LsSf https://astral.sh/uv/install.sh | sh # skip if `uv --version` already works
bash scripts/quickstart.sh
irm https://astral.sh/uv/install.ps1 | iex # skip if `uv --version` already works
scripts\quickstart.ps1
What that script runs, spelled out — useful if you'd rather type it yourself or adapt it:
uv venv .venv-mathslate
uv pip install --python .venv-mathslate "mathslate[jupyter]" # ipywidgets + anywidget come with it
.venv-mathslate/bin/python -m jupyter lab
(.venv-mathslate/Scripts/python.exe on Windows. The environment is named
rather than the usual .venv so running this inside a clone cannot replace a
development environment already there.)
The one line every notebook needs, at the top of the first cell:
from mathslate import *
That single import is the whole setup — plot, analyze, sin, x and
everything else in this README come from it. ipywidgets/anywidget are
never imported directly; mathslate uses them internally for slider().
Swap the extra for marimo and the last line for -m marimo edit to use
marimo instead.
3. Just evaluating — no install at all
A live, in-browser preview (JupyterLite, no local setup) runs at https://berd2.github.io/mathslate/. It's slower than a local install — everything runs client-side via Pyodide — and needs one extra cell before the first import:
import piplite
await piplite.install("mathslate")
Treat it as a five-minute look, not a working environment; move to path 1 or 2 once you're sold.
marimo users: marimo rejects
import *at parse time — it needs to know statically which names each cell defines in order to build its reactive graph. Import explicitly instead; everything else is identical. See the manual.from mathslate import plot, polar, sin, cos, tan, exp, sqrt, x, y, t
The one rule
plot() infers what you meant. There is exactly one thing to memorise:
- a list means several things together
- a tuple means one vector-valued object
plot([sin(x), cos(x)]) # two curves, overlaid
plot((sin(t), cos(t))) # one parametric curve
Everything else follows the dispatch contract:
| Input | Free symbols | Result |
|---|---|---|
Expr |
1 | 2D curve |
Expr |
0 | horizontal line + message |
list[Expr] |
1 shared | curves overlaid |
tuple[Expr, Expr] |
1 shared | 2D parametric curve |
callable |
— | numeric sampling |
| array-like | — | data series |
(xdata, ydata) |
— | scatter |
Expr |
2 | surface, kind="contour" to flatten |
Eq(lhs, rhs) |
2 | implicit curve |
tuple[Expr × 3] |
1 or 2 | space curve / parametric surface |
Two cases inference cannot decide in principle, so you say them out loud:
polar(1 + cos(t)) # r = f(θ) is indistinguishable from y = f(x)
plot(x*y, kind="contour") # surface vs. contour
Every call reports what it inferred, in one line. That line is not logging: it is where you first learn that parameters you never wrote exist.
Which symbol becomes the axis
- an explicit range wins —
plot(a*sin(x), (a, -1, 1)) - bound parameters are never axes —
plot(a*sin(x), parameters={a: 3}) - otherwise the conventional order:
x, y, z→t, u, v→r, θ→ alphabetical - if it is still ambiguous, MathSlate asks instead of guessing
The hard part: discontinuities
plot(tan(x)) drawing no spurious vertical lines is the feature that separates
MathSlate from a plotting wrapper. Everything else here is convenience.
plot(tan(x)) # broken at every pole, y-window from the 2nd–98th percentile
plot(1/x) # broken at 0
plot(floor(x)) # broken at every integer
plot(sqrt(x)) # never sampled outside its real domain
plot(x/abs(x)) # broken at 0
How, in order:
- Symbolic singularities from
sympy.calculus.singularities— never guessed. - Real domain from
sympy.calculus.util.continuous_domain— never sampled outside it. - Adaptive subdivision: 200 uniform points, then midpoints wherever three adjacent points bend, to depth 8 and at most 5000 points.
- Line breaking: NaN at every discontinuity. Jumps SymPy cannot see
(
floor,sign,Piecewise) are found by bisection — a genuine jump keeps its size as the interval shrinks, a steep slope does not. - Y-clipping: the visible window comes from the 2nd–98th percentile with near-pole samples excluded.
- Vectorised evaluation via
lambdify(modules="numpy"), falling back to element-wise evaluation loudly — never silently. The last element-wise tier is SymPy's ownevalf, which is what makeszeta,Siandbesseljplottable at all: no numeric backend carries them.
All six apply to parametric and polar curves as well. There the continuous
pieces are the intersection of both components' domains, the jump probe watches
x(t) as well as y(t), and the window is clipped horizontally too — x(t)
can reach a pole in a way x never can when it is the axis.
plot((tan(t), t)) # broken at t = π/2 and 3π/2, not drawn out to 10¹⁶
polar(tan(t)) # the same, through the polar reduction
What a function is: analyze()
Explicit, never automatic — property detection is too slow and too noisy to run on every plot.
analyze(x**3 - 3*x) # roots -sqrt(3), 0, sqrt(3); max at -1; min at 1
analyze(1/x) # x = 0 vertical; y = 0 at both ends; odd
plot(tan(x)).analyze() # analyses the window you are looking at
Roots, extrema, inflection points, symmetry, periodicity, asymptotes, discontinuities and monotonic intervals. Exact where SymPy can solve it, sampled where it cannot — and the sampled lines are labelled approximate, because an approximation dressed as a proof is worse than no answer.
It reports what a function is. It never narrates how the answer was reached.
Interactive: slider()
Bind a parameter and it becomes something the reader can drag. You never write a callback.
a = slider(-3, 3, default=1, name="a")
plot(a*sin(x)) # x is the axis, a is the parameter — inferred
animate(a*sin(x)) # the same, with a play button
The control is Plotly's own, carried inside the figure, so it needs no frontend
package and survives export to one self-contained HTML file — which is the
point if you are handing it to a class. Slider.widget() gives you
mo.ui.slider or ipywidgets instead when you want live recomputation.
Your own numbers: dataset()
The bridge from symbolic to data. Write the model as you would on paper; get the same expression back with its parameters filled in, still SymPy.
readings = dataset({"x": [0, 1, 2, 3], "y": [1.0, 3.1, 4.9, 7.2]})
found = readings.fit(a*x + b) # a = 2.05, b = 0.98, R² = 0.999
diff(found.expr, x) # still an expression — everything works on it
Linear in the parameters is solved exactly; anything else is refined
iteratively. .residuals and .r_squared come back so the fit can be judged
rather than trusted.
plot(readings) # the columns
plot(readings, kind="hist") # their shape
plot(Matrix([[2, 1], [1, 3]])) # a matrix as what it does, with eigenvectors
Numbers, and one file to hand out
table(sin(x), (x, 0, 1)) # the same function, read as values
plot(a*sin(x)).to_html("lesson.html")
to_html() embeds Plotly itself, so the page opens with no network and nothing
installed — sliders included. That is why the slider is built from frames
carried inside the figure rather than a notebook widget: a widget needs a live
kernel, and a file handed to a class does not have one.
Escape hatches
Peeling the wrapper off costs nothing:
f = plot(sin(x)/x)
f.plotly # the Plotly Figure — yours to mutate
f.sympy # the expression
f.numpy # the sampled (x, y) arrays
f.python() # the equivalent code, as a string
Optional: the assistant, and worksheets
from mathslate.ai import ask
print(ask("plot the tangent over one period").code) # you read it, then run it
Claude, OpenAI or Gemini — install one and set its key, either as the provider's
environment variable or with api_key= on ask()/configure(). The core never
imports any of it and works fully offline, which is the point on a school
network; the test suite asserts that in a subprocess. ask() returns code, it
does not execute it. Suggestion.run() validates a restricted MathSlate subset
by default; unrestricted Python requires the explicit unsafe=True escape hatch.
from mathslate.classroom import worksheet
worksheet([
"Where does sin(x)/x go at zero?",
("The graph", plot(sin(x)/x)),
("The numbers", table(sin(x)/x, (x, -1, 1))),
], title="Limits", path="handout.html")
Plotly is embedded once however many figures the page holds.
Pass standalone=False to worksheet(...), .html(), .save() or .preview()
to link the versioned Plotly CDN and keep networked outputs small.
What MathSlate is not
- Not a CAS. All symbolic computation is SymPy's.
- Not a step-by-step derivation engine. There is no
explain(), no "show steps", no generated worked solutions. SymPy provides no general step-by-step engine, and a partial one would be worse than none. - Not a notebook or editor. It runs inside marimo and Jupyter.
- Not Wolfram Language compatible.
- Not a grading or LMS tool.
- Not high-performance numerics — educational scale (≤10⁶ points).
- Not a GUI equation editor. Input is Python code.
Development
python -m venv .venv && .venv/Scripts/pip install -e ".[dev,jupyter,marimo]"
python -m pytest -q
The suite covers each PRD acceptance criterion in its own class, plus the
200-function corpus, a 30-case discontinuity review, a three-environment parity
check, and every example in docs/.
Try it
A guided tour of everything in v1.0, as a notebook you run yourself — 95 cells
of curves, discontinuities, 3D, sliders, analyze(), tables, data fitting,
worksheets and refusals. Nothing in it needs a network.
marimo edit examples/mathslate_tour.py
jupyter lab examples/mathslate_tour.ipynb
The two are the same notebook: both are generated from
examples/build_tour.py, and the test suite executes
the Jupyter one end to end on every run, so a tour cell that stops working
fails the build.
Documentation
- Tutorial — a one-sitting walkthrough, from your first graph to reading real Python. Start here.
- Reference manual — every option, the full dispatch contract, the sampling algorithm, and the exact guarantees.
mathslate_prd_0.3.md— the product requirements document, with an implementation-status section kept up to date.
Every python example in both documents is executed by the test suite in
document order, so the manuals cannot drift from the code.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mathslate-0.1.1.tar.gz.
File metadata
- Download URL: mathslate-0.1.1.tar.gz
- Upload date:
- Size: 391.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eabece614ac3915ab0acebbad08907dd81f8911889215191e0781c68e5c217a4
|
|
| MD5 |
840dd95bfe6a9cd432c82ca9189e8d2b
|
|
| BLAKE2b-256 |
e075466dd416d39606f8b8688f3d23f213260907a6631972aef0ff17ae7a043e
|
Provenance
The following attestation bundles were made for mathslate-0.1.1.tar.gz:
Publisher:
publish.yml on berd2/mathslate
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mathslate-0.1.1.tar.gz -
Subject digest:
eabece614ac3915ab0acebbad08907dd81f8911889215191e0781c68e5c217a4 - Sigstore transparency entry: 2318496827
- Sigstore integration time:
-
Permalink:
berd2/mathslate@fbda94313ab318f3bde878941f7375ef696f35c0 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/berd2
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@fbda94313ab318f3bde878941f7375ef696f35c0 -
Trigger Event:
push
-
Statement type:
File details
Details for the file mathslate-0.1.1-py3-none-any.whl.
File metadata
- Download URL: mathslate-0.1.1-py3-none-any.whl
- Upload date:
- Size: 148.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9fd1f98dfbf08ec11f2ea4ef5a84ccbbe3cd8ac8d7e272001428cf1e96b5ab94
|
|
| MD5 |
37feee614d5417e9ba116f00d2e6ff16
|
|
| BLAKE2b-256 |
ce7f97a9b01467c5f1f9a7989da09cc0ba9de0243de65dd9d1fab222fe6dfff5
|
Provenance
The following attestation bundles were made for mathslate-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on berd2/mathslate
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mathslate-0.1.1-py3-none-any.whl -
Subject digest:
9fd1f98dfbf08ec11f2ea4ef5a84ccbbe3cd8ac8d7e272001428cf1e96b5ab94 - Sigstore transparency entry: 2318496904
- Sigstore integration time:
-
Permalink:
berd2/mathslate@fbda94313ab318f3bde878941f7375ef696f35c0 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/berd2
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@fbda94313ab318f3bde878941f7375ef696f35c0 -
Trigger Event:
push
-
Statement type: