mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-04 20:14:57 +08:00
Update references to kino (#1148)
* Update references to kino * Update changelog * Make the Chart cell section branching * Apply suggestions from code review Co-authored-by: José Valim <jose.valim@dashbit.co> * Capitalization * Add link to the Iris dataset * Pass keys to Process.info/2 Co-authored-by: José Valim <jose.valim@dashbit.co>
This commit is contained in:
parent
6884d3cec3
commit
8f72d0175e
21 changed files with 141 additions and 738 deletions
|
@ -30,6 +30,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Warning when running in the cloud and system memory is low ([#1122](https://github.com/livebook-dev/livebook/pull/1122))
|
||||
- Notification when a new Livebook version is available ([#1121](https://github.com/livebook-dev/livebook/pull/1121))
|
||||
- Insert button for Markdown cell with a Mermaid diagram ([#1134](https://github.com/livebook-dev/livebook/pull/1134))
|
||||
- Taskbar icon for the desktop app ([#1119](https://github.com/livebook-dev/livebook/pull/1119))
|
||||
- Notebook discussing Smart cells ([#1141](https://github.com/livebook-dev/livebook/pull/1141))
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
@ -30,11 +30,11 @@ defmodule Livebook do
|
|||
|
||||
[
|
||||
%{
|
||||
dependency: {:kino, "~> 0.5.2"},
|
||||
dependency: {:kino, "~> 0.6.0"},
|
||||
description: "Interactive widgets for Livebook",
|
||||
name: "kino",
|
||||
url: "https://hex.pm/packages/kino",
|
||||
version: "0.5.2"
|
||||
version: "0.6.0"
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -71,21 +71,6 @@ defmodule Livebook.Notebook.Explore do
|
|||
cover_url: "/images/vega_lite.png"
|
||||
}
|
||||
},
|
||||
%{
|
||||
path: Path.join(__DIR__, "explore/intro_to_nx.livemd"),
|
||||
details: %{
|
||||
description:
|
||||
"Enter Numerical Elixir, experience the power of multi-dimensional arrays of numbers.",
|
||||
cover_url: "/images/nx.png"
|
||||
}
|
||||
},
|
||||
# %{
|
||||
# path: Path.join(__DIR__, "explore/intro_to_axon.livemd"),
|
||||
# details: %{
|
||||
# description: "Build Neural Networks in Elixir using a high-level, composable API.",
|
||||
# cover_url: "/images/axon.png"
|
||||
# }
|
||||
# },
|
||||
%{
|
||||
ref: :kino_intro,
|
||||
path: Path.join(__DIR__, "explore/kino/intro_to_kino.livemd")
|
||||
|
|
|
@ -56,7 +56,7 @@ double-click on "Notebook dependencies and setup", add and run the code.
|
|||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.0"}
|
||||
{:kino, "~> 0.6.0"}
|
||||
])
|
||||
```
|
||||
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
# Neural Networks with Axon
|
||||
|
||||
TODO: content 🐈
|
|
@ -1,641 +0,0 @@
|
|||
# Introduction to Nx
|
||||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:nx, "~> 0.1.0-dev", github: "elixir-nx/nx", sparse: "nx", override: true}
|
||||
])
|
||||
```
|
||||
|
||||
## Numerical Elixir
|
||||
|
||||
Elixir's primary numerical datatypes and structures are not optimized
|
||||
for numerical programming. Nx is a library built to bridge that gap.
|
||||
|
||||
[Elixir Nx](https://github.com/elixir-nx/nx) is a numerical computing library
|
||||
to smoothly integrate to typed, multidimensional data implemented on other
|
||||
platforms (called tensors). This support extends to the compilers and
|
||||
libraries that support those tensors. Nx has three primary capabilities:
|
||||
|
||||
* In Nx, tensors hold typed data in multiple, named dimensions.
|
||||
* Numerical definitions, known as `defn`, support custom code with
|
||||
tensor-aware operators and functions.
|
||||
* [Automatic differentiation](https://arxiv.org/abs/1502.05767), also known as
|
||||
autograd or autodiff, supports common computational scenarios
|
||||
such as machine learning, simulations, curve fitting, and probabilistic models.
|
||||
|
||||
Here's more about each of those capabilities. Nx [tensors]() can hold
|
||||
unsigned integers (u8, u16, u32, u64),
|
||||
signed integers (s8, s16, s32, s64),
|
||||
floats (f32, f64), and brain floats (bf16).
|
||||
Tensors support backends implemented outside of Elixir, including Google's
|
||||
Accelerated Linear Algebra (XLA) and LibTorch.
|
||||
|
||||
Numerical definitions have compiler support to allow just-in-time compilation
|
||||
that support specialized processors to speed up numeric computation including
|
||||
TPUs and GPUs.
|
||||
|
||||
To know Nx, we'll get to know tensors first. This rapid overview will touch
|
||||
on the major libraries. Then, future notebooks will take a deep dive into working
|
||||
with tensors in detail, autograd, and backends. Then, we'll dive into specific
|
||||
problem spaces like Axon, the machine learning library.
|
||||
|
||||
## Nx and tensors
|
||||
|
||||
Systems of equations are a central theme in numerical computing.
|
||||
These equations are often expressed and solved with multidimensional
|
||||
arrays. For example, this is a two dimensional array:
|
||||
|
||||
$$
|
||||
\begin{bmatrix}
|
||||
1 & 2 \\
|
||||
3 & 4
|
||||
\end{bmatrix}
|
||||
$$
|
||||
|
||||
Elixir programmers typically express a similar data structure using
|
||||
a list of lists, like this:
|
||||
|
||||
```elixir
|
||||
[
|
||||
[1, 2],
|
||||
[3, 4]
|
||||
]
|
||||
```
|
||||
|
||||
This data structure works fine within many functional programming
|
||||
algorithms, but breaks down with deep nesting and random access.
|
||||
|
||||
On top of that, Elixir numeric types lack optimization for many numerical
|
||||
applications. They work fine when programs
|
||||
need hundreds or even thousands of calculations. They tend to break
|
||||
down with traditional STEM applications when a typical problem
|
||||
needs millions of calculations.
|
||||
|
||||
In Nx, we express multi-dimensional data using typed tensors. Simply put,
|
||||
a tensor is a multi-dimensional array with a predetermined shape and
|
||||
type. To interact with them, Nx relies on tensor-aware operators rather
|
||||
than `Enum.map/2` and `Enum.reduce/3`.
|
||||
|
||||
In this section, we'll look at some of the various tools for
|
||||
creating and interacting with tensors. The IEx helpers will assist our
|
||||
exploration of the core tensor concepts.
|
||||
|
||||
```elixir
|
||||
import IEx.Helpers
|
||||
```
|
||||
|
||||
Now, everything is set up, so we're ready to create some tensors.
|
||||
|
||||
<!-- livebook:{"break_markdown":true} -->
|
||||
|
||||
### Creating tensors
|
||||
|
||||
Start out by getting a feel for Nx through its documentation.
|
||||
Do so through the IEx helpers, like this:
|
||||
|
||||
<!-- livebook:{"disable_formatting":true} -->
|
||||
|
||||
```elixir
|
||||
h Nx
|
||||
```
|
||||
|
||||
Immediately, you can see that tensors are at the center of the
|
||||
API. The main API for creating tensors is `Nx.tensor/2`:
|
||||
|
||||
<!-- livebook:{"disable_formatting":true} -->
|
||||
|
||||
```elixir
|
||||
h Nx.tensor
|
||||
```
|
||||
|
||||
We use it to create tensors from raw Elixir lists of numbers, like this:
|
||||
|
||||
```elixir
|
||||
tensor =
|
||||
1..4
|
||||
|> Enum.chunk_every(2)
|
||||
|> Nx.tensor(names: [:y, :x])
|
||||
```
|
||||
|
||||
The result shows all of the major fields that make up a tensor:
|
||||
|
||||
* The data, presented as the list of lists `[[1, 2], [3, 4]]`.
|
||||
* The type of the tensor, a signed integer 64 bits long, with the type `s64`.
|
||||
* The shape of the tensor, going left to right, with the outside dimensions listed first.
|
||||
* The names of each dimension.
|
||||
|
||||
We can easily convert it to a binary:
|
||||
|
||||
```elixir
|
||||
binary = Nx.to_binary(tensor)
|
||||
```
|
||||
|
||||
A tensor of type s64 uses four bytes for each integer. The binary
|
||||
shows the individual bytes that make up the tensor, so you can see
|
||||
the integers `1..4` interspersed among the zeros that make
|
||||
up the tensor. If all of our data only uses positive numbers from
|
||||
`0..255`, we could save space with a different type:
|
||||
|
||||
```elixir
|
||||
Nx.tensor([[1, 2], [3, 4]], type: {:u, 8}) |> Nx.to_binary()
|
||||
```
|
||||
|
||||
If you have already have binary, you can directly convert it to a tensor
|
||||
by passing the binary and the type:
|
||||
|
||||
```elixir
|
||||
Nx.from_binary(<<0, 1, 2>>, {:u, 8})
|
||||
```
|
||||
|
||||
This function comes in handy when working with published datasets
|
||||
because they must often be processed. Elixir binaries make quick work
|
||||
of dealing with numerical data structured for platforms other than
|
||||
Elixir.
|
||||
|
||||
We can get any cell of the tensor:
|
||||
|
||||
```elixir
|
||||
tensor[0][1]
|
||||
```
|
||||
|
||||
Now, try getting the first row of the tensor:
|
||||
|
||||
```elixir
|
||||
# ...your code here...
|
||||
```
|
||||
|
||||
We can also get a whole dimension:
|
||||
|
||||
```elixir
|
||||
tensor[x: 1]
|
||||
```
|
||||
|
||||
or a range:
|
||||
|
||||
```elixir
|
||||
tensor[y: 0..1]
|
||||
```
|
||||
|
||||
Now,
|
||||
|
||||
* create your own `{3, 3}` tensor with named dimensions
|
||||
* return a `{2, 2}` tensor containing the first two columns
|
||||
of the first two rows
|
||||
|
||||
We can get information about this most recent term with
|
||||
the IEx helper `i`, like this:
|
||||
|
||||
<!-- livebook:{"disable_formatting":true} -->
|
||||
|
||||
```elixir
|
||||
i tensor
|
||||
```
|
||||
|
||||
The tensor is a struct that supports the usual `Inspect` protocol.
|
||||
The struct has keys, but we typically treat the `Nx.Tensor`
|
||||
as an _opaque data type_ (meaning we typically access the contents and
|
||||
shape of a tensor using the tensor's API instead of the struct).
|
||||
|
||||
Primarily, a tensor is a struct, and the
|
||||
functions to access it go through a specific backend. We'll get to
|
||||
the backend details in a moment. For now, use the IEx `h` helper
|
||||
to get more documentation about tensors. We could also open a Code
|
||||
cell, type Nx.tensor, and hover the cursor over the word `tensor`
|
||||
to see the help about that function.
|
||||
|
||||
We can get the shape of the tensor with `Nx.shape/1`:
|
||||
|
||||
```elixir
|
||||
Nx.shape(tensor)
|
||||
```
|
||||
|
||||
We can also create a new tensor with a new shape using `Nx.reshape/2`:
|
||||
|
||||
```elixir
|
||||
Nx.reshape(tensor, {1, 4}, names: [:batches, :values])
|
||||
```
|
||||
|
||||
This operation reuses all of the tensor data and simply
|
||||
changes the metadata, so it has no notable cost.
|
||||
|
||||
The new tensor has the same type, but a new shape.
|
||||
|
||||
Now, reshape the tensor to contain three dimensions with
|
||||
one batch, one row, and four columns.
|
||||
|
||||
```elixir
|
||||
# ...your code here...
|
||||
```
|
||||
|
||||
We can create a tensor with named dimensions, a type, a shape,
|
||||
and our target data. A dimension is called an _axis_, and axes
|
||||
can have names. We can specify the tensor type and dimension names
|
||||
with options, like this:
|
||||
|
||||
```elixir
|
||||
Nx.tensor([[1, 2, 3]], names: [:rows, :cols], type: {:u, 8})
|
||||
```
|
||||
|
||||
We created a tensor of the shape `{1, 3}`, with the type `u8`,
|
||||
the values `[1, 2, 3]`, and two axes named `rows` and `cols`.
|
||||
|
||||
Now we know how to create tensors, so it's time to do something with them.
|
||||
|
||||
## Tensor aware functions
|
||||
|
||||
In the last section, we created a `s64[2][2]` tensor. In this section,
|
||||
we'll use Nx functions to work with it. Here's the value of `tensor`:
|
||||
|
||||
```elixir
|
||||
tensor
|
||||
```
|
||||
|
||||
We can use `IEx.exports/1` or code completion to find
|
||||
some functions in the `Nx` module that operate on tensors:
|
||||
|
||||
<!-- livebook:{"disable_formatting":true} -->
|
||||
|
||||
```elixir
|
||||
exports Nx
|
||||
```
|
||||
|
||||
You might recognize that many of those functions have names that
|
||||
suggest that they would work on primitive values, called scalars.
|
||||
Indeed, a tensor can be a scalar:
|
||||
|
||||
```elixir
|
||||
pi = Nx.tensor(3.1415, type: {:f, 32})
|
||||
```
|
||||
|
||||
Take the cosine:
|
||||
|
||||
```elixir
|
||||
Nx.cos(pi)
|
||||
```
|
||||
|
||||
That function took the cosine of `pi`. We can also call them
|
||||
on a whole tensor, like this:
|
||||
|
||||
```elixir
|
||||
Nx.cos(tensor)
|
||||
```
|
||||
|
||||
We can also call a function that aggregates the contents
|
||||
of a tensor. For example, to get a sum of the numbers
|
||||
in `tensor`, we can do this:
|
||||
|
||||
```elixir
|
||||
Nx.sum(tensor)
|
||||
```
|
||||
|
||||
That's `1 + 2 + 3 + 4`, and Nx went to multiple dimensions to get that sum.
|
||||
To get the sum of values along the `x` axis instead, we'd do this:
|
||||
|
||||
```elixir
|
||||
Nx.sum(tensor, axes: [:x])
|
||||
```
|
||||
|
||||
Nx sums the values across the `x` dimension: `1 + 2` in the first row
|
||||
and `3 + 4` in the second row.
|
||||
|
||||
Now,
|
||||
|
||||
* create a `{2, 2, 2}` tensor
|
||||
* with the values `1..8`
|
||||
* with dimension names `[:z, :y, :x]`
|
||||
* calculate the sums along the `y` axis
|
||||
|
||||
```elixir
|
||||
# ...your code here...
|
||||
```
|
||||
|
||||
Sometimes, we need to combine two tensors together with an
|
||||
operator. Let's say we wanted to subtract one tensor from
|
||||
another. Mathematically, the expression looks like this:
|
||||
|
||||
$$
|
||||
\begin{bmatrix}
|
||||
5 & 6 \\
|
||||
7 & 8
|
||||
\end{bmatrix} -
|
||||
\begin{bmatrix}
|
||||
1 & 2 \\
|
||||
3 & 4
|
||||
\end{bmatrix} =
|
||||
\begin{bmatrix}
|
||||
4 & 4 \\
|
||||
4 & 4
|
||||
\end{bmatrix}
|
||||
$$
|
||||
|
||||
To solve this problem, add each integer on the left with the
|
||||
corresponding integer on the right. Unfortunately, we cannot
|
||||
use Elixir's built-in subtraction operator as it is not tensor-aware.
|
||||
Luckily, we can use the `Nx.subtract/2` function to solve the
|
||||
problem:
|
||||
|
||||
```elixir
|
||||
tensor2 = Nx.tensor([[5, 6], [7, 8]])
|
||||
Nx.subtract(tensor2, tensor)
|
||||
```
|
||||
|
||||
We get a `{2, 2}` shaped tensor full of fours, exactly as we expected.
|
||||
When calling `Nx.subtract/2`, both operands had the same shape.
|
||||
Sometimes, you might want to process functions where the dimensions
|
||||
don't match. To solve this problem, Nx takes advantage of
|
||||
a concept called _broadcasting_.
|
||||
|
||||
## Broadcasts
|
||||
|
||||
Often, the dimensions of tensors in an operator don't match.
|
||||
For example, you might want to subtract a `1` from every
|
||||
element of a `{2, 2}` tensor, like this:
|
||||
|
||||
$$
|
||||
\begin{bmatrix}
|
||||
1 & 2 \\
|
||||
3 & 4
|
||||
\end{bmatrix} - 1 =
|
||||
\begin{bmatrix}
|
||||
0 & 1 \\
|
||||
2 & 3
|
||||
\end{bmatrix}
|
||||
$$
|
||||
|
||||
Mathematically, it's the same as this:
|
||||
|
||||
$$
|
||||
\begin{bmatrix}
|
||||
1 & 2 \\
|
||||
3 & 4
|
||||
\end{bmatrix} -
|
||||
\begin{bmatrix}
|
||||
1 & 1 \\
|
||||
1 & 1
|
||||
\end{bmatrix} =
|
||||
\begin{bmatrix}
|
||||
0 & 1 \\
|
||||
2 & 3
|
||||
\end{bmatrix}
|
||||
$$
|
||||
|
||||
That means we need a way to convert `1` to a `{2, 2}` tensor.
|
||||
`Nx.broadcast/2` solves that problem. This function takes
|
||||
a tensor or a scalar and a shape.
|
||||
|
||||
```elixir
|
||||
Nx.broadcast(1, {2, 2})
|
||||
```
|
||||
|
||||
This broadcast takes the scalar `1` and translates it
|
||||
to a compatible shape by copying it. Sometimes, it's easier
|
||||
to provide a tensor as the second argument, and let `broadcast/2`
|
||||
extract its shape:
|
||||
|
||||
```elixir
|
||||
Nx.broadcast(1, tensor)
|
||||
```
|
||||
|
||||
The code broadcasts `1` to the shape of `tensor`. In many operators
|
||||
and functions, the broadcast happens automatically:
|
||||
|
||||
```elixir
|
||||
Nx.subtract(tensor, 1)
|
||||
```
|
||||
|
||||
This result is possible because Nx broadcasts _both tensors_
|
||||
in `subtract/2` to compatible shapes. That means you can provide
|
||||
scalar values as either argument:
|
||||
|
||||
```elixir
|
||||
Nx.subtract(10, tensor)
|
||||
```
|
||||
|
||||
Or subtract a row or column. Mathematically, it would look like this:
|
||||
|
||||
$$
|
||||
\begin{bmatrix}
|
||||
1 & 2 \\
|
||||
3 & 4
|
||||
\end{bmatrix} -
|
||||
\begin{bmatrix}
|
||||
1 & 2
|
||||
\end{bmatrix} =
|
||||
\begin{bmatrix}
|
||||
0 & 0 \\
|
||||
2 & 2
|
||||
\end{bmatrix}
|
||||
$$
|
||||
|
||||
which is the same as this:
|
||||
|
||||
$$
|
||||
\begin{bmatrix}
|
||||
1 & 2 \\
|
||||
3 & 4
|
||||
\end{bmatrix} -
|
||||
\begin{bmatrix}
|
||||
1 & 2 \\
|
||||
1 & 2
|
||||
\end{bmatrix} =
|
||||
\begin{bmatrix}
|
||||
0 & 0 \\
|
||||
2 & 2
|
||||
\end{bmatrix}
|
||||
$$
|
||||
|
||||
This rewrite happens in Nx too, also through a broadcast. We want to
|
||||
broadcast the tensor `[1, 2]` to match the `{2, 2}` shape, like this:
|
||||
|
||||
```elixir
|
||||
Nx.broadcast(Nx.tensor([1, 2]), {2, 2})
|
||||
```
|
||||
|
||||
The `subtract` function in `Nx` takes care of that broadcast
|
||||
implicitly, as before:
|
||||
|
||||
```elixir
|
||||
Nx.subtract(tensor, Nx.tensor([1, 2]))
|
||||
```
|
||||
|
||||
The broadcast worked as advertised, copying the `[1, 2]` row
|
||||
enough times to fill a `{2, 2}` tensor. A tensor with a
|
||||
dimension of `1` will broadcast to fill the tensor:
|
||||
|
||||
```elixir
|
||||
[[1], [2]] |> Nx.tensor() |> Nx.broadcast({1, 2, 2})
|
||||
```
|
||||
|
||||
```elixir
|
||||
[[[1, 2, 3]]]
|
||||
|> Nx.tensor()
|
||||
|> Nx.broadcast({4, 2, 3})
|
||||
```
|
||||
|
||||
Both of these examples copy parts of the tensor enough
|
||||
times to fill out the broadcast shape. You can check out the
|
||||
Nx broadcasting documentation for more details:
|
||||
|
||||
<!-- livebook:{"disable_formatting":true} -->
|
||||
|
||||
```elixir
|
||||
h Nx.broadcast
|
||||
```
|
||||
|
||||
Much of the time, you won't have to broadcast yourself. Many of
|
||||
the functions and operators Nx supports will do so automatically.
|
||||
|
||||
We can use tensor-aware operators via various `Nx` functions and
|
||||
many of them implicitly broadcast tensors.
|
||||
|
||||
Throughout this section, we have been invoking `Nx.subtract/2` and
|
||||
our code would be more expressive if we could use its equivalent
|
||||
mathematical operator. Fortunately, Nx provides a way. Next, we'll
|
||||
dive into numerical definitions using `defn`.
|
||||
|
||||
## Numerical definitions (defn)
|
||||
|
||||
The `defn` macro simplifies the expression of mathematical formulas
|
||||
containing tensors. Numerical definitions have two primary benefits
|
||||
over classic Elixir functions.
|
||||
|
||||
* They are _tensor-aware_. Nx replaces operators like `Kernel.-/2`
|
||||
with the `Defn` counterparts — which in turn use `Nx` functions
|
||||
optimized for tensors — so the formulas we express can use
|
||||
tensors out of the box.
|
||||
|
||||
* `defn` definitions allow for building computation graph of all the
|
||||
individual operations and using a just-in-time (JIT) compiler to emit
|
||||
highly specialized native code for the desired computation unit.
|
||||
|
||||
We don't have to do anything special to get access to
|
||||
get tensor awareness beyond importing `Nx.Defn` and writing
|
||||
our code within a `defn` block.
|
||||
|
||||
To use Nx in a Mix project or a notebook, we need to include
|
||||
the `:nx` dependency and import the `Nx.Defn` module. The
|
||||
dependency is already included, so import it in a Code cell,
|
||||
like this:
|
||||
|
||||
```elixir
|
||||
import Nx.Defn
|
||||
```
|
||||
|
||||
Just as the Elixir language supports `def`, `defmacro`, and `defp`,
|
||||
Nx supports `defn`. There are a few restrictions. It allows only
|
||||
numerical arguments in the form of primitives or tensors as arguments
|
||||
or return values, and has a subset of the language within
|
||||
the `defn` block.
|
||||
|
||||
The subset of Elixir allowed within `defn` is quite broad, though. We can
|
||||
use macros, pipes, and even conditionals, so we're not giving up
|
||||
much when you're declaring mathematical functions.
|
||||
|
||||
Additionally, despite these small concessions, `defn` provides huge benefits.
|
||||
Code in a `defn` block uses tensor aware operators and types, so the math
|
||||
beneath your functions has a better chance to shine through. Numerical
|
||||
definitions can also run on accelerated numerical processors like GPUs and
|
||||
TPUs. Here's an example numerical definition:
|
||||
|
||||
```elixir
|
||||
defmodule TensorMath do
|
||||
import Nx.Defn
|
||||
|
||||
defn subtract(a, b) do
|
||||
a - b
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This module has a numerical definition that will be compiled.
|
||||
If we wanted to specify a compiler for this module, we could add
|
||||
a module attribute before the `defn` clause. One of such compilers
|
||||
is [the EXLA compiler](https://github.com/elixir-nx/nx/tree/main/exla).
|
||||
You'd add the `mix` dependency for EXLA and do this:
|
||||
|
||||
<!-- livebook:{"force_markdown":true} -->
|
||||
|
||||
```elixir
|
||||
@defn_compiler EXLA
|
||||
defn subtract(a, b) do
|
||||
a - b
|
||||
end
|
||||
```
|
||||
|
||||
Now, it's your turn. Add a `defn` to `TensorMath`
|
||||
that accepts two tensors representing the lengths of sides of a
|
||||
right triangle and uses the pythagorean theorem to return the
|
||||
[length of the hypotenuse](https://www.mathsisfun.com/pythagoras.html).
|
||||
Add your function directly to the previous Code cell.
|
||||
|
||||
The last major feature we'll cover is called auto-differentiation, or autograd.
|
||||
|
||||
## Automatic differentiation (autograd)
|
||||
|
||||
An important mathematical property for a function is the
|
||||
rate of change, or the gradient. These gradients are critical
|
||||
for solving systems of equations and building probabilistic
|
||||
models. In advanced math, derivatives, or differential equations,
|
||||
are used to take gradients. Nx can compute these derivatives
|
||||
automatically through a feature called automatic differentiation,
|
||||
or autograd.
|
||||
|
||||
Here's how it works.
|
||||
|
||||
<!-- livebook:{"disable_formatting":true} -->
|
||||
|
||||
```elixir
|
||||
h Nx.Defn.grad
|
||||
```
|
||||
|
||||
We'll build a module with a few functions,
|
||||
and then create another function to create the gradients of those
|
||||
functions. The function `grad/1` takes a function, and returns
|
||||
a function returning the gradient. We have two functions: `poly/1`
|
||||
is a simple numerical definitin, and `poly_slope_at/1` returns
|
||||
its gradient:
|
||||
|
||||
$$
|
||||
poly: f(x) = 3x^2 + 2x + 1 \\
|
||||
$$
|
||||
|
||||
$$
|
||||
polySlopeAt: g(x) = 6x + 2
|
||||
$$
|
||||
|
||||
Here's the Elixir equivalent of those functions:
|
||||
|
||||
```elixir
|
||||
defmodule Funs do
|
||||
defn poly(x) do
|
||||
3 * Nx.power(x, 2) + 2 * x + 1
|
||||
end
|
||||
|
||||
def poly_slope_at(x) do
|
||||
grad(&poly/1).(x)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Notice the second `defn`. It uses `grad/1` to take its
|
||||
derivative using autograd. It uses the intermediate `defn` AST
|
||||
and mathematical composition to compute the derivative. You can
|
||||
see it at work here:
|
||||
|
||||
```elixir
|
||||
Funs.poly_slope_at(2)
|
||||
```
|
||||
|
||||
Nice. If you plug the number 2 into the function $$ 6x + 2 $$
|
||||
you get 14! Said another way, if you look at the graph at
|
||||
exactly 2, the rate of increase is 14 units of `poly(x)`
|
||||
for every unit of `x`, precisely at `x`.
|
||||
|
||||
Nx also has helpers to get gradients corresponding to a number of inputs.
|
||||
These come into play when solving systems of equations.
|
||||
|
||||
Now, you try. Find a function computing the gradient of a `sin` wave.
|
||||
|
||||
```elixir
|
||||
# your code here
|
||||
```
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:vega_lite, "~> 0.1.2"},
|
||||
{:kino, "~> 0.5.0"}
|
||||
{:vega_lite, "~> 0.1.4"},
|
||||
{:kino_vega_lite, "~> 0.1.0"}
|
||||
])
|
||||
```
|
||||
|
||||
|
@ -14,8 +14,8 @@ We need two libraries for plotting in Livebook:
|
|||
* The [`vega_lite`](https://github.com/elixir-nx/vega_lite)
|
||||
package allows us to define our graph specifications
|
||||
|
||||
* The [`kino`](https://github.com/elixir-nx/kino) package
|
||||
renders our specifications
|
||||
* The [`kino_vega_lite`](https://github.com/elixir-nx/kino) package
|
||||
instructs Livebook how to render our specifications
|
||||
|
||||
Let's install them by running the setup cell above.
|
||||
|
||||
|
@ -28,6 +28,69 @@ alias VegaLite, as: Vl
|
|||
|
||||
<!-- livebook:{"branch_parent_index":0} -->
|
||||
|
||||
## The Chart smart cell
|
||||
|
||||
Before we get into exploring all the various chart types, let's have
|
||||
a look at an awesome feature that comes with `kino_vega_lite` - the
|
||||
**Chart** smart cell!
|
||||
|
||||
Coding up a chart usually involves a couple steps. If you don't know
|
||||
the API of a particular library, it may be a bit challenging. On the
|
||||
other hand, if you know the API in-and-out, it's a rather repetitive
|
||||
task. That's where the **Chart** smart cell comes in, it is a
|
||||
high-level UI that helps us write our chart code. It is great
|
||||
a tool for learning and for automating our workflows. Let's
|
||||
give it a try!
|
||||
|
||||
First, we need some data to work with, here's a small excerpt from
|
||||
the popular [Iris dataset](https://www.kaggle.com/datasets/uciml/iris):
|
||||
|
||||
```elixir
|
||||
iris = [
|
||||
%{"petal_length" => 5.1, "petal_width" => 1.9, "species" => "Iris-virginica"},
|
||||
%{"petal_length" => 4.0, "petal_width" => 1.3, "species" => "Iris-versicolor"},
|
||||
%{"petal_length" => 1.6, "petal_width" => 0.2, "species" => "Iris-setosa"},
|
||||
%{"petal_length" => 1.6, "petal_width" => 0.2, "species" => "Iris-setosa"},
|
||||
%{"petal_length" => 4.6, "petal_width" => 1.4, "species" => "Iris-versicolor"},
|
||||
%{"petal_length" => 4.8, "petal_width" => 1.8, "species" => "Iris-virginica"},
|
||||
%{"petal_length" => 5.6, "petal_width" => 2.2, "species" => "Iris-virginica"},
|
||||
%{"petal_length" => 5.1, "petal_width" => 1.6, "species" => "Iris-versicolor"},
|
||||
%{"petal_length" => 1.5, "petal_width" => 0.3, "species" => "Iris-setosa"},
|
||||
%{"petal_length" => 4.5, "petal_width" => 1.6, "species" => "Iris-versicolor"}
|
||||
]
|
||||
|
||||
:ok
|
||||
```
|
||||
|
||||
Now, to insert a new Chart cell, place your cursor between cells,
|
||||
click on the <kbd>+ Smart</kbd> button and select Chart.
|
||||
|
||||
You can see an example of that below. Click on the "Evaluate"
|
||||
button to see the chart, then see how it changes as you customize
|
||||
the parameters.
|
||||
|
||||
<!-- livebook:{"attrs":{"chart_title":"Iris","height":200,"layers":[{"chart_type":"point","color_field":"species","color_field_aggregate":null,"color_field_type":null,"data_variable":"iris","x_field":"petal_length","x_field_aggregate":null,"x_field_type":"quantitative","y_field":"petal_width","y_field_aggregate":null,"y_field_type":"quantitative"}],"vl_alias":"Elixir.Vl","width":400},"kind":"Elixir.KinoVegaLite.ChartCell","livebook_object":"smart_cell"} -->
|
||||
|
||||
```elixir
|
||||
Vl.new(width: 400, height: 200, title: "Iris")
|
||||
|> Vl.data_from_values(iris, only: ["petal_length", "petal_width", "species"])
|
||||
|> Vl.mark(:point)
|
||||
|> Vl.encode_field(:x, "petal_length", type: :quantitative)
|
||||
|> Vl.encode_field(:y, "petal_width", type: :quantitative)
|
||||
|> Vl.encode_field(:color, "species")
|
||||
```
|
||||
|
||||
Under cell actions there is a "Source" button, click on it to
|
||||
see the source code of your chart. You can even convert it to
|
||||
a regular Code cell for further adjustments!
|
||||
|
||||
The Chart smart cell is one of many Smart cells available
|
||||
in Livebook ⚡ Not only that, you can create your own Smart
|
||||
cells too, which we discuss in the
|
||||
[Exploring Smart cells](/explore/notebooks/smart-cells) notebook!
|
||||
|
||||
<!-- livebook:{"branch_parent_index":0} -->
|
||||
|
||||
## Basic concepts
|
||||
|
||||
Composing a basic Vega-Lite graphic usually consists of the following steps:
|
||||
|
@ -36,7 +99,7 @@ Composing a basic Vega-Lite graphic usually consists of the following steps:
|
|||
# Initialize the specification, optionally with some top-level properties
|
||||
Vl.new(width: 400, height: 400)
|
||||
# Specify data source for the graphic using one of the data_from_* functions
|
||||
|> Vl.data_from_series(iteration: 1..100, score: 1..100)
|
||||
|> Vl.data_from_values(iteration: 1..100, score: 1..100)
|
||||
# Pick a visual mark
|
||||
|> Vl.mark(:line)
|
||||
# Map data fields to visual properties of the mark, in this case point positions
|
||||
|
@ -437,7 +500,7 @@ the shape, conflating the two dimensions.
|
|||
# Source: https://vega.github.io/vega-lite/examples/arc_radial.html
|
||||
|
||||
Vl.new()
|
||||
|> Vl.data_from_series(data: [12, 23, 47, 6, 52, 19])
|
||||
|> Vl.data_from_values(data: [12, 23, 47, 6, 52, 19])
|
||||
|> Vl.encode_field(:theta, "data", type: :quantitative, stack: true)
|
||||
|> Vl.encode_field(:radius, "data", scale: [type: :sqrt, zero: true, range_min: 20])
|
||||
|> Vl.encode_field(:color, "data", type: :nominal, legend: nil)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.0"}
|
||||
{:kino, "~> 0.6.0"}
|
||||
])
|
||||
```
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.0"}
|
||||
{:kino, "~> 0.6.0"}
|
||||
])
|
||||
```
|
||||
|
||||
|
@ -43,10 +43,8 @@ end
|
|||
|
||||
Let's break it down.
|
||||
|
||||
To define a custom kino we need to create a new module,
|
||||
conventionally under the `Kino.` prefix, so that the end
|
||||
user can easily autocomplete all available kinos. In
|
||||
this case we went with `KinoGuide.HTML`.
|
||||
To define a custom kino we need to create a new module. In
|
||||
this case we go with `KinoGuide.HTML`.
|
||||
|
||||
We start by adding `use Kino.JS`, which makes our module
|
||||
asset-aware. In particular, it allows us to use the `asset/2`
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.0"}
|
||||
{:kino, "~> 0.6.0"}
|
||||
])
|
||||
```
|
||||
|
||||
|
@ -96,16 +96,14 @@ side-by-side. We can actually gather information about these
|
|||
processes and render it as a table:
|
||||
|
||||
```elixir
|
||||
processes = Process.list() |> Enum.map(&Process.info/1)
|
||||
```
|
||||
keys = [:registered_name, :initial_call, :reductions, :stack_size]
|
||||
|
||||
We can pick the data keys that are relevant for us:
|
||||
processes =
|
||||
for pid <- Process.list(),
|
||||
info = Process.info(pid, keys),
|
||||
do: info
|
||||
|
||||
```elixir
|
||||
Kino.DataTable.new(
|
||||
processes,
|
||||
keys: [:registered_name, :initial_call, :reductions, :stack_size]
|
||||
)
|
||||
Kino.DataTable.new(processes)
|
||||
```
|
||||
|
||||
Now you can use the table above to sort by the number of
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.0"}
|
||||
{:kino, "~> 0.6.0"}
|
||||
])
|
||||
```
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:kino, github: "livebook-dev/kino"},
|
||||
{:kino, "~> 0.6.0"},
|
||||
{:jason, "~> 1.3"}
|
||||
])
|
||||
```
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:vega_lite, "~> 0.1.2"},
|
||||
{:kino, "~> 0.5.0"}
|
||||
{:kino, "~> 0.6.0"},
|
||||
{:kino_vega_lite, "~> 0.1.0"}
|
||||
])
|
||||
|
||||
alias VegaLite, as: Vl
|
||||
|
|
|
@ -102,11 +102,11 @@ defmodule Livebook.Runtime.Dependencies do
|
|||
|
||||
## Examples
|
||||
|
||||
iex> Livebook.Runtime.Dependencies.parse_term(~s|{:kino, "~> 0.5.0"}|)
|
||||
{:ok, {:kino, "~> 0.5.0"}}
|
||||
iex> Livebook.Runtime.Dependencies.parse_term(~s|{:jason, "~> 1.3.0"}|)
|
||||
{:ok, {:jason, "~> 1.3.0"}}
|
||||
|
||||
iex> Livebook.Runtime.Dependencies.parse_term(~s|{:kino, "~> 0.5.0", runtime: false, meta: 'data'}|)
|
||||
{:ok, {:kino, "~> 0.5.0", runtime: false, meta: 'data'}}
|
||||
iex> Livebook.Runtime.Dependencies.parse_term(~s|{:jason, "~> 1.3.0", runtime: false, meta: 'data'}|)
|
||||
{:ok, {:jason, "~> 1.3.0", runtime: false, meta: 'data'}}
|
||||
|
||||
iex> Livebook.Runtime.Dependencies.parse_term(~s|%{name: "Jake", numbers: [1, 2, 3.4]}|)
|
||||
{:ok, %{name: "Jake", numbers: [1, 2, 3.4]}}
|
||||
|
|
|
@ -17,37 +17,38 @@ defmodule Livebook.Runtime.ElixirStandalone do
|
|||
server_pid: pid() | nil
|
||||
}
|
||||
|
||||
kino_dep = {:kino, github: "livebook-dev/kino"}
|
||||
kino_vega_lite_dep = {:kino_vega_lite, "~> 0.1.0"}
|
||||
kino_db_dep = {:kino_db, "~> 0.1.0"}
|
||||
|
||||
@extra_smart_cell_definitions [
|
||||
%{
|
||||
kind: "Elixir.Kino.SmartCell.DBConnection",
|
||||
kind: "Elixir.KinoDB.ConnectionCell",
|
||||
name: "Database connection",
|
||||
requirement: %{
|
||||
name: "Kino",
|
||||
name: "KinoDB",
|
||||
variants: [
|
||||
%{name: "PostgreSQL", dependencies: [kino_dep, {:postgrex, "~> 0.16.1"}]},
|
||||
%{name: "MySQL", dependencies: [kino_dep, {:myxql, "~> 0.6.1"}]}
|
||||
%{name: "PostgreSQL", dependencies: [kino_db_dep, {:postgrex, "~> 0.16.3"}]},
|
||||
%{name: "MySQL", dependencies: [kino_db_dep, {:myxql, "~> 0.6.2"}]}
|
||||
]
|
||||
}
|
||||
},
|
||||
%{
|
||||
kind: "Elixir.Kino.SmartCell.SQL",
|
||||
kind: "Elixir.KinoDB.SQLCell",
|
||||
name: "SQL query",
|
||||
requirement: %{
|
||||
name: "Kino",
|
||||
name: "KinoDB",
|
||||
variants: [
|
||||
%{name: "Default", dependencies: [kino_dep]}
|
||||
%{name: "Default", dependencies: [kino_db_dep]}
|
||||
]
|
||||
}
|
||||
},
|
||||
%{
|
||||
kind: "Elixir.Kino.SmartCell.ChartBuilder",
|
||||
name: "Chart builder",
|
||||
kind: "Elixir.KinoVegaLite.ChartCell",
|
||||
name: "Chart",
|
||||
requirement: %{
|
||||
name: "Kino",
|
||||
name: "KinoVegaLite",
|
||||
variants: [
|
||||
%{name: "Default", dependencies: [kino_dep, {:vega_lite, "~> 0.1.3"}]}
|
||||
%{name: "Default", dependencies: [kino_vega_lite_dep]}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 33 KiB |
Binary file not shown.
Before Width: | Height: | Size: 21 KiB |
|
@ -5,23 +5,23 @@ defmodule Livebook.Runtime.DependenciesTest do
|
|||
|
||||
doctest Dependencies
|
||||
|
||||
@kino {:kino, "~> 0.5.0"}
|
||||
@jason {:jason, "~> 1.3.0"}
|
||||
|
||||
describe "add_mix_deps/2" do
|
||||
test "prepends Mix.install/2 call if there is none" do
|
||||
assert Dependencies.add_mix_deps("", [@kino]) ==
|
||||
assert Dependencies.add_mix_deps("", [@jason]) ==
|
||||
{:ok,
|
||||
"""
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.0"}
|
||||
{:jason, "~> 1.3.0"}
|
||||
])\
|
||||
"""}
|
||||
|
||||
assert Dependencies.add_mix_deps("# Comment", [@kino]) ==
|
||||
assert Dependencies.add_mix_deps("# Comment", [@jason]) ==
|
||||
{:ok,
|
||||
"""
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.0"}
|
||||
{:jason, "~> 1.3.0"}
|
||||
])
|
||||
|
||||
# Comment\
|
||||
|
@ -37,12 +37,12 @@ defmodule Livebook.Runtime.DependenciesTest do
|
|||
|
||||
# Final comment\
|
||||
""",
|
||||
[@kino]
|
||||
[@jason]
|
||||
) ==
|
||||
{:ok,
|
||||
"""
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.0"}
|
||||
{:jason, "~> 1.3.0"}
|
||||
])
|
||||
|
||||
# Outer comment
|
||||
|
@ -62,13 +62,13 @@ defmodule Livebook.Runtime.DependenciesTest do
|
|||
{:req, "~> 0.2.0"}
|
||||
])\
|
||||
""",
|
||||
[@kino]
|
||||
[@jason]
|
||||
) ==
|
||||
{:ok,
|
||||
"""
|
||||
Mix.install([
|
||||
{:req, "~> 0.2.0"},
|
||||
{:kino, "~> 0.5.0"}
|
||||
{:jason, "~> 1.3.0"}
|
||||
])\
|
||||
"""}
|
||||
|
||||
|
@ -84,7 +84,7 @@ defmodule Livebook.Runtime.DependenciesTest do
|
|||
# Result
|
||||
:ok\
|
||||
""",
|
||||
[@kino]
|
||||
[@jason]
|
||||
) ==
|
||||
{:ok,
|
||||
"""
|
||||
|
@ -92,7 +92,7 @@ defmodule Livebook.Runtime.DependenciesTest do
|
|||
Mix.install([
|
||||
# Inner comment leading
|
||||
{:req, "~> 0.2.0"},
|
||||
{:kino, "~> 0.5.0"}
|
||||
{:jason, "~> 1.3.0"}
|
||||
# Inner comment trailing
|
||||
])
|
||||
|
||||
|
@ -111,14 +111,14 @@ defmodule Livebook.Runtime.DependenciesTest do
|
|||
]
|
||||
)\
|
||||
""",
|
||||
[@kino]
|
||||
[@jason]
|
||||
) ==
|
||||
{:ok,
|
||||
"""
|
||||
Mix.install(
|
||||
[
|
||||
{:req, "~> 0.2.0"},
|
||||
{:kino, "~> 0.5.0"}
|
||||
{:jason, "~> 1.3.0"}
|
||||
],
|
||||
system_env: [
|
||||
# {"XLA_TARGET", "cuda111"}
|
||||
|
@ -130,34 +130,34 @@ defmodule Livebook.Runtime.DependenciesTest do
|
|||
test "does not add the dependency if it already exists" do
|
||||
code = """
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.2"}
|
||||
{:jason, "~> 1.3.0"}
|
||||
])\
|
||||
"""
|
||||
|
||||
assert Dependencies.add_mix_deps(code, [@kino]) == {:ok, code}
|
||||
assert Dependencies.add_mix_deps(code, [@jason]) == {:ok, code}
|
||||
|
||||
code = """
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.2", runtime: false}
|
||||
{:jason, "~> 1.3.0", runtime: false}
|
||||
])\
|
||||
"""
|
||||
|
||||
assert Dependencies.add_mix_deps(code, [@kino]) == {:ok, code}
|
||||
assert Dependencies.add_mix_deps(code, [@jason]) == {:ok, code}
|
||||
end
|
||||
|
||||
test "given multiple dependencies adds the missing ones" do
|
||||
assert Dependencies.add_mix_deps(
|
||||
"""
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.2"}
|
||||
{:jason, "~> 1.3.0"}
|
||||
])\
|
||||
""",
|
||||
[{:vega_lite, "~> 0.1.3"}, {:kino, "~> 0.5.0"}, {:req, "~> 0.2.0"}]
|
||||
[{:vega_lite, "~> 0.1.3"}, {:jason, "~> 1.3.0"}, {:req, "~> 0.2.0"}]
|
||||
) ==
|
||||
{:ok,
|
||||
"""
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.2"},
|
||||
{:jason, "~> 1.3.0"},
|
||||
{:vega_lite, "~> 0.1.3"},
|
||||
{:req, "~> 0.2.0"}
|
||||
])\
|
||||
|
@ -165,11 +165,11 @@ defmodule Livebook.Runtime.DependenciesTest do
|
|||
|
||||
code = """
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.2", runtime: false}
|
||||
{:jason, "~> 1.3.0", runtime: false}
|
||||
])\
|
||||
"""
|
||||
|
||||
assert Dependencies.add_mix_deps(code, [@kino]) == {:ok, code}
|
||||
assert Dependencies.add_mix_deps(code, [@jason]) == {:ok, code}
|
||||
end
|
||||
|
||||
test "returns an error if the code has a syntax error" do
|
||||
|
@ -178,7 +178,7 @@ defmodule Livebook.Runtime.DependenciesTest do
|
|||
# Comment
|
||||
[,1]
|
||||
""",
|
||||
[@kino]
|
||||
[@jason]
|
||||
) ==
|
||||
{:error,
|
||||
"""
|
||||
|
|
|
@ -142,7 +142,7 @@ defmodule Livebook.SessionTest do
|
|||
|
||||
Session.subscribe(session.id)
|
||||
|
||||
Session.add_dependencies(session.pid, [{:kino, "~> 0.5.0"}])
|
||||
Session.add_dependencies(session.pid, [{:jason, "~> 1.3.0"}])
|
||||
|
||||
session_pid = session.pid
|
||||
assert_receive {:operation, {:apply_cell_delta, ^session_pid, "setup", :primary, _delta, 1}}
|
||||
|
@ -154,7 +154,7 @@ defmodule Livebook.SessionTest do
|
|||
%{
|
||||
source: """
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.0"}
|
||||
{:jason, "~> 1.3.0"}
|
||||
])\
|
||||
"""
|
||||
}
|
||||
|
@ -173,7 +173,7 @@ defmodule Livebook.SessionTest do
|
|||
|
||||
Session.subscribe(session.id)
|
||||
|
||||
Session.add_dependencies(session.pid, [{:kino, "~> 0.5.0"}])
|
||||
Session.add_dependencies(session.pid, [{:json, "~> 1.3.0"}])
|
||||
|
||||
assert_receive {:error, "failed to add dependencies to the setup cell, reason:" <> _}
|
||||
end
|
||||
|
|
|
@ -905,12 +905,12 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
# Search the predefined dependencies in the embedded runtime
|
||||
search_view
|
||||
|> element(~s{form[phx-change="search"]})
|
||||
|> render_change(%{"search" => "ki"})
|
||||
|> render_change(%{"search" => "ja"})
|
||||
|
||||
page = render(view)
|
||||
assert page =~ "kino"
|
||||
assert page =~ "Interactive widgets for Livebook"
|
||||
assert page =~ "0.5.2"
|
||||
assert page =~ "jason"
|
||||
assert page =~ "A blazing fast JSON parser and generator in pure Elixir"
|
||||
assert page =~ "1.3.0"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -22,11 +22,11 @@ defmodule Livebook.Runtime.Embedded.Dependencies do
|
|||
def entries() do
|
||||
[
|
||||
%{
|
||||
dependency: {:kino, "~> 0.5.2"},
|
||||
description: "Interactive widgets for Livebook",
|
||||
name: "kino",
|
||||
url: "https://hex.pm/packages/kino",
|
||||
version: "0.5.2"
|
||||
dependency: {:jason, "~> 1.3.0"},
|
||||
description: "A blazing fast JSON parser and generator in pure Elixir",
|
||||
name: "jason",
|
||||
url: "https://hex.pm/packages/jason",
|
||||
version: "1.3.0"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
|
Loading…
Add table
Reference in a new issue