CalculusWithJuliaSquared API

CalculusWithJuliaSquared is a separate, pure-Julia package — a fork of CalculusWithJulia — that these study materials are built on. It has its own repository, versioning, and documentation; it is not maintained as part of Calculus.

Calculus does, however, @reexport it, so its exported helpers (lim, tangent, secant, D, riemann, ∇, …) are available directly once you using Calculus — no separate install or using CalculusWithJuliaSquared needed. That is why its API is documented here: the reference below is generated from the package's own docstrings, so it always tracks the installed version.

The companion notes that exercise this package are a work-in-progress port of the Calculus with Julia notes onto Symbolics / pure Julia:

CalculusWithJuliaSquared.CalculusWithJuliaSquared — Module
CalculusWithJuliaSquared

A personal, pure-Julia fork of CalculusWithJulia.jl to accompany notes at https://calculuswithjulia.github.io on using Julia for topics from the calculus sequence.

This package does two things:

  • It loads a few other packages making it easier to use (and install) the functionality provided by them and

  • It defines a handful of functions for convenience. The exported ones

are unzip, rangeclamp tangent, secant, D (and the prime notation), divergence, gradient, curl, and ∇, along with some plotting functions. The constant e is assigned to exp(1).

  • It supplies the exact symbolic algebra that Symbolics core does not: exact_trig_values

(the special-angle table, so cos(π/6) becomes √3/2 instead of staying unevaluated), factored_poly and poly_factors (factoring over the rationals), and partial_fractions. These stand in for SymPy's automatic special angles, factor and apart. Alongside them, numeric_roots and root_enclosures find roots exactly and report them as floats or as certified intervals, standing in for N.(solve(...)) and sympy.real_roots. divrem, div (÷) and poly_rem divide one polynomial by another, and combine_fractions puts a sum over one fraction bar, standing in for SymPy's together.

  • It displays symbolic mathematics in conventional notation. conventional_latex typesets an

expression the way a mathematics text writes it – term and factor order, fractions, powers, partial fractions, names – and everything symbolic a reader meets displays through it in Quarto, Jupyter and Documenter: an expression, a vector or matrix of them, a tuple holding one, a single root taken from a solution set, and a symlim result. set_conventional_default, get_conventional_default and reset_conventional_default choose, for the session, which letters are the variables, which way a sum runs, how the factors of a product are ordered and which way partial-fraction powers run.

Packages loaded by CalculusWithJuliaSquared

  • The SpecialFunctions is loaded giving access to a few special functions used in these notes, e.g., airyai, gamma

  • The ForwardDiff package is loaded giving access to its derivative, gradient, jacobian, and hessian functions for finding automatic derivatives of functions. In addition, this package defines ' (for functions) to return a derivative (which commits type piracy), ∇ to find the gradient (∇(f)), the divergence (∇⋅F). and the curl (∇×F), along with divergence and curl.

  • The LinearAlgebra package is loaded for access to several of its functions for working with vectors norm, cdot (⋅), cross (×), det.

  • The PlotUtils package is loaded so that its adapted_grid function is available.

  • The Symbolics package is loaded (and reexported) giving access to symbolic math (@variables, etc.) along with symbolic gradient, divergence, and curl methods – pure Julia, no Python dependency.

  • The Nemo package is loaded – imported, not reexported – which switches on Symbolics.symbolic_solve for polynomial equations, and also backs factored_poly, poly_factors, partial_fractions, the polynomial division functions and combine_fractions. The module name itself IS exported, so Nemo.overlaps, Nemo.midpoint and Nemo.radius are available for the balls root_enclosures returns; but no using Nemo is needed downstream, and none of Nemo's own names (derivative, coeff, roots, ...) enter the namespace, where they would collide with Symbolics.

  • The Plots package is loaded (and reexported) providing the plotting interface directly – no separate using Plots needed.

  • The LaTeXStrings package is loaded (and reexported), so L"..." works with no separate using. Plots does not pass this through. It is carried here because it is house standard across the sibling study repos, which means a downstream package or notebook environment need not name it alongside this one.

Several plot recipes are provided to ease the creation of plots in the notes. plotif, trimplot, and signchart are used for plotting univariate functions; plot_polar and plot_parametric are used to plot curves in 2 or 3 dimensions; plot_parametric also makes the plotting og parameterically defined surfaces easier; vectorfieldplot and vectorfieldplot3d can be used to plot vector fields; and arrow is a simplified interface to quiver that also indicates 3D vectors.

The plot_implicit function can plot 2D implicit plots. (It is borrowed from ImplicitPlots.jl, which is avoided, as it has dependencies that hold other packages back.)

Other packages with a recurring role in the accompanying notes:

  • Roots is used to find zeros of univariate functions

  • QuadGK and HCubature are used for numeric integration

Cautions for anyone else using this package

This is a personal study fork, not a registered package: you have to go out of your way to install it, and nobody maintains it for you. Several of its conveniences change Julia's behaviour globally for the whole session, not only for calls into this package. They are collected here so that nobody has to discover them by debugging.

Type piracy: what this package does, and the rule it follows

Type piracy means adding a method to a function you do not own, dispatching on a type you do not own. It matters because Julia's method tables are global: such a method is visible to every package in the session, not only to code that calls into this one. Loading this package can therefore change how unrelated code behaves, which is why the practice is discouraged.

This package commits it six times, each deliberately, and each passing the same test:

The benign test. Every call the pirated method answers is a call that would otherwise have thrown — a MethodError, or an outright error. No working code changes its behaviour; code that used to fail now succeeds. The manual's own carve-out for tightly coupled packages that "separate features from definitions" is the ground these stand on.

A piracy that changed the result of a call that already worked would not pass that test, and none of the six below does.

