{
"cells": [
{
"cell_type": "markdown",
"source": [
"# Interval arithmetic tutorial"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"## Setup"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"The `IntervalArithmetic.jl` package can be installed with"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"```julia-repl\n",
"julia> using Pkg; Pkg.add(\"IntervalArithmetic\")\n",
"```"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"Now you can import the package"
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"using IntervalArithmetic"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"## Introduction"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"This tutorial will show you how to perform interval computations in Julia using the `IntervalArithmetic.jl` package.\n",
"Interval arithmetic, opposed to traditional floating point arithmetic, uses intervals instead of single numbers.\n",
"As floating point arithmetic introduces numerical error, interval arithmetic offers a framework to perform *rigorous* computations,\n",
"meaning the output of the computation, be it solving a system of equations or an optimisation problem, will be an interval that is guaranteed to contain the correct solution.\n",
"Before we dive into interval arithmetic, let's have a motivational example."
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"Observe the graph below, at first sight, it seems to have a small cuspid at $x=\\frac{4}{3}$, zooming closer also seems to confirm it."
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"f(x) = (1/80) * log(abs(3*(1 - x) + 1)) + x^2 + 1 # hide\n",
"using Plots # hide\n",
"p1 = plot(0.5:0.01:2.0, f, leg=false) # hide\n",
"p2 = plot(1.2:0.0001:1.5, f, leg=false) # hide\n",
"plot(p1, p2, layout=(1,2), leg=false) # hide"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"However, the function in the figure is (this example is due to William Kahan, the father of floating point arithmetic)"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"$$\n",
"f(x) = \\frac{1}{80}\\log(|3(1 - x) + 1|) + x^2 + 1\n",
"$$"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"From the expression is now evident that the function has a vertical asympote at $x=\\frac{4}{3}$ So what is going on?\n",
"First, you may think that using more points might help, however the issue is nastier. First, floating point system cannot represent all real numbers,\n",
"i.e. whenever you do real computations using floating points, you introduce a discretization error. Particularly, the number $\\frac{4}{3}$ itself is not\n",
"exactly representable with floating point arithmetic, hence when you type `x=4/3` in your code, you actually get a number close to it. Observe"
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"x = 4/3\n",
"f(x) = 1/80 * log(abs(3*(1 - x) + 1)) + x^2 + 1\n",
"@show f(x)"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"Hence using floating point arithmetic the function seems to be defined at `4/3`, which is obviously an absurd. Let us try to evaluate now the function at the previous and next floating point number."
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"f(prevfloat(x))\n",
"f(nextfloat(x))"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"Again, the function seems perfectly finite and well behaving. Conclusion: there is nothing you can do with floating point arithmetic to detect the singularity.\n",
"Interval arithmetic will offer a framework to perform rigorous computations with real numbers and avoid this kind of issues. In this tutorial you will also see how interval arithmetic can detect the singularity for this function."
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"Interval arithmetic offers a methematical framework to performs *rigorous* computations. Computations with traditional floating point arithmetic introduce rounding error and hence\n",
"the final result of the computation will be an approximate solution which is (hopefully!) close to the correct value.\n",
"Interval arithmetic on the other side will output an interval which is *guaranteed*\n",
"to contain the correct theoretical result of the computation. This tutorial will show\n",
"you how to perform interval computations in Julia using the `IntervalArithmetic.jl` package.\n",
"After this tutorial, you will be ready to harness the power of interval arithmetic and learn how to apply it to root finding, optimisation or differential equations."
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"## Defining intervals"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"An interval $I$ is a subset of $\\mathbb{R}$ in the form $I=\\{x\\in\\mathbb{R}|a\\leq x\\leq b\\}$ and is denoted as\n",
"$I=[a, b]$ and we say that $a$ and $b$ are the lower and upper bound. Note that the bounds must satisfy $a\\leq b$. The IntervalArithmetic.jl package offers several ways to define an interval"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"### Dot notation"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"An interval $[a,b]$, with $a\\leq b$ can be easily defined with the syntax `a..b`."
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"a = 1..2\n",
"@show a\n",
"\n",
"b = 2..1"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"\\note{If the upper bound is negative, then it should be within parentheses `-5..(-2)` or adding a space before it `-5.. -2`.\n",
"The expression `-5..-2` will throw a syntax error.}"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"### Midpoint notation"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"An interval can be created also using the *midpoint notation*, i.e. $m± r$, where $m=\\frac{a+b}{2}$ is the midpoint of the interval and $r=\\frac{b-a}{2}$ is the radius. In julia, the symbol $\\pm$ can be typed writing `\\pm`.\n",
"This method is equivalent to `(m-r)..(m+r)`"
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"2 ± 1"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### `@interval` macro and `interval` method"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"Another way to define an interval is to use the `@interval(a, b)` macro or the `interval(a, b)` method.\n",
"If called with a single input such as `@interval(a)` or `interval(a)`, they will create the degenerated interval $[a,a]$.\n",
"Here some examples:"
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"a = @interval(1,2)\n",
"\n",
"b = interval(1, 2)\n",
"\n",
"c = interval(1)\n",
"\n",
"d = @interval 1\n",
"\n",
"@show a, b, c, d"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"The macro is particularly useful when you want to create an interval from a more complicated expression, as demonstrated in the example below"
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"@interval sin(0.2)+cos(1.3)-exp(0.4)"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"This is equivalent to `sin(interval(0.2))+cos(interval(1.3))-exp(interval(0.4))`."
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"There is a fundamental difference between `interval` and `@interval`: the former does not perform direct rounding on the boundaries.\n",
"Suppose we want to create the interval $[0.1, 0.1]$. The number $0.1$ cannot be represented exactly with floating-point arithmetic, i.e. when the user types $0.1$ the computer automatically approximates to the closest representable number."
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"a = 0.1\n",
"\n",
"@show big(a)"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"The following trick allows you to create a number as close as possible to the exact real number"
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"correct = big\"0.1\"\n",
"@show correct"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
" If we create the interval with the `@interval` macro or the `..`, the package will check whether the number is exactly representable in floating point arithmetic and if it is not, it will round down the lower bound and round up the upper bound, ensuring that $0.1$ is actually included in the interval. However, if the `interval()` method is used, no rounding is performed, resulting in the number not being contained in the interval."
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"I = interval(a)\n",
"\n",
"II = @interval 0.1\n",
"III = a..a\n",
"\n",
"@show correct ∈ I\n",
"@show correct ∈ II\n",
"@show correct ∈ III"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"Concluding, `..` and `@interval` are more sophisticated and ensure proper rounding if the number typed by the user is not exactly representable in floating point arithmetic. This introduces some computational overhead. `interval()` simply uses the floating point\n",
"representation of the user input. This is faster, but the real numbers meant by the user might not necessarily be in the interval."
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"## Operations on intervals"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"In general, a binary operation $\\circ$ between two intervals $X$ and $Y$ is defined as"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"$$X\\circ Y = [\\{x\\circ y: x\\in X, y\\in Y\\}]$$"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"where the brackets $[A]$ denote the interval closure, i.e. the smallest interval containing the set $A$, for example if $A=[1,2]\\cup[3,4]$ then $[A]=[1,4]$.\n",
"Bearing this definition in mind, traditional arithmetic and set operations can be defined for intervals. These operations are already implemented in IntervalArihtmetic.jl and can be used with the traditional operators `+, -, *, /, ∩, ∪`.\n",
"If you want to know how these are computed under the hood, check our interval functions [grimoire](/pages/explanations/intervalFunctionsGrimoire)!\n",
"A few examples:"
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"X = 1..2\n",
"Y = 3..4\n",
"\n",
"@show X ∩ Y # typed \\cap\n",
"@show X + Y\n",
"@show X*Y\n",
"@show X^3\n",
"@show X/Y"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"Note that the operation $X/Y$ is finite only if $0∉ Y$, otherwise the resulting interval will be unbounded"
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"X = 1..2\n",
"\n",
"Y = -1..1\n",
"\n",
"Z = 0..2\n",
"\n",
"@show X/Y\n",
"\n",
"@show X/Z"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"It is good to notice that some standard properties of arithmetic operations do not hold in interval arithmetic, observe the following examples"
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"X-X\n",
"\n",
"X/X\n",
"\n",
"Z*(X+Y)\n",
"\n",
"Z*X+Z*Y"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"Hence in interval arithmetic $ X-X\\neq0 $ and $ X/X\\neq1 $. The last two operations tell us that $X(Y+Z)\\subseteq XY+XZ$, which is called *subdistributive properties*."
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"## Functions over intervals"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"Similarly to binary operations, the functions $f$ over an interval $X$ is defined as"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"$$f(X) = [\\{f(x): x\\in X\\}]$$"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"Elementary functions have already been implemented in JuliaIntervals are are ready-to-go. If you are curious to see how they are computed under the hood, check our interval functions grimoire."
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"@show sin(0..2/3*π)\n",
"\n",
"@show log(-3.. -2)\n",
"\n",
"@show log(-3..2)\n",
"\n",
"@show √(-3..4)"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"Now that we have discussed basic interval arithmetic operations, we are ready to return to Kahan's example. Let us create a small interval around `4/3`and evaluate the function there."
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"f(x) = (1/80) * log(abs(3*(1 - x) + 1)) + x^2 + 1\n",
"x = @interval 4/3\n",
"@show f(x)"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"As we can notice, interval arithmetic can successfully detect the singularity in the function, which we could not do with floating point arithmetic."
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"### A note on functions: overestimation"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"Suppose we want to evaluate the function $f(x) = x^2+3x-1$ over the interval $X=[-2,2]$."
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"f(x) = x^2+3x-1\n",
"\n",
"X = -2..2\n",
"\n",
"@show f(X)"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"According to this, $f(X) = [-7,9]$. However, it can be verified that the real range of the function over $X$ is $[-3.25, 9] \\subset [-7, 9]$. In this case we *overestimate* the range of the function."
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"The reason for this overestimation is known as *dependence problem*. If in a function $f$ the same variable appears multiple times, then the interval evaluation of the function may lead to overestimation. A simple solution to overcome this problem would be to divide the interval in smaller disjoint intervals, evaluate the function on each interval and compute the union of the resulting intervals. For example, since $[-2,2]=[-2,-1.5]\\cup [-1.5, 2]$ we obtain"
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"@show f(-2.. -1.5) ∪ f(-1.5.. 2)"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"As can be noticed, the overestimate is now reduced. [Here](/pages/howTo/funcRange/) you will find a more detailed discussion on how to estimate the range of a function and deal with function overestimates."
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"## Interval boxes"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"Interval boxes generalize the concept of interval to higher dimensions. More formally an interval box $X$ is a subset of $\\mathbb{R}^n$ defined as the cartesian product of $n$ intervals, i.e."
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"$$\n",
"X = X_1\\times\\cdots\\times X_n\n",
"$$"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"In julia, interval boxes can be created using the constructor `IntervalBox`, which takes the single intervals `X1,X2,...,Xn` as arguments. For the special case when $X_1=\\cdots=X_n$ you can also use\n",
"the shorter signature `IntervalBox(x, n)`. Observe the following examples"
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"X1 = IntervalBox(1..2, 2..3, 3..4)\n",
"X2 = IntervalBox(1..2, 3..4)\n",
"X3 = IntervalBox(1..2, 5)\n",
"\n",
"@show X1\n",
"@show X2\n",
"@show X3"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"Note that an interval box is a vector of intervals and as such, only operations which are well defined for vectors (addition and scalar multiplication) are implemented.\n",
"Particularly, product of interval boxes or elementary functions are not well defined. If you want to apply a function to each component of the box, use broadcasting."
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"X1 = IntervalBox(1..2, 2..3)\n",
"X2 = IntervalBox(-1..1, 0..2)\n",
"\n",
"@show X1 + X2\n",
"@show sin.(X1)"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"2D interval boxes are particularly handy for visualization. We can give the function `plot` (and `plot!`) a 2D interval box, or an array of boxes, to visualize these boxes in the cartesian plane.\n",
"Recall our example from before"
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"f(x) = x^2 + 3x - 1\n",
"X = -2..2\n",
"f1 = f(X)\n",
"\n",
"X2 = [-2.. -1.5, -1.5..2]\n",
"f2 = f.(X2)"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"We had discussed that evaluated the function over the inverval overestimates the range, and splitting the interval helps reducing the overestimate. To help visualizing this, we first create the interval boxes $X×f(X)$, note you can\n",
"use broadcasting to create an array of boxes from arrays of interval"
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"box1 = IntervalBox(X, f1)\n",
"box2 = IntervalBox.(X2, f2)"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"now we can plot the boxes to visualize the overestimate"
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"using Plots\n",
"plot(box1)\n",
"plot!(box2)\n",
"plot!(f, -2, 2, leg=false, color=:black, linewidth=2)"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"As you can see, evaluating the range using two boxes reduces the overestimate of the range."
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"## Display modes\n",
"There are several useful output representations for intervals, some of which we have already touched on. The display is controlled globally by the `setformat` function, which has\n",
"the following options, specified by keyword arguments:"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"* `format`: interval output format\n",
" - `:standard`: output of the form `[1.09999, 1.30001]`, rounded to the current number of significant figures (default)\n",
" - `:full`: output of the form `Interval(1.0999999999999999, 1.3)`, as in the `showfull` function\n",
" - `:midpoint`: output in the midpoint-radius form, e.g. `1.2 ± 0.100001`\n",
"* `sigfigs`: number of significant figures to show in standard mode. Default value is 6\n",
"* `decorations` (boolean): whether to show decorations or not. Default false."
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"Here are some examples"
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"a = sqrt(2) .. sqrt(3)\n",
"@show a\n",
"\n",
"setformat(:full)\n",
"@show a\n",
"\n",
"setformat(:midpoint)\n",
"@show a\n",
"setformat(:standard)\n",
"\n",
"setformat(sigfigs=10)\n",
"@show a"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"## Advanced options: precision and rounding modes"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"### Changing bounds precision\n",
"By default, the methods described above use `Float64` for the lower and upper bound of the interval. You can verify this using the function `precision(Interval)`"
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"precision(Interval)"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"The result is a tuple. The first value tells us that the lower and upper bounds of the interval are stored as\n",
"64-bits floating points. The second value is the precision of `BigFloat` used for interval computations."
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"You can change the precision of the bounds using `setprecision(Interval, T)`, where `T` can be a type (e.g. `Float64`) or an integer\n",
"number indicating the number of bits."
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"setprecision(Interval, 256)\n",
"@show precision(Interval)\n",
"@interval π"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"The subscript `256` tells us that the bounds of the interval are `BigFloat` with 256 bits.\n",
"We can set the precision back to default value with"
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"setprecision(Interval, Float64)"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Rounding modes"
],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"As we discussed before, interval arithmetic computations use direct rounding. This means that\n",
"when computing `f(X)`, where `X` is an interval, the lower bound is rounded down and the upper bound is rounded up.\n",
"This is achieved using th \\elink{CRlibm}{https://github.com/JuliaIntervals/CRlibm.jl} library.\n",
"While this ensures the inverval is as tight as possible, it also introduces some computational overhead.\n",
"An alternative rounding method is to perform calculations using the (standard) RoundNearest rounding mode, and then widen the result by one machine epsilon in each direction using prevfloat and nextfloat.\n",
"This produces a wider interval, but significantly speeds up computations. The rounding mode can be changed using `setrounding(Interval, mode)` function.\n",
"The `mode` parameter can have values\n",
"- `:fast` tight (correct) rounding with errorfree arithmetic via FastRounding.jl. Faster than other tight methods, but might be incorrect for extreme inputs\n",
"- `:tight` tight (correct) rounding with improved errorfree arithmetic via RoundingEmulator.jl\n",
"- `:accurate` fast \"accurate\" rounding using prevfloat and nextfloat, slightly wider than needed but faster\n",
"- `:slow` tight (correct) rounding by changing rounding mode.\n",
"- `:none` no rounding (for speed comparisons; no enclosure is guaranteed)\n",
"For example"
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"setrounding(Interval, :accurate)"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"will use round to nearest during computations and then use `prevfloat` and `nextfloat`for the interval bounds.\n",
"You can return to the default mode as follows"
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"setrounding(Interval, :slow)"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"You can check the rounding mode you are currently using with the `rounding`function"
],
"metadata": {}
},
{
"outputs": [],
"cell_type": "code",
"source": [
"rounding(Interval)"
],
"metadata": {},
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"---\n",
"\n",
"*This notebook was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).*"
],
"metadata": {}
}
],
"nbformat_minor": 3,
"metadata": {
"language_info": {
"file_extension": ".jl",
"mimetype": "application/julia",
"name": "julia",
"version": "1.5.0"
},
"kernelspec": {
"name": "julia-1.5",
"display_name": "Julia 1.5.0",
"language": "julia"
}
},
"nbformat": 4
}