methodwhat it addswithout it
Base.adjoint on a functionf' returns the derivative (inherited from upstream CalculusWithJulia)MethodError
Base.show for Symbolics.Num and the vector symbolic_solve returns (text/latex and text/html); and, text/html only, for a Vector{Num}, a Matrix{Num}, a single unwrapped expression (BasicSymbolic), and a tuple holding an expression in its first 16 positionsdisplay math, so what a reader meets typesets in Quarto, Documenter and Jupyter instead of printing as Julia syntax or internal type namesplain text: the type, or Julia syntax such as sqrt(3) / 2
Roots.find_zero, find_zeros, ZeroProblem on a symbolic expression or ~ equationsolving find_zero(x^3 - x + 1, (-2, -1)) directly, mirroring the SymPy extension Roots already shipsMethodError
Base.divrem on two Symbolics.NumsEuclidean division of polynomials, a = b*q + r with deg r < deg bMethodError
Base.div on two Symbolics.Nums, and so ÷the quotient of that divisionMethodError
Plots._show for text/html on a Plot{PlotlyBackend}emits the plot body, so an interactive Plotly figure appears inline in a rendered page (inherited from upstream's Plots extension)errors: "only png or svg allowed. got: :html"

A symlim result displays through a type of this package's own, SymlimResult, which is not piracy at all. Four of the six are worth a further word:

  • show adds text/html only, where Symbolics already has text/latex. A Quarto page typesets a value only if it has a text/html method, which is why the containers needed one; Symbolics already has text/latex for Vector{Num}, Matrix{Num} and BasicSymbolic, and a more specific text/latex method here would change a display that works, which is exactly what the benign test forbids. A tuple with nothing symbolic in it, such as (1, 2), gains no method and keeps Julia's display.

  • divrem and div are named, not renamed. A poly_divrem of our own would have avoided the piracy entirely. It is not used because the point being taught is that Julia's generic divrem divides polynomials exactly as it divides integers; a bespoke name would state the opposite. The remainder alone is the exception: rem on two expressions already exists in Symbolics (it builds an unevaluated call), so it cannot be given this meaning, and poly_rem has a name of its own.

  • Plots._show pirates an internal. The leading underscore marks it as private to Plots, so unlike the other five it carries no API stability promise at all: a patch release could rename or remove it, and the symptom would be a plot that silently stops being interactive rather than an error. Measured 2026-09-11 against Plots v1 — _best_html_output_type maps :plotly => :html, and the generic _show(::IO, ::MIME"text/html", ::Plot) has no :html branch, so without this method the call throws. That is what keeps it on the benign side of the test above.

  • find_zero may one day collide. Should Roots add its own Symbolics support, expect a method-overwrite warning on load. That is not a bug to work around: the fix is to delete our block, because upstream's version supersedes it. The same applies to any of the six if the owning package adopts the method itself.

Nemo is imported, and only its name is exported

Nemo does two jobs here: it switches on Symbolics.symbolic_solve for polynomial equations (via Symbolics' SymbolicsNemoExt), and it backs factored_poly, poly_factors, partial_fractions, numeric_roots, root_enclosures, divrem, div, poly_rem and combine_fractions. So loading this package changes what Symbolics itself can do — code that fails without it will succeed with it.

How it is loaded is a deliberate middle course, and knowing which one you are in explains every "why must I qualify this?" question:

what your code seesNemo.overlaps(a, b)
import Nemo alonenothingUndefVarError
import Nemo + export Nemo ← this packagethe module binding, and nothing elseworks
@reexport using Nemoall ~1200 of Nemo's namesworks, at a price

The module name is exported, exactly as ForwardDiff's is, so you can call Nemo.overlaps, Nemo.midpoint, Nemo.radius and Nemo.contains on the balls root_enclosures returns without adding Nemo to your own project. No using Nemo is needed, and none of Nemo's functions enter your namespace.

Why not reexport. Nemo exports roots, degree, derivative, coeff, factor, term and terms. Every one of those collides with something these notes use — Polynomials.roots and Polynomials.degree above all, and derivative is a three-way clash between Nemo, Polynomials and Symbolics. Reexporting would turn working code into ambiguity errors. The same reasoning applies to Symbolics' own public-but-unexported names (derivative, value, get_variables, jacobian, hessian and others): this package does not re-export them either, so they stay qualified as Symbolics.derivative. That is upstream's decision and overriding it would break the collision-avoidance it exists for.

A lot of names arrive at once

Roots, LinearAlgebra, SpecialFunctions, IntervalSets, Symbolics, Plots and LaTeXStrings are reexported; ForwardDiff and Nemo are exported as module names; and e is exported as exp(1). Clashes are real rather than theoretical: alongside SciML's BracketingNonlinearSolve, both Bisection and solve become ambiguous and have to be qualified.

The plotting backend is configured on load. __init__ selects GR and forces headless mode whenever Julia is non-interactive, so that document renders embed figures instead of trying to open a window.

source
CalculusWithJuliaSquared.SignChart — Type
SignChart(f, a, b)

Numerically identifies values of x in [a,b] where f is 0, oo, or undefined.

Displays the output as a sideways interval displaying the sign of f in between these values.

Example

julia> SignChart((x -> sqrt(1 - x^2))', -1, 1)
         ↑
         ⋮
        1.0         is infinite
         ⋮
         +
         ⋮
        0.0         a zero
         ⋮
         -
         ⋮
       -1.0         is infinite
         ⋮
         ↓
source
CalculusWithJuliaSquared.SymlimResult — Type
SymlimResult

What symlim returns: a limit's value and the route that found it.

It behaves as the tuple (value, route) it replaced: r[1], r[2], v, route = r, first, last and length work, r == (1//1, :series) compares as the tuple does, and it prints as the tuple. In a notebook or rendered page it is typeset, route included. Tuple(r) gives the plain tuple.

source
Base.div — Method
div(a::Num, b::Num)
a ÷ b

The quotient of dividing one polynomial by another: the first half of divrem, with the same requirements and the same errors.

julia> using CalculusWithJuliaSquared

julia> @variables x;

julia> (5x^3 + 6x^2 + 2) ÷ (x - 1)
11 + 11x + 5(x^2)

For the remainder use poly_rem, not rem: see its docstring for why.

This method is type piracy

div belongs to Base and Num to Symbolics. It is benign in the sense set out under Cautions in the CalculusWithJuliaSquared module documentation: without it the call throws (Base's generic div for two reals ends in a MethodError), so no working code changes behaviour. ÷ is div itself, so it follows.

source
Base.divrem — Method
divrem(a::Num, b::Num)

Divide one polynomial by another, returning (quotient, remainder).

This is Julia's generic divrem extended to symbolic polynomials, and it means what it means for integers: a == b*q + r, with the remainder of strictly lower degree than b. It is the division algorithm behind rewriting a rational expression as a polynomial plus a proper fraction, which is how a slant asymptote is found.

julia> using CalculusWithJuliaSquared

julia> @variables x;

julia> q, r = divrem(5x^3 + 6x^2 + 2, x - 1)
(11 + 11x + 5(x^2), 13)

Both arguments must be polynomials over the rationals in the same single variable, which is read off the expressions themselves since there is no argument naming it. A constant on one side is fine. Dividing by zero throws DivideError, as it does for numbers.

This method is type piracy

divrem belongs to Base and Num belongs to Symbolics, so adding this method makes it visible to every package in the session. It is deliberate, and benign in the sense set out under Cautions in the CalculusWithJuliaSquared module documentation: without it the call throws MethodError, so no working code changes behaviour. It is named divrem rather than given a name of our own precisely because the point is that Julia's generic divrem divides polynomials just as it divides integers.

For the partial-fraction decomposition of the whole rational expression in one step, see partial_fractions.

source
Base.show — Method
show(io, ::MIME"text/latex", x::Symbolics.Num)
show(io, ::MIME"text/html",  x::Symbolics.Num)

Typeset a symbolic expression as display math in HTML/LaTeX frontends — Quarto, Jupyter, Documenter — parallel to the text/latex show SymPy has built in. Companion methods do the same for the vector of roots Symbolics.symbolic_solve returns, which would otherwise print its full internal type name ahead of the mathematics, and, since v0.16.0, for everything else symbolic a reader meets: a Vector{Num} (a column), a Matrix{Num} (a grid), a single unwrapped expression such as one root taken from a solution set, a tuple holding an expression – \left( x - 3,\ 8 \right) from divrem, with a symbol or boolean in it written as code – and a SymlimResult.

The mathematics is typeset by conventional_latex, so an expression displays the way a text writes it — a x^{2} + b x + c, not c + b x + x^{2} a — and the session defaults set by set_conventional_default (which letters are variables, which way a sum runs) apply to display too. See conventional_latex for the conventions. The solution vector keeps Latexify's array layout, one conventionally typeset root per row.

These emit a clean \[ ... \] through both MIME types, because Latexify's own text/latex output wraps the expression in $$\begin{equation}...\end{equation}$$, which Quarto renders literally. There is no effect in the plain-text REPL, which uses text/plain.

These methods are type piracy

show belongs to Base and Num belongs to Symbolics, so these methods change how symbolic expressions display for every package in the session. They are benign in the sense set out under Cautions in the CalculusWithJuliaSquared module documentation: none of these types has a text/html method without them (and Num and the solution vector no text/latex either), so nothing that previously displayed changes — output that was unavailable becomes available. The containers get text/html only: that is what a Quarto page needs, and Symbolics already has text/latex for them.

The vector and matrix methods are for exactly Vector{Num} and Matrix{Num}. A symbolic array variable (@variables zs[1:3]) is an AbstractVector{Num} too, and it stays plain, as a declaration should; so does an array of three or more dimensions. An echoed @variables x y returns a Vector{Num} and does display as a column: end the cell with ; to hide it.

source
CalculusWithJuliaSquared.D — Function
D(f)
D(f, n::Int)

Function interface to ForwardDiff.derivative; D(f, n) applies it n times, with D(f, 0) returning f itself.

A method for Base.adjoint on functions dispatches to D, so that the notation f' can be used to take the derivative of a function.

julia> using CalculusWithJuliaSquared

julia> f(u) = u^2;

julia> f'(3.0)
6.0
This method is type piracy

adjoint and Function both belong to Base, so adding Base.adjoint(::Function) makes f' mean derivative for every package in the session, not only for code calling into this one. It is inherited from upstream CalculusWithJulia, and it is benign in the sense set out under Cautions in the CalculusWithJuliaSquared module documentation: without it the call throws MethodError, so no working code changes behaviour.

One caveat it does introduce: ' on a collection of functions still means the ordinary transpose. [sin, cos]' is an Adjoint{Function, Vector{Function}}, not [sin', cos']. Write D.([sin, cos]) when derivatives are meant.

source
CalculusWithJuliaSquared._side — Method

What the samples on ONE side of c say. s is +1 for the right, -1 for the left.

  • nothing — ex cannot be evaluated on that side, so its domain does not reach c from that direction. Not a failure: log at 0 is exactly this.
  • (:finite, y) — the samples are settling on y.
  • (:diverges, ±Inf) — the increments are not collapsing and hold one sign.
  • (:erratic, NaN) — evaluable, but neither settling nor monotone. No opinion.

Testing the increments rather than the magnitude is what catches logarithmic divergence, which grows by a constant per decade and never passes a fixed threshold.

source
CalculusWithJuliaSquared._usable — Method

Is a substituted result usable as a limit?

A numeric result must be finite. A symbolic result is fine too — 5x^4 is the honest answer to a limit taken in h — provided it no longer mentions the limit variable and grounds to a finite value. Requiring a Number here was a bug: it threw away correct answers for every limit carrying a parameter, and sent them to the Gruntz engine instead.

source
CalculusWithJuliaSquared.arrow! — Method

arrow(p, v)

Add vector, v, to plot anchored at point p.

Example

Fn = parametric(t -> [2cos(t), 3sin(t)])
Fnp = t -> ForwardDiff.derivative(Fn, t)
p = plot(Fn, 0, 2pi, legend=false)
for t in 0:pi/4:pi
   arrow!(Fn(t), Fnp(t))
end
p
source
CalculusWithJuliaSquared.arrow! — Method

arrow(p, v)

Add vector, v, to plot anchored at point p.

Example

Fn = parametric(t -> [2cos(t), 3sin(t)])
Fnp = t -> ForwardDiff.derivative(Fn, t)
p = plot(Fn, 0, 2pi, legend=false)
for t in 0:pi/4:pi
   arrow!(Fn(t), Fnp(t))
end
p
source
CalculusWithJuliaSquared.arrow! — Method

arrow!(p, v)

Add the vector v to the plot anchored at p.

This would just be a call to quiver, but there is no 3-D version of that. As well, the syntax for quiver is a bit awkward for plotting just a single arrow. (Though efficient if plotting many).

using Plots
r(t) = [sin(t), cos(t), t]
rp(t) = [cos(t), -sin(t), 1]
plot(unzip(r, 0, 2pi)...)
t0 = 1
arrow!(r(t0), rp(t0))
source
CalculusWithJuliaSquared.arrow — Method

arrow(p, v)

Add vector, v, to plot anchored at point p.

Example

Fn = parametric(t -> [2cos(t), 3sin(t)])
Fnp = t -> ForwardDiff.derivative(Fn, t)
p = plot(Fn, 0, 2pi, legend=false)
for t in 0:pi/4:pi
   arrow!(Fn(t), Fnp(t))
end
p
source
CalculusWithJuliaSquared.combine_fractions — Method
combine_fractions(ex)

Put a sum of fractions – or a polynomial plus fractions – over one fraction bar, cancelling every common factor. SymPy spells this together; it undoes partial_fractions (SymPy's apart).

The numerator is multiplied out and the denominator is left factored:

julia> using CalculusWithJuliaSquared

julia> @variables x a;

julia> combine_fractions(x^3 + 2x^2 + 6x + 12 + 29/(x - 2))
(5 + 2(x^2) + x^4) / (-2 + x)

julia> combine_fractions((x^2 - a^2)/(x - a) + 1)
1 + a + x

Cancellation reaches through parameters, as in the second example, and through non-polynomial subterms: sin(x)/(sin(x)^2 - 1) + 1/(sin(x) + 1) becomes (2sin(x) - 1)/((sin(x) - 1)(sin(x) + 1)). Use expand on the result for a multiplied-out denominator.

Coefficients must be exact, as in partial_fractions: a float such as 0.5 is refused with an ArgumentError (an integer-valued float such as 2.0 is accepted).

Limits

Every subterm that is not a sum, product, quotient or integer power – sin(x), sqrt(2), π, x^n – is treated as an independent unknown. So identities between such terms are not used: (x^2 - 2)/(x - sqrt(2)) is not reduced to x + sqrt(2), since that needs sqrt(2)^2 = 2, and sin(x)^2 + cos(x)^2 is not 1.

source
CalculusWithJuliaSquared.conventional_latex — Method
conventional_latex(ex; variables, order, factor_order, partial_fraction_powers) -> String

Typeset a symbolic expression the way a mathematics text writes it.

Latexify renders a Symbolics expression faithfully, and faithfully means printing the canonical algebraic form – storage order, separate rational factors, names in typewriter – rather than conventional notation. This walks the expression and emits the LaTeX directly. It is also what every symbolic expression, and the solutions symbolic_solve returns, use for display once this package is loaded, so the conventions below are what a reader sees.

The result is the body of a math expression, with no delimiters, so a caller wraps it as it needs – inline, display, or as a cell of a table.

julia> @variables x a b c;

julia> conventional_latex(a*x^2 + b*x + c)
"a x^{2} + b x + c"

julia> conventional_latex((2//3) * Symbolics.Num(pi))
"\\frac{2 \\pi}{3}"

Fractions

latexifyconventional_latex
rational coefficient\frac{2}{3} ~ \pi\frac{2 \pi}{3}
rational numerator\frac{\frac{1}{3}}{2 + x}\frac{1}{3 (x + 2)}
common denominator\frac{\sqrt{41}}{4} + \frac{3}{4}\frac{3 + \sqrt{41}}{4}
negative power\left( \frac{1}{x} \right)^{2}\frac{1}{x^{2}}

When every term of a sum is a fraction over the same denominator they are written over one bar, which is what turns the pieces of the quadratic formula into the formula rather than two fractions added together. Terms over different denominators are left alone – otherwise a partial-fraction decomposition, whose entire point is separate denominators, would be recombined into the thing it was decomposed from.

Symbolics stores x^(-2) as (1/x)^2 – the two are one expression – so a power of a reciprocal is written as one fraction, \frac{1}{x^{2}}, and a product containing one puts it in the denominator: a*x^(-2) is \frac{a}{x^{2}}.

A fraction never sits inside a numerator: a numerator over its own number merges that number into the denominator, \frac{2 x - 1}{25 \left( x^{2} - x - 1 \right)}. A numerator whose every term is negative puts the minus in front when the denominator contains a variable and the numerator has no root in it, -\frac{x + 1}{x^{2} + 1}. Over a number, or with a root, the signs stay inside – \frac{-x - 1}{2}, \frac{-1 - \sqrt{3}}{2}, \frac{-b - \sqrt{b^{2} - 4 a c}}{2 a} – so the two roots of any quadratic look alike. The rule is judged on the expression, not on how it was built: the same number reads the same whether it came from symbolic_solve or was typed as a quotient. When both halves lead with a minus – judged by the highest-degree term, so 1 - x leads with -x – both are negated: \frac{x - 1}{x + 1}, not \frac{1 - x}{-x - 1}.

The order of the terms of a sum

A sum is ordered by these keys, each deciding only between terms the ones before it tie:

  1. Degree in the variables, highest first. The variables are, by default, the letters a text uses for them: n r t u v w x y z θ (θ also spelled theta). Every other name is a constant, and so is a subscripted name, whatever its letter – x0, x_0 and x₀ are fixed values, like an expansion point or an initial condition.
  2. Total degree, highest first.
  3. Alphabetical, ignoring case, higher power first letter by letter: k^{2} - 2 k s + s^{2}.

So a*x^2 + b*x + c is a x^{2} + b x + c rather than being sorted by the degree of a x^2 as a whole, and x - b*pi is x - \pi b.

Two exceptions follow convention rather than degree:

  • A term that is nothing but a radical comes last, whatever its degree: \frac{-b + \sqrt{b^{2} - 4 c}}{2}, \frac{3 + \sqrt{41}}{4}, and the imaginary part of a complex root. A radical multiplied by a variable is ordered normally: \sqrt{2} x + 1.
  • A sum of two terms does not open with a minus: 1 - x, 1 - x^{2}, 100 - 16 t^{2}, not -x + 1, and two radicals swap too: \sqrt{1 + \frac{\sqrt{2}}{2}} - \sqrt{1 - \frac{\sqrt{2}}{2}}. With three or more terms the order above stands: -x^{2} + 2 x - 1.

Partial fractions

A sum with a term over a polynomial factor – what partial_fractions returns – is written the way the decomposition is taught: the polynomial part first, then the fractions grouped by factor, in the factor order of a product (below), each factor's powers rising:

x - 4 + \frac{40}{3 \left( x + 3 \right)} + \frac{2}{3 \left( x - 3 \right)} and -\frac{2}{25 \left( x - 3 \right)} + \frac{1}{5 \left( x - 3 \right)^{2}} + \frac{2}{5 \left( x - 3 \right)^{3}} + \frac{2 x - 1}{25 \left( x^{2} - x - 1 \right)}.

Here the factor order wins over "two terms do not open with a minus", so the template \frac{A}{x - 1} + \frac{B}{x - 2} reads -\frac{1}{x - 1} + \frac{1}{x - 2}. Only a factor that is a polynomial in one variable with rational coefficients groups a sum, so a Laurent polynomial, x + 1 + \frac{1}{x}, and a difference quotient, \frac{1}{x + h} - \frac{1}{x}, keep the order above.

The input form makes no difference: 1 - x and -x + 1 construct the same expression.

Choosing the variables and the direction

The keywords override the session defaults for one call:

  • variables – the names to treat as variables, replacing the default set. Symbolic variables or Symbols. Use it when an expression's variable is not one of the default letters:

    julia> @variables s k;
    
    julia> conventional_latex(expand((s - k)^2))                     # no default variable
    "k^{2} - 2 k s + s^{2}"
    
    julia> conventional_latex(expand((s - k)^2); variables = [s])
    "s^{2} - 2 k s + k^{2}"
  • order – :descending (the default, as a polynomial is written) or :ascending, as a Taylor polynomial or power series is written:

    julia> conventional_latex(Symbolics.taylor(exp(x), x, 0:3); order = :ascending)
    "1 + x + \\frac{x^{2}}{2} + \\frac{x^{3}}{6}"
  • factor_order – how the bracketed sums of a product are ordered: :roots (the default) puts lower degree first and linear factors by their root on the number line, as a sign chart reads, \left( x + 3 \right) \left( x + 1 \right) \left( 2 x - 1 \right) \left( x - 3 \right); :degree puts monic factors before the rest within a degree, \left( x + 3 \right) \left( x + 1 \right) \left( x - 3 \right) \left( 2 x - 1 \right).

  • partial_fraction_powers – :ascending (the default), \frac{A}{x - 3} + \frac{B}{\left( x - 3 \right)^{2}}, or :descending, highest power first, as a Laurent expansion is written.

To change any of these for display – which cannot be passed a keyword – use set_conventional_default; reset_conventional_default undoes it.

The order of the factors of a product

Numbers, then constants (\pi, \sqrt{2}), then letters alphabetically with the variables after the rest, then bracketed sums, then functions: 3 h x, 2 \pi x, a E^{2}, 2 x \left( x - 1 \right) \left( x - 2 \right), x^{2} e^{x}. Bracketed sums go by factor_order (above): by default lower degree first, then a linear factor by its root on the number line, \left( x + 1 \right) \left( x - 1 \right) \left( x^{2} + 1 \right); a factor whose root is a letter, x - a, follows the numeric ones. A power sorts by its base. Functions go in textbook order – radicals and absolute values, exponentials, sin cos tan cot sec csc, inverse trigonometric, hyperbolic, logarithms, then any other alphabetically, and derivatives last – so 2 \sin x \cos x, e^{x} \sin x and u\left( x \right) \frac{d v\left( x \right)}{dx}. The same function twice goes simpler argument first: \sin\left( x \right) \sin\left( 2 x \right). Elements of an array go by name, then index: \mathit{xs}_{0} \mathit{xs}_{1}.

Powers

A power's base is bracketed unless it is a symbol or a non-negative number: \left( a x \right)^{y}, \left( \frac{x}{2} \right)^{2}, \left( e^{x} \right)^{2}, \left( \sqrt{x} \right)^{3}, but x^{2}, 2^{x}, \pi^{x}. A power of a trigonometric or hyperbolic function is written on the name, \sin^{2}\left( x \right), as Latexify does.

Functions and their arguments

A function is typeset in Latexify's own shape for it – \sin\left( x \right), e^{x}, \left|x\right|, \log_{10}\left( x \right) – with its arguments typeset by all of the rules here: \cos\left( \frac{\pi x}{2} \right), e^{-\frac{x^{2}}{2}}. A function Latexify names with a plain word is set upright as an operator, \operatorname{sign}, using LaTeX's own command where there is one: \max, \min. So is the placeholder for roots symbolic_solve cannot find: \operatorname{roots\_of}\left( x^{5} - x + 1, x \right). A derivative is always in fraction form, \frac{d \sin\left( x \right)}{dx} or \frac{d^{2} f}{dx^{2}}: the operator form \frac{d}{dx} u v would read as the derivative of the whole product. The d is italic, as calculus texts write it, rather than Latexify's upright \mathrm{d}.

Complex numbers

The real part first, the imaginary unit last: -1 + 2 i, -\frac{1}{2} + \frac{\sqrt{3}}{2} i. Two or more imaginary terms are written as one imaginary part, as Cardano's roots are: -\frac{u}{2} - \frac{v}{2} + \left( \frac{\sqrt{3} u}{2} - \frac{\sqrt{3} v}{2} \right) i. Symbolics computes Cardano's halves as the float 0.5; an exact half in a complex coefficient is shown as the fraction it stands for, while every other float, and every real one, stays a float.

Names

A name longer than one character is split into a base and a subscript – after an _, as Unicode subscript characters, or as trailing digits – and each piece is written as a text would:

declaredtypeset
x0, x_0, x₀x_{0}
theta, θ\theta
rho_0, lambda1\rho_{0}, \lambda_{1}
f_x, x_alphaf_{x}, x_{\alpha}
v_maxv_{\mathrm{max}}
height\mathit{height}
xs[1]\mathit{xs}_{1}

A spelled Greek name becomes the letter. A longer base is italic, because italic marks a quantity; a longer subscript is upright, because a descriptive subscript is a label, while a one-letter or numeric subscript stays italic.

Everything else

The ordering is total, so a rebuild renders a given expression identically; it will not quietly rearrange a published page. A leading negative never follows a space, which Pandoc would refuse to open as inline math. Any expression shape this does not recognise is passed to Latexify unchanged, so an unfamiliar function renders as it always did rather than failing. A string is returned as it is.

source
CalculusWithJuliaSquared.exact_trig_values — Method
exact_trig_values(ex)

Replace every trigonometric function applied to a rational multiple of π in ex with its exact value, leaving the rest of the expression alone.

Symbolics has no table of special angles, so cos(Num(π)/6) simply stays unevaluated, and simplify makes matters worse by folding the π to a float. This walks the expression instead and substitutes the exact value, which is an ordinary symbolic term: sqrt(3)/2 prints as √3/2 and typesets as such.

Handles sin, cos, tan, csc, sec and cot at any rational multiple of π with denominator 1, 2, 3, 4 or 6, in every quadrant.

Anything else is returned unchanged – an angle that is not such a multiple, an argument still carrying a free variable, and the poles (tan(π/2), cot(0)), which have no value to give. Note that the angle has to be exact going in: a Float64 that merely rounds to π/6 is refused rather than guessed at.

Examples

julia> PI = Symbolics.Num(pi);

julia> exact_trig_values(cos(PI/6))
sqrt(3) / 2

julia> exact_trig_values.(cos.([0, PI/6, PI/4, PI/3, PI/2]))
5-element Vector{Num}:
         1
 sqrt(3) / 2
 sqrt(2) / 2
       1//2
         0

julia> exact_trig_values(cos(Symbolics.Num(0.5235987755982988)))   # a float, not π/6
cos(0.5235987755982988)

See also factored_poly, partial_fractions.

source
CalculusWithJuliaSquared.factored_poly — Method
factored_poly(ex, var)

Factor the univariate polynomial ex over the rationals and return the factored expression.

This is poly_factors multiplied back together, and carries the same restrictions: the factorisation is over the rationals, so x^2 - 2 is returned unchanged, and a non-polynomial or non-rational coefficient throws.

Examples

julia> @variables x;

julia> factored_poly(x^3 - 6x^2 + 11x - 6, x)
(-3 + x)*(-2 + x)*(-1 + x)

julia> factored_poly(x^2 - 2, x)
-2 + x^2

See also poly_factors, partial_fractions.

source
CalculusWithJuliaSquared.fisheye — Method
fisheye(f)

Transform f defined on (-∞, ∞) to a new function whose domain is in (-π/2, π/2) and range is within (-π/2, π/2). Useful for finding all zeros over the real line. For example

f(x) = 1 + 100x^2 - x^3
find_zeros(f, -100, 100) # empty just misses the zero found with:
find_zeros(fisheye(f), -pi/2, pi/2) .|> tan  # finds 100.19469143521222, not perfect but easy to get

By Gunter Fuchs.

source
CalculusWithJuliaSquared.fubini — Method
fubini(f, [zs], [ys], xs; rtol=missing, kws...)

Integrate f of 1, 2, or 3 input variables.

The zs may depend (x,y), the ys may depend on x

Examples

# integrate over the unit square
fubini((x,y) -> sin(x-y), (0,1), (0,1))

# integrate over a triangle
fubini((x,y) -> 1, (0,identity), (0,1 ))

#
f(x,y,z) = x*y^2*z^3
fubini(f, (0,(x,y) ->  x+ y), (0, x -> x), (0,1))

!!! Note This uses nested calls to quadgk. The use of hcubature is recommended, typically after a change of variables to make a rectangular domain. The relative tolerance increases at each nested level.

source
CalculusWithJuliaSquared.lim — Method
lim(f, c; n=6, m=1, dir="+-")
lim(f, c, dir; n-5)

Means to generate numeric table of values of f as h gets close to c.

  • n, m: powers of 10 to add (subtract) to (from) c.
  • dir: Either "+-" (show left and right), "+" (right limit), or "-" (left limit). Can also use functions +, -, ±.

Example:

julia> f(x) = sin(x) / x
f (generic function with 1 method)

julia> lim(f, 0)
 0.1        0.9983341664682815
 0.01       0.9999833334166665
 0.001      0.9999998333333416
 0.0001     0.9999999983333334
 1.0e-5     0.9999999999833332
 1.0e-6     0.9999999999998334
   ⋮          ⋮
   c          L?
   ⋮          ⋮
-1.0e-6     0.9999999999998334
-1.0e-5     0.9999999999833332
-0.0001     0.9999999983333334
-0.001      0.9999998333333416
-0.01       0.9999833334166665
-0.1        0.9983341664682815
source
CalculusWithJuliaSquared.newton_plot! — Method
newton_plot!(f, x0; steps=5, annotate_steps::Int=0, kwargs...)

Add trace of Newton's method to plot.

  • steps: how many steps from x0 to illustrate
  • annotate_steps::Int: how may steps to annotate
source
CalculusWithJuliaSquared.numeric_roots — Method
numeric_roots(ex, var; real_only=false)

Every root of the univariate polynomial ex, as a floating-point number.

Returns ComplexF64 values by default, or Float64 values when real_only=true, in which case the non-real roots are dropped. This is the counterpart of SymPy's N.(solve(...)) and, with real_only=true, of sympy.real_roots.

Roots are found exactly, as algebraic numbers, and rounded only on the way out. So they are returned even for polynomials that have no formula in radicals, where symbolic_solve can only answer roots_of(...):

julia> using CalculusWithJuliaSquared

julia> @variables x;

julia> numeric_roots(x^2 - 2, x; real_only=true)
2-element Vector{Float64}:
 -1.4142135623730951
  1.4142135623730951

Two guarantees worth relying on when rendering a document:

  • The order is stable. Real results are sorted ascending; complex results are sorted by real part, then imaginary part. The same input renders the same way every time.
  • Repeated roots repeat. A double root appears twice, so with real_only=false the number of values always equals the degree.

ex must be a polynomial in var alone with rational coefficients; anything else throws rather than guessing. A constant is refused, since it has either no roots or all of them.

For real roots with an error bound you can reason about – rather than a float whose accuracy is unstated – use root_enclosures.

See also poly_factors, factored_poly.

source
CalculusWithJuliaSquared.partial_fractions — Method
partial_fractions(ex, var)

Decompose the rational expression ex into partial fractions over the rationals, returning the decomposition as a symbolic sum (SymPy spells this apart).

ex is a rational function of var: a quotient p/q of polynomials, a sum of such terms (x + 1/(x + 1), combined first with combine_fractions), or a polynomial, which is returned unchanged. Coefficients must be rational.

The polynomial part of an improper fraction is included in the sum, and repeated factors in the denominator produce the expected higher-power terms. Each denominator is factored over the integers, as a text writes it: 2x + 1, never x + \frac{1}{2}.

Examples

julia> @variables x;

julia> partial_fractions(1/((x-1)*(x-2)), x)
1 / (-2 + x) + -1 / (-1 + x)

julia> partial_fractions((x+3)/((x-1)^2*(x+2)), x)
(-1//9) / (-1 + x) + (1//9) / (2 + x) + (4//3) / ((-1 + x)^2)

julia> partial_fractions(x^3/((x-1)*(x-2)), x)      # improper: polynomial part included
3 + x + -1 / (-1 + x) + 8 / (-2 + x)

julia> partial_fractions(1/((2x+1)*(3x-1)), x)       # integer factors below
(-2//5) / (1 + 2x) + (3//5) / (-1 + 3x)

julia> partial_fractions(x + 1/(2x + 1), x)          # a sum is combined first
x + 1 / (1 + 2x)

On a page each term is typeset as a text writes it, e.g. $-\frac{2}{5(2x + 1)} + \frac{3}{5(3x - 1)}$.

See also factored_poly, exact_trig_values.

source
CalculusWithJuliaSquared.plot_implicit_surface — Function
Visualize `F(x,y,z) = c` by plotting assorted contour lines

This graphic makes slices in the x, y, and/or z direction of the 3-D level surface and plots them accordingly. Which slices (and their colors) are specified through a dictionary.

Examples:

F(x,y,z) = x^2 + y^2 + x^2
plot_implicit_surface(F, 20)  # 20 slices in z direction
plot_implicit_surface(F, 20, slices=Dict(:x=>:blue, :y=>:red, :z=>:green), nlevels=6) # all 3 shown

# A heart
a,b = 1,3
F(x,y,z) = (x^2+((1+b)*y)^2+z^2-1)^3-x^2*z^3-a*y^2*z^3
plot_implicit_surface(F, xlims=-2..2,ylims=-1..1,zlims=-1..2)

Note: Idea from.

Not exported.

source
CalculusWithJuliaSquared.plot_parametric — Method
plot_parametric(ab, r; kwargs...)
plot_parametric!(ab, r; kwargs...)
plot_parametric(u, v, F; kwargs...)
plot_parametric!(u, v, F; kwargs...)

Make a parametric plot of a space curve or parametrized surface

The intervals to plot over are specifed using a..b notation, from IntervalSets

source
CalculusWithJuliaSquared.poly_factors — Method
poly_factors(ex, var)

Factor the univariate polynomial ex over the rationals and return its factors as a vector, each repeated according to its multiplicity, with any constant factor first.

Use this where a count is wanted – length(poly_factors(ex, x)) – and factored_poly where the factored expression itself is wanted.

Factoring is over the rationals, so x^2 - 2 comes back as a single factor: the factorisation (x-√2)(x+√2) exists but is not rational. symbolic_solve will give those roots.

The factors come in the order factored_poly shows them: the constant first, then lower degree first, and linear factors by their root on the number line, as a sign chart reads. set_conventional_default(factor_order = :degree) switches both to monic factors first. The order is fixed, so repeated runs and repeated renders agree.

Throws if ex is not a polynomial in var alone, or if any coefficient is not rational.

Examples

julia> @variables x;

julia> poly_factors(x^3 - 6x^2 + 11x - 6, x)
3-element Vector{Num}:
 -1 + x
 -2 + x
 -3 + x

julia> poly_factors(2x^4 + x^3 - 19x^2 - 9x + 9, x)     # roots -3, -1, 1/2, 3
4-element Vector{Num}:
   3 + x
   1 + x
 -1 + 2x
  -3 + x

julia> length(poly_factors(x^12 - 1, x))
6

julia> poly_factors(x^2 - 2, x)          # irreducible over the rationals
1-element Vector{Num}:
 -2 + x^2

See also factored_poly, partial_fractions.

source
CalculusWithJuliaSquared.poly_rem — Method
poly_rem(a, b)

The remainder of dividing one polynomial by another: the second half of divrem, with the same requirements and the same errors.

julia> using CalculusWithJuliaSquared

julia> @variables x;

julia> poly_rem(x^5 - x + 1, x^2 - x + 1)
2 - 2x
Not `rem`

rem(a, b) on two symbolic expressions does NOT divide polynomials. Symbolics already defines it to build an unevaluated symbolic call – rem(x^2 + 1, x) stays rem(x^2 + 1, x), and even substitute(rem(n, 2), n => 7) stays rem(7, 2) – so it is not a gap this package could fill. Redefining another package's method fails precompilation, which is why this function has a name of its own, unlike divrem and div.

source
CalculusWithJuliaSquared.rangeclamp — Function
rangeclamp(f, hi=20, lo=-hi; replacement=NaN)

Modify f so that values of f(x) outside of [lo,hi] are replaced by replacement.

Examples

f(x) = 1/x
plot(rangeclamp(f), -1, 1)
plot(rangeclamp(f, 10), -1, 1) # no `abs(y)` values exceeding 10
source
CalculusWithJuliaSquared.riemann — Method
riemann(f, a, b, n; method="right"

Compute an approximations to the definite integral of f over [a,b] using an equal-sized partition of size n+1.

method: "right" (default), "left", "trapezoid", "simpsons", "ct", "m̃" (minimum over interval), "M̃" (maximum over interval)

Example:

f(x) = exp(x^2)
riemann(f, 0, 1, 1000)   # default right-Riemann sums
riemann(f, 0, 1, 1000; method="left")       # left sums
riemann(f, 0, 1, 1000; method="trapezoid")  # use trapezoid rule
riemann(f, 0, 1, 1000; method="simpsons")   # use Simpson's rule
source
CalculusWithJuliaSquared.riemann_plot — Method
riemann_plot!(f, a, b, n; method="method", fill, kwargs...)
riemann_plot(f, a, b, n; method="method", fill, kwargs...)

Add visualization of riemann sum in a layer.

  • method: one of right, left, trapezoid, simpsons
  • fill: to specify fill color, something like ("green", 0.25, 0) will fill in green with an alpha transparency.
source
CalculusWithJuliaSquared.root_enclosures — Method
root_enclosures(ex, var; bits=128)

The real roots of the univariate polynomial ex, each as an interval that is guaranteed to contain one.

Returns Nemo.ArbFieldElem values – ball arithmetic, printed as [midpoint +/- radius] – sorted ascending. Each is a rigorous enclosure computed from the exact algebraic root, not a float with an informal error estimate.

The point of an enclosure is that it supports proof rather than eyeballing. Two nearby floats cannot tell you whether there are two roots or one root computed twice; two enclosures that do not overlap can only come from two distinct roots:

julia> @variables s;

julia> es = root_enclosures(s^15 - 16129s^2 + 254s - 1, s; bits=96);

julia> Nemo.overlaps(es[1], es[2])      # disjoint => genuinely two roots
false

bits sets the working precision, and is a real dial rather than decoration. On the polynomial above the first two enclosures still overlap at 53 bits – the honest answer being "these cannot be told apart yet" – and separate only at 64. That threshold is the point of the whole exercise: 53 bits is exactly the precision of a Float64 mantissa, so no computation carried in double precision can establish that this cluster is two roots rather than one. Nothing built on Float64 interval endpoints could do it either, since their radius bottoms out at an ulp.

Useful Nemo functions for working with the results – Nemo is imported by this package but not reexported, so they must be qualified:

  • Nemo.overlaps(a, b) – do two enclosures intersect?
  • Nemo.contains(ball, x) – is x inside? Note: there is no method for a Float64 argument; wrap it first, as Nemo.contains(b, Nemo.ArbField(96)(6.94)).
  • Nemo.midpoint(ball), Nemo.radius(ball) – the two halves of [m +/- r].

Arithmetic works and stays certified (b + 1.0, b^2, sin(b), sqrt(b)), and Float64.(root_enclosures(...)) drops back to plain numbers when that is all you need.

Only real roots are returned; a ball is real by construction. For the complex ones, or for plain floats, use numeric_roots.

source
CalculusWithJuliaSquared.secant — Method
secant(f::Function, a, b)

Returns a function describing the secant line to the graph of f at x=a and x=b.

Example. Where does the secant line intersect the y axis?

f(x) = sin(x)
a, b = pi/4, pi/3
sl = secant(f, a, b)  # or sl(x) = secant(f, a, b)(x) to use a generic function
sl(0)
source
CalculusWithJuliaSquared.set_conventional_default — Method
set_conventional_default(; variables, order, factor_order, partial_fraction_powers) -> NamedTuple

Change, for the rest of the session, the defaults conventional_latex uses – and therefore how every symbolic expression displays, since a notebook or document cell has no way to pass a keyword to its own display.

  • variables – the names treated as variables when ordering a sum. A collection of symbolic variables or Symbols ([s, θ] or [:s, :θ]); it replaces the default set rather than adding to it.
  • order – :descending (highest degree first, as a polynomial is written) or :ascending (lowest first, as a Taylor polynomial or power series is written).
  • factor_order – :roots (linear factors by root, the default) or :degree (monic factors first); see conventional_latex.
  • partial_fraction_powers – :ascending (the default) or :descending.

Options not passed keep their current value, so two calls setting one option each combine, as they do for Latexify's own set_default. Returns the resulting defaults, as get_conventional_default does. An unknown option or order throws an ArgumentError and changes nothing.

This is global state for the session, shared by every package that displays a Num.

julia> @variables s k;

julia> set_conventional_default(; variables = [s]);

julia> conventional_latex(expand((s - k)^2))
"s^{2} - 2 k s + k^{2}"

julia> reset_conventional_default();
source
CalculusWithJuliaSquared.sign_chart — Method

sign_chart(f, a, b; atol=1e-4)

Create a sign chart for f over (a,b). Returns a collection of named tuples, each with an identified zero or vertical asymptote and the corresponding sign change. The tolerance is used to disambiguate numerically found values.

Example

julia> sign_chart(x -> (x-1/2)/(x*(1-x)), 0, 1)
3-element Vector{NamedTuple{(:zero_oo_NaN, :sign_change)}}:
 (zero_oo_NaN = 0.0, sign_change = an endpoint)
 (zero_oo_NaN = 0.5, sign_change = - to +)
 (zero_oo_NaN = 1.0, sign_change = an endpoint)
Warning

This uses find_zeros to find zeros of f and x -> 1/f(x). The find_zeros function is a hueristic and can miss answers.

source
CalculusWithJuliaSquared.symlim — Method
symlim(ex, v, c; side = :both, cancel = true, check = true, n = 8, secs = 10)

Symbolic limit of the expression ex as the variable v approaches c.

Returns (value, route), where route names the method that produced the answer, as a SymlimResult, which behaves as that tuple and is typeset in a notebook or rendered page. A value of nothing means every method declined — an honest refusal rather than a guess.

Routes, in the order they are tried

routefires whenexact?
:substitutionex is defined at cyes
:cancela removable singularity simplify_fractions clearsyes
:seriesstill indeterminate; leading-order Taylor comparisonyes
:reciprocalc is infinite and ex is a ratio of polynomialsyes
:gruntzc is infinite, or nothing above appliedfloat
:divergent_numericthe value grows without bound±Inf
:squeezeinterval enclosures, c excluded, collapse to a pointonly when the enclosure has zero width; else float
:compositionlim f(h) = f(lim h) for f continuous at the inner limitinherits
:sides_disagreeleft and right limits both exist and differ—
:undefined_on_sidea side was asked for that ex does not reach—
:parameter_dependentthe answer turns on a free parameter, or a numeric route would have to invent a value for one—
:unresolvednothing worked—
julia> @variables x::Real;

julia> symlim((x^2 - 1)/(x - 1), x, 1)
(2, :cancel)

julia> symlim(sin(x)/x, x, 0)
(1//1, :series)

julia> symlim(log(x)/x, x, Inf)
(0, :gruntz)

julia> symlim(x * sin(1/x), x, 0)
(0.0, :squeeze)

julia> symlim(abs(x)/x, x, 0)
(nothing, :sides_disagree)

julia> symlim(abs(x)/x, x, 0; side = :right)
(1//1, :series)

julia> symlim(abs(x)/x, x, 0; side = :left)
(-1//1, :series)

julia> symlim(floor(x), x, 0; side = :right), symlim(floor(x), x, 0; side = :left)
((0, :substitution), (-1, :squeeze))

julia> @variables c::Real;

julia> symlim(3x^2 + c, x, 0; side = :right)
(c, :substitution)

x·sin(1/x) has the limit 0 by the squeeze theorem, which the :squeeze route establishes: interval arithmetic bounds the function on a shrinking neighbourhood of 0, and the enclosures collapse to a point. Where they do not collapse the route declines — sin(x) at infinity encloses to [-1, 1] at every scale, correctly, because it has no limit. The boxes exclude c itself, since a limit never consults f(c); that is what lets floor resolve from the left, where the value at 0 is not the limit.

:substitution is the definition of continuity, computed: the value at c exists and the function approaches it. The route says so. floor from the right is :substitution and from the left is not, which is the statement that floor is right-continuous at the integers.

A free parameter rides through the exact routes — c, 5x^4, 1/x are all honest answers to limits taken in another variable. A route that can only produce a number (:squeeze, :divergent_numeric, the engine at a finite point) is not allowed to invent a value for one, and refuses with :parameter_dependent instead.

Sidedness

A limit exists at c only if the left and right limits both exist and agree, so side = :both refuses when they demonstrably do not: abs(x)/x and 1/x at 0 are jumps, not limits. Ask for :left or :right to get the one-sided answer.

Refusing needs positive evidence from both sides. Where ex simply does not reach c from one direction — log(x) at 0, x^x at 0, anything at the edge of its domain — the defined side is the answer, which is the ordinary reading of lim_{x→0} log(x) = -∞, and every route is asked for that side's limit. Only a genuine two-sided disagreement is refused.

Why the ordering matters

Symbolics computes no limits itself; the available engine, SymbolicLimits, implements the Gruntz algorithm for log-exponential asymptotics at infinity. It is excellent at that and unreliable elsewhere. As of v1.1.5 it does not cancel common factors, so limit((x^2-1)/(x-1), x, 1) returns 0 rather than 2 — with its assumption set reporting confidence. Cancelling and series comparison are tried first because they are both exact and dependable; the engine is asked last, and only for the work it was built for.

Keyword arguments

  • cancel = true — clear removable singularities before the engine ever sees the expression. Setting false skips that stage only; the series stage below it will usually still find the right answer, so this is not a way to observe the underlying engine misbehaving. Use check = false for that, or call SymbolicLimits.limit directly.
  • side = :both — :left or :right for a one-sided limit. See Sidedness.
  • check = true — require every route's answer to agree with the numeric evidence on the side being approached, and move to the next route when it does not. Leave this on: the failure modes it guards against are silent, so nothing else will catch them. Setting false returns the first route's answer unchecked, which is the way to watch the engine misbehave.
  • n — highest order used by the series route.
  • secs — deadline for the Gruntz stage, which is the only one that can fail to terminate. Start Julia with -t 2 or more, or the watchdog cannot run: a hung call never yields, so a same-thread timer would never fire.

Known limits

  • Expansion points involving π. Symbolics.taylor converts π to a float and then to a rational, so a series about π/2 carries a spurious constant term near 1e-17 instead of an exact 0, which defeats leading-order ranking. A Num limit point such as Num(pi)/2 is folded to a float at the door, so it behaves exactly like pi/2 and does not help either. Use lim for those.
  • (1 + 1/x)^x as x → ∞. SymbolicLimits does not terminate on this, nor on the log/exp rewrite its own error message suggests; the deadline returns :unresolved.
  • Oscillation without a limit. sin(x) as x → ∞ genuinely has none. The :squeeze route treats its enclosure [-1, 1] as a refusal rather than an answer; call IntervalArithmetic directly to see the bound itself, which is the closest analogue to what a SymPy user gets from AccumBounds.
  • :squeeze is tried last, because interval arithmetic cannot see that two occurrences of x are the same number: sin(x)/x over [h/10, h] encloses to roughly [0.1, 10] however small h is, and x·floor(1/x) declines the same way. Every exact route is asked first for that reason. The enclosures must close at the rate the scales do, so sqrt(x)·sin(1/x) — whose limit is 0, approached too slowly — declines. Its answer is typed by what the route knows: an enclosure of zero width is exact and reports an integer (floor from the left is -1); one containing zero reports 0.0, not -1.0e-14; anything else is the float midpoint. A float from this route means bounded, not derived — (2/3)(1 - (1/4)^(n+1)) at infinity is 0.6666… here and 2//3 from the Gruntz engine after the a^m → e^{m·log a} rewrite, and the difference is the point.
  • log divergence cannot be delegated. SymbolicLimits.limit(log(u), u, 0, :right) returns 0 rather than -Inf (v1.1.5). That answer now fails the check comparison and is discarded, and the numeric increment test supplies the -Inf — load-bearing, not a convenience.
  • Unknown symbolic exponents. The order of x^k is k, so the limit of sin(sin(x^2))/x^k at 0 is 0, 1 or ∞ depending on k alone. The series route declines rather than picking one; substitute a concrete k, or rewrite a^x as exp(x·log(a)) where the exponent is no longer the unknown.
  • Limits at infinity are mostly the Gruntz engine. Ratios of polynomials go through :reciprocal and come back exact. Everything else is a time-boxed call into the engine, with no numeric evidence to check it against and no series to take. It handles pure power/log/exp forms; add a sin or a sqrt — exp(-x)·sin(x), x/sqrt(x^2+4) — and the answer is :unresolved.

See also tlim, lim.

source
CalculusWithJuliaSquared.tangent — Method
tangent(f::Function, c)

Returns a function describing the tangent line to the graph of f at x=c.

Example. Where does the tangent line intersect the y axis?

f(x) = sin(x)
tl = tangent(f, pi/4)  # or tl(x) = tangent(f, pi/3)(x) to use a generic function
tl(0)

Uses the automatic derivative of f to find the slope of the tangent line at x=c.

source
CalculusWithJuliaSquared.tlim — Function
tlim(num, den, v, c = 0; n = 6)

Limit of num/den as v → c, evaluated by Taylor series.

Both parts are expanded about c and compared by leading order: when the two series start at the same power the limit is the ratio of their leading coefficients; when the numerator starts higher the limit is 0. The answer comes back as an exact Rational wherever the coefficients are exact.

This reaches the limits SymbolicLimits declines outright — anything involving trigonometric functions or roots — because a series turns them into polynomials:

julia> @variables x::Real;

julia> tlim(sin(x), x, x)
1//1

julia> tlim(1 - cos(x), x^2, x)
1//2

julia> tlim(2sin(x) - sin(2x), x - sin(x), x)
6//1

Returns nothing when the method does not apply: an essential singularity, a coefficient that stays symbolic, or a numerator vanishing slower than the denominator (a pole rather than a limit).

Pass side = :left or :right where the expression contains an abs. A series cannot see a sign change: Symbolics.taylor(abs(w), w, 0:n) returns w, which is the right answer only from the right. Given a side, each abs is first resolved against the sign its argument actually holds there, so abs(x)/x at 0 expands to x/x on the right and -x/x on the left:

julia> @variables x::Real;

julia> tlim(abs(x), x, x, 0; side = :right), tlim(abs(x), x, x, 0; side = :left)
(1, -1)

Where the sign is not settled the abs stays put and the method declines.

Two cautions. The expansion point must be one the series can be taken about, so v → ∞ is out of reach — use symlim, which delegates those to the Gruntz engine. And a series argument is circular if used to derive the derivatives its own coefficients assume: proving [sin(x)]' = cos(x) from the Taylor series of sin assumes the answer. Computing lim sin(x)/x is not circular in that way, since the limit is the goal rather than a step toward it.

See also symlim, lim.

source
CalculusWithJuliaSquared.unzip — Method
unzip(vs)
unzip(v1, v2, ...)
unzip(r::Function, a, b)

Take a vector of points described by vectors (as returned by, say r(t)=[sin(t),cos(t)], r.([1,2,3]), and return a tuple of collected x values, y values, and optionally z values.

Wrapper around the invert function of SplitApplyCombine.

If the argument is specified as a comma separated collection of vectors, then these are combined and passed along.

If the argument is a function and two end points, then the function is evaluated at points between a and b; for the univaraite case, the points are chosen adaptively.

This is useful for plotting when the data is more conveniently represented in terms of vectors, but the plotting interface requires the x and y values collected.

Examples:

using Plots
r(t) = [sin(t), cos(t)]
rp(t) = [cos(t), -sin(t)]
plot(unzip(r, 0, 2pi)...)  # calls plot(xs, ys)

t0, t1 = pi/6, pi/4

p, v = r(t0), rp(t0)
plot!(unzip(p, p+v)...)  # connect p to p+v with line

p, v = r(t1), rp(t1)
quiver!(unzip([p])..., quiver=unzip([v]))

Based on unzip from the Plots package. Implemented through invert of SplitApplyCombine

Note: for a vector of points, xs, each of length 2, a similar functionality would be (first.(xs), last.(xs)). If each point had length 3, then with second(x)=x[2], a similar functionality would be (first.(xs), second.(xs), last.(xs)).

```

source
CalculusWithJuliaSquared.vectorfieldplot! — Method
vectorfieldplot(F; [xlim=(-5,5)], [ylim=(-5,5)], [nx=8], [ny=8])

Create a vector field plot using a grid described by xlim, ylim with nx and ny grid points in each direction.

F(x,y) = [-y, x]
vectorfieldplot(F, xlim=(-4,4), ylim=(-4,4))
source
CalculusWithJuliaSquared.vectorfieldplot! — Method
vectorfieldplot(F; [xlim=(-5,5)], [ylim=(-5,5)], [nx=8], [ny=8])

Create a vector field plot using a grid described by xlim, ylim with nx and ny grid points in each direction.

F(x,y) = [-y, x]
vectorfieldplot(F, xlim=(-4,4), ylim=(-4,4))
source
CalculusWithJuliaSquared.vectorfieldplot! — Method
vectorfieldplot(V; xlim=(-5,5), ylim=(-5,5), n=10; kwargs...)

V is a function that takes a point and returns a vector (2D dimensions), such as V(x) = x[1]^2 + x[2]^2.

The grid xlim × ylim is paritioned into (n+1) × (n+1) points. At each point, pt, a vector proportional to V(pt) is drawn.

This is written to add to an existing plot.

plot()  # make a plot
V(x,y) = [x, y-x]
vectorfield_plot!(p, V)
p
source
CalculusWithJuliaSquared.vectorfieldplot — Method
vectorfieldplot(F; [xlim=(-5,5)], [ylim=(-5,5)], [nx=8], [ny=8])

Create a vector field plot using a grid described by xlim, ylim with nx and ny grid points in each direction.

F(x,y) = [-y, x]
vectorfieldplot(F, xlim=(-4,4), ylim=(-4,4))
source
CalculusWithJuliaSquared.vectorfieldplot3d! — Method
vectorfieldplot3d(F; [xlim=(-5,5)], [ylim=(-5,5)], [nx=5], [ny=5])

Create a 3 dimensional vector field plot using a grid described by xlim, ylim, zlim with nx, ny, and nz grid points in each direction.

Note: the vectors are represented with line, not arrow due to no implementation of :quiver3d.

F(x,y,z) = [-y, x,z]
vectorfieldplot3d(F, xlims=(-4,4), ylims=(-4,4), zlims=(0,3))
source
CalculusWithJuliaSquared.vectorfieldplot3d! — Method
vectorfieldplot3d(F; [xlim=(-5,5)], [ylim=(-5,5)], [nx=5], [ny=5])

Create a 3 dimensional vector field plot using a grid described by xlim, ylim, zlim with nx, ny, and nz grid points in each direction.

Note: the vectors are represented with line, not arrow due to no implementation of :quiver3d.

F(x,y,z) = [-y, x,z]
vectorfieldplot3d(F, xlims=(-4,4), ylims=(-4,4), zlims=(0,3))
source
CalculusWithJuliaSquared.vectorfieldplot3d — Method
vectorfieldplot3d(F; [xlim=(-5,5)], [ylim=(-5,5)], [nx=5], [ny=5])

Create a 3 dimensional vector field plot using a grid described by xlim, ylim, zlim with nx, ny, and nz grid points in each direction.

Note: the vectors are represented with line, not arrow due to no implementation of :quiver3d.

F(x,y,z) = [-y, x,z]
vectorfieldplot3d(F, xlims=(-4,4), ylims=(-4,4), zlims=(0,3))
source
Plots._show — Method
Plots._show(io, ::MIME"text/html", plt::Plots.Plot{Plots.PlotlyBackend})

Write a Plotly plot as its HTML body rather than as a standalone document, so the figure embeds in a rendered page — a Quarto chapter, a Jupyter cell — and stays interactive. Inherited from upstream CalculusWithJulia, which kept it behind a Plots package extension; here Plots is a hard dependency, so it lives in the package proper.

This method is type piracy

Both Plots._show and Plots.Plot belong to Plots. It is benign in the sense set out under Cautions in the CalculusWithJuliaSquared module documentation: measured 2026-09-11, Plots._best_html_output_type maps :plotly => :html while the generic _show(::IO, ::MIME"text/html", ::Plot) handles only :png and :svg, so without this method the call throws "only png or svg allowed. got: :html". Nothing that previously worked changes.

Unlike the other four, this one pirates an internal: the leading underscore means Plots promises nothing about it across releases. If a future Plots renames or removes _show, the symptom is a plot that quietly stops being interactive, not a load error — so re-check it when Plots takes a major bump.

source
Roots.find_zeros — Function
find_zero(ex, x0; kwargs...)
find_zeros(ex, a, b; kwargs...)
ZeroProblem(ex, x0)

Solve for the zeros of a symbolic expression or ~ equation, wherever Roots expects a function.

julia> using CalculusWithJuliaSquared

julia> @variables x;

julia> find_zero(x^3 - x + 1, (-2, -1))
-1.324717957244746

julia> find_zero(cos(x) ~ x, (0, 2))
0.7390851332151607

julia> find_zeros(x^2 - 1, -3, 3)
2-element Vector{Float64}:
 -1.0
  1.0

The expression must contain exactly one free variable, since nothing in the call names the one being solved for; substitute values for the others first, e.g. substitute(ex, Dict(a => 1)). An equation lhs ~ rhs is solved as lhs - rhs == 0.

Roots ships precisely this for SymPy (RootsSymPyExt) but has no Symbolics equivalent. These methods are that extension's mirror image, with Symbolics.build_function in place of lambdify.

These methods are type piracy

An extension of Roots can only be declared inside Roots itself, so supplying it from here means adding methods to Roots.Callable_Function, Roots.FnWrapper and Roots.find_zeros — functions we do not own — dispatching on Symbolics.Num and Symbolics.Equation, types we do not own either. It is benign in the sense set out under Cautions in the CalculusWithJuliaSquared module documentation: every call above throws MethodError without it, so no working code changes behaviour.

Should Roots ever ship its own Symbolics support, expect a method-overwrite warning on load. That is not a bug to work around — the fix is to delete this block, because upstream's version supersedes it.

source