# learner


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

This module is the base of the engine. Every other module imports from
it, and it imports from no other module of abhyasa. It holds what
abhyasa knows about the learner, and the shelf where it keeps it. The
parts come in the order that they import each other:

- **Settings**: the settings file, the settings table and its defaults.
- **Prompts**: the system prompts that you can see and change in
  Settings.
- **The store**: the learning shelf in your vault. Each concept, attempt
  and session is one document.
- **The learner model**: the depth, the floor and the ceiling of each
  concept, and when to review it.
- **Ways to learn**: which way of teaching helps you, such as code or an
  analogy.
- **The profile**: your background, goals, interests and pace.
- **Background jobs**: the stored rows of the jobs that the activity
  chip shows.
- **Stored walks**: read and save a walk, find its current idea, and
  count its steps.

Two functions of the profile ask a model. They import the model layer
(`abhyasa.models`) when they run, because that layer imports this
module.

Import the modules that this module uses. The heavy ones load inside the
functions that need them, so the import stays light.

## Settings

One table for each value that you can change, and the file that keeps
your changes.

Each value that a person can change is a row in one table, `SETTINGS`. A
row has a group, a name, a label, one line of help, a type, a default
and, for a number, a range. The engine, the web app and the Settings
page all read this table. Thus each value has one place.

Your changes go in `settings.json` in
[`cfg_dir()`](https://vedicreader.github.io/abhyasa/learner.html#cfg_dir).
The code reads a value when it uses it, so a change works at once, with
no restart. A saved value outside its range gets the nearest end of the
range. A saved value of the wrong type gets the default. A bad file
never stops the app.

This module imports only the standard library and fastcore. Each engine
module reads its values from here, so the import must stay light.

### The groups

The Settings page shows the groups in this order. Each group has a label
and one lead line.

### The table

[`add`](https://vedicreader.github.io/abhyasa/learner.html#add) puts one
row in `SETTINGS`. The name is `section.key`: the section of
`settings.json` and the key in it. The type comes from the default.
`more` puts the row under the closed “More” part of its group.
`store='profile'` marks a value that the vault keeps in your profile,
not this file. `hi` can be the name of another row. Then that value is
the top of the range. `ui=False` marks a value that its group shows with
its own controls. `reset=False` keeps a value when you use the defaults
of its group: the theme, and the speech engine and model.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L50"
target="_blank" style="float:right; font-size:smaller">source</a>

### add

``` python
def add(
    name:str, group:str, label:str, help:str, dflt, lo:NoneType=None, hi:NoneType=None, more:bool=False,
    choices:NoneType=None, unit:str='', pct:bool=False, store:str='file', ui:bool=True, reset:bool=True
):
```

*Add the setting `name` to `SETTINGS` and return its row.*

The **Look** group keeps its own controls: the theme swatches and the
mode. The **Learning** group holds the three values of your profile.

The **Walks** group sets the size of a walk and of a side walk.

The **Talks** group sets how a talk runs: the questions, the time, the
vote and the room.

The **Checks and grading** group sets the tries, the grade of a shaky
pass, and when an idea counts as learned.

The **Review** group sets the time to the next review. A daily limit on
review is new: 0 means no limit, as before.

The **Links** group sets how much of a link the tutor gets. The pages
read by default can not be more than the page cap.

The **Research** group sets the web search for a step with no source.
The tutor searches the web for the idea, reads the best pages, and
quotes one passage word for word. When research is off, the tutor does
no web search, and the step says that the text is in the tutor’s words.

The **Speech to text** group keeps the engine and the model, and the
pauses at which the mic cuts what you say. The **Screen guide** group
has the voice and the look of a point mark. A point mark is weaker than
a circle in each look, so a circle is always the main mark of a step.
The default is `tick`, because it has no ring that looks like a circle.

The **Speaking voice** group picks who reads the steps of the screen
guide aloud. `pocket` runs Pocket TTS on this Mac. `openai` sends each
line to OpenAI. `browser` is the voice of the browser (or of the Mac in
the overlay). The `voice` module reads these values. The default is
`pocket`. Until its model is downloaded, the browser speaks. An old `hf`
or `kokoro` value is not one of the choices, so it reads as `pocket`.
The speed is only for OpenAI: Pocket TTS has no speed.

The **Models** group has the default model and one model for each task.
An empty value uses the default model. The tasks after
[`extract`](https://vedicreader.github.io/abhyasa/sources.html#extract)
borrow the model of an older task when they have none: `BORROWS` says
which. The screen guide (`guide`) and its figure labels (`guide_vision`)
use the model for hints until you pick one. The reply caps are in
tokens.

The **Lists** group sets how many items show in the lists of the app.
All of it is under “More”.

Each group except **Prompts** has rows, and each row is in a group. The
Prompts group lists the prompts of `prompts.PROMPTS`.

``` python
assert set(GROUPS) - {'prompts'} == {r.group for r in SETTINGS.values()}
assert all(r.kind != 'int' or r.lo <= r.dflt for r in SETTINGS.values())
len(SETTINGS)
```

### The file

`DFLT_SETTINGS` is the default of each section: the rows that the file
keeps, the saved prompt texts and the models that the Settings page
downloaded.
[`settings()`](https://vedicreader.github.io/abhyasa/learner.html#settings)
gives the saved settings over the defaults. A section that is not a dict
in the file is ignored, and a top-level key that is not known is
dropped.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L180"
target="_blank" style="float:right; font-size:smaller">source</a>

### cfg_dir

``` python
def cfg_dir()->pathlib.Path:
```

*The folder for abhyasa’s settings and keys: `$ABHYASA_CONFIG`, else
`~/.config/abhyasa`.*

[`_saved`](https://vedicreader.github.io/abhyasa/learner.html#_saved)
reads the file only when it changed, so a value read in a loop costs one
`stat`.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L205"
target="_blank" style="float:right; font-size:smaller">source</a>

### settings

``` python
def settings()->dict:
```

*The saved settings over the defaults.*

[`save_settings`](https://vedicreader.github.io/abhyasa/learner.html#save_settings)
writes the file: first a temporary file with its own name, then a
rename, so a crash never leaves half a file. A lock keeps two changes
from the web app apart. It does not keep a value that is the default.
[`update_settings`](https://vedicreader.github.io/abhyasa/learner.html#update_settings)
changes keys in one section.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L233"
target="_blank" style="float:right; font-size:smaller">source</a>

### update_settings

``` python
def update_settings(
    section:str, **kw
)->dict:
```

*Change some keys in one section of the settings.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L217"
target="_blank" style="float:right; font-size:smaller">source</a>

### save_settings

``` python
def save_settings(
    d:dict
)->dict:
```

*Write all the settings.*

With no file,
[`settings()`](https://vedicreader.github.io/abhyasa/learner.html#settings)
gives the defaults. A change in one section keeps the others.

``` python
test_eq(settings()['walks']['steps'], 7)
update_settings('stt', segment=False)
test_eq(settings()['stt']['segment'], False)
test_eq(settings()['look'], dict(theme='paper', mode='auto', keys_float=True))
update_settings('stt', segment=True)
```

A file from before this table still loads: its sections update the
defaults, and the new sections get their defaults.

``` python
_path().write_text(json.dumps(dict(stt=dict(engine='mlx', model='m', segment=True), models=dict(question='', grade='', hint='', lesson=''),
                                   tokens=dict(question='', grade='', hint='', lesson='', extract=''), look=dict(theme='ink', mode='dark'), prompts={}, downloaded=[])))
d = settings()
test_eq((d['stt']['engine'], d['stt']['pause'], d['look']['theme'], d['walks']['steps'], d['models']['extract']), ('mlx', 700, 'ink', 7, ''))
save_settings(dict(prompts={}, downloaded=[]))
```

A file that is not JSON, or a section that is not a dict, gives the
defaults. An unknown top-level key is dropped.

``` python
_path().write_text('{not json')
test_eq(settings()['walks']['steps'], 7)
_path().write_text(json.dumps(dict(walks=5, mine=1)))
d = settings()
test_eq((d['walks']['steps'], 'mine' in d), (7, False))
save_settings(dict(prompts={}, downloaded=[]))
```

### Read one value

[`fit`](https://vedicreader.github.io/abhyasa/learner.html#fit) makes a
saved value fit its row. A number outside the range gets the nearest
end. A value of the wrong type gets the default. A whole number can come
as `12.0` from a form; it stays a whole number. A choice must be one of
the choices.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L244"
target="_blank" style="float:right; font-size:smaller">source</a>

### fit

``` python
def fit(
    r, v
):
```

*The value `v` made to fit row `r`: clamped to the range, or the default
for the wrong type.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L240"
target="_blank" style="float:right; font-size:smaller">source</a>

### top

``` python
def top(
    r
):
```

*The top of the range of row `r`: a number, or the value of the row that
`r.hi` names.*

[`val`](https://vedicreader.github.io/abhyasa/learner.html#val) gives
the value of a row now: the saved value made to fit, else the default.
The engine calls it each time it needs the value.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L270"
target="_blank" style="float:right; font-size:smaller">source</a>

### dflt

``` python
def dflt(
    name:str
):
```

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L263"
target="_blank" style="float:right; font-size:smaller">source</a>

### val

``` python
def val(
    name:str
):
```

*The value of the setting `name` now.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L257"
target="_blank" style="float:right; font-size:smaller">source</a>

### raw

``` python
def raw(
    name:str
):
```

*The saved value of `name` as it is in the file, or None.*

`sources.roots` is a text list, one path for each line. A source folder
has the same rules as a folder that you name: it must be in your home
folder, it must not be the home folder itself, and no part of it can be
hidden or a link.
[`source_root_why`](https://vedicreader.github.io/abhyasa/learner.html#source_root_why)
gives the reason when a path breaks a rule.
[`roots`](https://vedicreader.github.io/abhyasa/learner.html#roots)
gives the paths that keep the rules, and
[`dropped_roots`](https://vedicreader.github.io/abhyasa/learner.html#dropped_roots)
gives the others with their reasons, so the Settings page can show them.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L291"
target="_blank" style="float:right; font-size:smaller">source</a>

### dropped_roots

``` python
def dropped_roots()->list:
```

*The lines of `sources.roots` that abhyasa does not use, each as (line,
reason).*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L287"
target="_blank" style="float:right; font-size:smaller">source</a>

### roots

``` python
def roots()->list:
```

*The paths of the setting `sources.roots` that keep the rules of
[`source_root_why`](https://vedicreader.github.io/abhyasa/learner.html#source_root_why),
one for each line that is not empty.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L275"
target="_blank" style="float:right; font-size:smaller">source</a>

### source_root_why

``` python
def source_root_why(
    p
)->str:
```

*Why abhyasa does not read the source folder `p`: outside the home
folder, the home folder itself, hidden, or a link. ’’ when it can.*

``` python
test_eq(roots(), ['~/code/personal/orgs'])
```

A saved 50 ideas becomes 7, the most. A string gives the default. A
share above 1 becomes 1.

``` python
update_settings('walks', steps=50, hands='a lot', note=40.0)
test_eq((val('walks.steps'), val('walks.hands'), val('walks.note')), (7, 1/3, 40))
update_settings('walks', hands=1.5, steps=1)
test_eq((val('walks.hands'), val('walks.steps')), (1., 3))
save_settings(dict(prompts={}, downloaded=[]))
```

The top of the pages read by default is the page cap.

``` python
update_settings('links', pages=40, cap=10)
test_eq((val('links.pages'), val('links.cap')), (10, 10))
save_settings(dict(prompts={}, downloaded=[]))
```

The look of a point mark is one of three choices. A look that is not a
choice gives the default, `tick`.

``` python
update_settings('guide', point='dot')
test_eq(val('guide.point'), 'dot')
update_settings('guide', point='arrow')
test_eq((val('guide.point'), SETTINGS['guide.point'].choices), ('tick', ('tick', 'dot', 'ring')))
save_settings(dict(prompts={}, downloaded=[]))
```

### Change one value

[`set_val`](https://vedicreader.github.io/abhyasa/learner.html#set_val)
saves a value made to fit its row, and returns it.
[`reset`](https://vedicreader.github.io/abhyasa/learner.html#reset) puts
back the default of one row, and
[`reset_group`](https://vedicreader.github.io/abhyasa/learner.html#reset_group)
of each row in a group.
[`changed`](https://vedicreader.github.io/abhyasa/learner.html#changed)
is true when a value is not its default.
[`clamped`](https://vedicreader.github.io/abhyasa/learner.html#clamped)
gives the value in the file when it was outside the range, so the page
can say so.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L326"
target="_blank" style="float:right; font-size:smaller">source</a>

### clamped

``` python
def clamped(
    name:str
):
```

*The number in the file for `name` when it was outside the range, else
None.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L324"
target="_blank" style="float:right; font-size:smaller">source</a>

### changed

``` python
def changed(
    name:str
)->bool:
```

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L315"
target="_blank" style="float:right; font-size:smaller">source</a>

### reset_group

``` python
def reset_group(
    group:str
)->list:
```

*Use the default of each row of `group` that the file keeps and that a
group reset can change. Return their names.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L311"
target="_blank" style="float:right; font-size:smaller">source</a>

### rows

``` python
def rows(
    group:str
)->list:
```

*The rows of `group`, in table order.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L302"
target="_blank" style="float:right; font-size:smaller">source</a>

### reset

``` python
def reset(
    name:str
):
```

*Use the default of the setting `name` again.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L296"
target="_blank" style="float:right; font-size:smaller">source</a>

### set_val

``` python
def set_val(
    name:str, v
):
```

*Save the value `v` of the setting `name`, made to fit its row. Return
the value in use.*

The source folders drop the root of the disk, the home folder itself and
a hidden folder.

``` python
set_val('sources.roots', '/\n~\n~/.ssh\n~/code/personal/orgs\n')
test_eq(roots(), ['~/code/personal/orgs'])
test_eq([l for l, _ in dropped_roots()], ['/', '~', '~/.ssh'])
reset('sources.roots')
```

A value saved out of range is saved as the end of the range.
[`reset`](https://vedicreader.github.io/abhyasa/learner.html#reset) and
[`reset_group`](https://vedicreader.github.io/abhyasa/learner.html#reset_group)
put back the defaults.

``` python
test_eq(set_val('walks.steps', 99), 7)
test_eq(set_val('walks.steps', 1), 3)
test_eq(raw('walks.steps'), 3)
assert changed('walks.steps') and not changed('walks.note')
reset('walks.steps')
test_eq(val('walks.steps'), 7)
set_val('checks.tries', 5); set_val('checks.near', 8)
test_eq(sorted(reset_group('checks')), ['checks.near', 'checks.tries'])
test_eq((val('checks.tries'), val('checks.near')), (3, 7))
update_settings('walks', steps=50)
test_eq(clamped('walks.steps'), 50)
# a number typed by hand as text works, and text that is not a number gives the default
update_settings('tokens', hint='900', grade='many')
test_eq((val('tokens.hint'), val('tokens.grade')), (900, 2048))
# a value that the profile keeps has no reset here
test_fail(lambda: reset('learning.pace'), contains='profile')
save_settings(dict(prompts={}, downloaded=[]))
```

The speaking engine is `browser` until you change it, and a value that
is not an engine gives the default.

``` python
test_eq((val('voice.out'), val('voice.out_speed')), ('pocket', 1.))
test_eq((fit(SETTINGS['voice.out'], 'kokoro'), fit(SETTINGS['voice.out'], 'hf')), ('pocket', 'pocket'))
```

### What stays fixed

Some values keep you safe, so they have no setting: the link checks for
local addresses, plain HTTP, the link kinds, the HTML allowlist, the
check of a pane address, the local-only hosts, the test allowlist, the
grade scale and the depth ladder. The room for phones keeps its safety
limits fixed too: the bind rule, the join code, the rate limits, the
body size and the headers. `FIXED` names them, and no row may have one
of these words in its name.

``` python
assert not [n for n in SETTINGS for w in FIXED if w in n]
```

## Prompts

The instruction text of the tutor. You can change it on the Settings
page.

Each prompt of the tutor has two parts. The variable parts (the concept,
the thread, the JSON shape) stay in code. The instruction text is in
this registry. Each prompt has a name, a plain label, a note on what it
changes, and its default text from code. The Settings page shows each
prompt, and you can change its text there. The tutor reads each prompt
when it calls the model. Thus a change works on the next question, with
no restart.

Import the tools that this module uses. The settings come from
[`settings`](https://vedicreader.github.io/abhyasa/learner.html#settings)
only when a prompt is read, so the import stays light.

### The registry

[`register`](https://vedicreader.github.io/abhyasa/learner.html#register)
adds a prompt to `PROMPTS` and returns its default text. A `check` can
refuse a bad text: it raises `ValueError` with a plain message.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L339"
target="_blank" style="float:right; font-size:smaller">source</a>

### register

``` python
def register(
    name:str, label:str, note:str, dflt:str, check:NoneType=None
)->str:
```

*Add the prompt `name` with a plain
[`label`](https://vedicreader.github.io/abhyasa/sources.html#label), a
`note` on what it changes, its
[`dflt`](https://vedicreader.github.io/abhyasa/learner.html#dflt) text
and an optional `check`. Return
[`dflt`](https://vedicreader.github.io/abhyasa/learner.html#dflt).*

[`lines`](https://vedicreader.github.io/abhyasa/learner.html#lines)
gives the lines of a text that are not empty.
[`need_lines`](https://vedicreader.github.io/abhyasa/learner.html#need_lines)
makes a check that refuses a text without exactly `n` such lines.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L349"
target="_blank" style="float:right; font-size:smaller">source</a>

### need_lines

``` python
def need_lines(
    n:int, what:str
):
```

*A check that refuses a text that does not have `n` lines that are not
empty.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L345"
target="_blank" style="float:right; font-size:smaller">source</a>

### lines

``` python
def lines(
    text:str
)->list:
```

*The lines of `text` that are not empty, without spaces at the ends.*

### Read and save

[`prompt`](https://vedicreader.github.io/abhyasa/learner.html#prompt)
gives the saved text of a prompt, else its default.
[`set_prompt`](https://vedicreader.github.io/abhyasa/learner.html#set_prompt)
saves a text in the settings, under the `prompts` key. An empty text, or
a text that is the same as the default, removes the saved text. Then the
default is in use again, also when a new version changes the default.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L365"
target="_blank" style="float:right; font-size:smaller">source</a>

### set_prompt

``` python
def set_prompt(
    name:str, text:str=''
)->str:
```

*Save the text of the prompt `name`. An empty text uses the default. A
text that fails the check raises `ValueError`. Return the text in use.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L360"
target="_blank" style="float:right; font-size:smaller">source</a>

### prompt

``` python
def prompt(
    name:str
)->str:
```

*The text of the prompt `name` now: the saved text, else the default.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L356"
target="_blank" style="float:right; font-size:smaller">source</a>

### saved

``` python
def saved()->dict:
```

*The saved prompt texts: name -\> text.*

### Tests

A small prompt with a check of 2 lines. Its default is in use at first.

``` python
register('greet', 'Greeting', 'The first line.', 'Say hello.\nSay your name.', need_lines(2, 'The greeting'))
test_eq(prompt('greet'), 'Say hello.\nSay your name.')
```

A saved text is in use at once, and the settings keep it under
`prompts`.

``` python
test_eq(set_prompt('greet', 'Say hi.\r\nSay your job.\n'), 'Say hi.\nSay your job.')
test_eq((prompt('greet'), saved()), ('Say hi.\nSay your job.', {'greet': 'Say hi.\nSay your job.'}))
```

A text that fails the check is refused with a plain message, and the
saved text stays.

``` python
test_fail(lambda: set_prompt('greet', 'Only one line.'), contains='needs 2 lines')
test_eq(prompt('greet'), 'Say hi.\nSay your job.')
```

An empty text, or only spaces, removes the saved text, so the default is
in use again. A text that is the same as the default is not kept.

``` python
test_eq(set_prompt('greet', '  \n '), 'Say hello.\nSay your name.')
set_prompt('greet', 'Say hello.\nSay your name.')
test_eq(saved(), {})
```

An unknown name is an error. Then the test prompt goes away.

``` python
test_fail(lambda: prompt('nope'))
del PROMPTS['greet']
```

## The store

The `learning` shelf: concepts, attempts and sessions as vault
documents.

This module keeps all learning data in your vault, on one shelf named
`learning`. Each concept, attempt and session is one vault document.
Thus each tool that reads the vault can search it. The other modules
read and write the shelf only through the
[`Learning`](https://vedicreader.github.io/abhyasa/learner.html#learning)
class in this module.

Import the tools that this module uses, and the `Vault` class of the
vault.

### The shelf

Search reads the text of a document. The `meta` JSON keeps the state.
`Vault.set_meta` changes the state and does not embed the text again.
The table gives the source URI of each kind of document.

<table>
<thead>
<tr>
<th>kind</th>
<th>source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>concept</code></td>
<td><code>learning://concept/&lt;slug&gt;</code></td>
</tr>
<tr>
<td><code>attempt</code></td>
<td><code>learning://attempt/&lt;id&gt;</code></td>
</tr>
<tr>
<td><code>session</code></td>
<td><code>learning://session/&lt;id&gt;</code></td>
</tr>
</tbody>
</table>

These constants name the shelf, give the start state of a concept, and
list the fields that merge.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L392"
target="_blank" style="float:right; font-size:smaller">source</a>

### meta_of

``` python
def meta_of(
    r
)->dict:
```

*The meta of the document row `r`: a dict from vishalakshi `by_source`,
or JSON text from `t.docs`.*

[`slugify`](https://vedicreader.github.io/abhyasa/learner.html#slugify)
makes the key of a concept from its name. It changes a name in an Indic
script, such as Devanagari, to IAST first. It changes `+`, `#` and `&`
to words, so C, C++ and C# get different slugs. A name with no letters
that it can read gets a short hash of the name. Thus the same name
always gives the same slug.
[`_src`](https://vedicreader.github.io/abhyasa/learner.html#_src) makes
the source URI of a document.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L417"
target="_blank" style="float:right; font-size:smaller">source</a>

### usable_name

``` python
def usable_name(
    name
)->bool:
```

*Whether `name` has two characters at least, and a letter or a digit. A
name such as `x` is not an idea.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L413"
target="_blank" style="float:right; font-size:smaller">source</a>

### listed

``` python
def listed(
    c
)->bool:
```

*Whether concept `c` shows in Library and the graph: an
[`idea_name`](https://vedicreader.github.io/abhyasa/learner.html#idea_name),
and not a statement of one word. The stray `x` of an old tool had the
statement `x`.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L409"
target="_blank" style="float:right; font-size:smaller">source</a>

### real_idea

``` python
def real_idea(
    name, statement
)->bool:
```

*Whether a new concept `name` with `statement` and no source text is an
idea: an
[`idea_name`](https://vedicreader.github.io/abhyasa/learner.html#idea_name),
and a statement of 3 words or more. A plan or a gap with no source uses
it.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L404"
target="_blank" style="float:right; font-size:smaller">source</a>

### idea_name

``` python
def idea_name(
    name
)->bool:
```

*Whether `name` can name a new idea: it has a letter or a digit, and it
is not one lower-case letter such as `x`. One capital letter, such as
`C` or `R`, can.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L400"
target="_blank" style="float:right; font-size:smaller">source</a>

### slugify

``` python
def slugify(
    s:str
)->str:
```

*Make a lower-case ASCII slug from `s` (vishalakshi `slug`, whole):
`saṃdhi` and `संधि` give `samdhi`, and `C++` gives `c-plus-plus`.*

[`_union`](https://vedicreader.github.io/abhyasa/learner.html#_union)
merges two lists and keeps the order.
[`concept_text`](https://vedicreader.github.io/abhyasa/learner.html#concept_text)
makes the text that search reads.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L433"
target="_blank" style="float:right; font-size:smaller">source</a>

### concept_text

``` python
def concept_text(
    c
)->str:
```

*The searchable text of a concept: its name, statement, aliases and
subject.*

[`Learning`](https://vedicreader.github.io/abhyasa/learner.html#learning)
opens one shelf of a vault. It also accepts a path, or `None` for the
default vault.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L440"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning

``` python
def Learning(
    vault:NoneType=None, shelf:str='learning'
):
```

*Concepts, attempts and sessions on one shelf of your vault.*

### Shelves of documents

A vault file holds more stores than the shelves of documents. For
example, the vault keeps the names that it finds in a store named
`entities`. That store has no `doc_id` column and no `docs` table.
vishalakshi’s `Vault.shelf_names` gives only the shelves that hold
documents. `doc_shelf` opens one of them with `shelf(create=False)`, or
gives None. It never makes a new shelf, so a name in a link or in an old
walk does not add a store to your vault.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L465"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.doc_shelf

``` python
def doc_shelf(
    name:str
):
```

*The shelf of documents `name` (vishalakshi `shelf(create=False)`), or
None when the vault has no such shelf. It never makes a shelf.*

### Read concepts

Each method returns concepts as `AttrDict`s with the learner state
filled in.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L490"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.subjects

``` python
def subjects()->list:
```

*The subjects that have concepts, with counts.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L484"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.concepts

``` python
def concepts(
    subject:str=None
)->fastcore.foundation.L:
```

*All concepts, or the concepts of one subject.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L478"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.concept_by_doc

``` python
def concept_by_doc(
    doc_id:str
)->fastcore.basics.AttrDict | None:
```

*The concept that a search hit (its `doc_id`) belongs to, or None when
the hit is not a concept.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L472"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.concept

``` python
def concept(
    slug:str
)->fastcore.basics.AttrDict | None:
```

*One concept by slug, with its learner state.*

### Write concepts

`put` adds a concept or adds to it. It keeps the learner state, merges
the list fields and replaces the other fields. It does not change the
group of a concept that the learner moved by hand (see Groups below).
Two fields say what the learner must understand. `core` is the
understanding in 1 or 2 sentences: what the idea does and why. It has no
sizes, years or scores, unless the number is the idea. `wrong` lists 1
to 3 common wrong ideas, each as `{'think': ..., 'fact': ...}`. The
question and the grade work from these two fields. `statement` stays as
the reference text. An empty `core` or `wrong` keeps the old value.
[`wrong_ideas`](https://vedicreader.github.io/abhyasa/learner.html#wrong_ideas)
makes each wrong idea a dict.
[`clean_name`](https://vedicreader.github.io/abhyasa/learner.html#clean_name)
makes a name one line with no control characters, at most `NAME_LEN`
characters. `put` and `rename` use it.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L508"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.put

``` python
def put(
    name:str, # the concept's name
    statement:str='', # a true, self-contained statement of the idea
    subject:str='', # e.g. 'sanskrit', 'jyotisha', 'ml'
    slug:str=None, # defaults to `slugify(name)`
    prereqs:tuple=(), # slugs this concept needs first
    related:tuple=(), # slugs linked to it, in any direction
    aliases:tuple=(), # other names
    provenance:tuple=(), # where it came from: dicts such as `{'src': 'pasted', 'at': ...}`
    questions:tuple=(), # ready questions: `{'q':..., 'a':..., 'depth':...}`
    core:str='', # the understanding: what the idea does and why, in 1 or 2 sentences
    wrong:tuple=(), # 1 to 3 common wrong ideas: `{'think':..., 'fact':...}`
    **meta
)->fastcore.basics.AttrDict:
```

*Add a concept or add to one. Keep the learner state, merge the list
fields and replace the other fields. A name that you set by hand stays.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L502"
target="_blank" style="float:right; font-size:smaller">source</a>

### wrong_ideas

``` python
def wrong_ideas(
    xs, n:int=3
)->list:
```

*At most `n` wrong ideas from `xs`, each as
`{'think': str, 'fact': str}`. A plain string is a `think` with no
`fact`; an empty one goes.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L498"
target="_blank" style="float:right; font-size:smaller">source</a>

### clean_name

``` python
def clean_name(
    s, n:int=200
)->str:
```

*`s` as one line of at most `n` characters, with no control or format
characters. The joiners of Indic scripts stay.*

[`update`](https://vedicreader.github.io/abhyasa/learner.html#update)
and `link` change one concept. `drop` removes it.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L556"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.drop

``` python
def drop(
    slug:str
):
```

*Remove a concept. Its attempts stay as a record of what happened.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L549"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.link

``` python
def link(
    slug:str, prereq:str
):
```

*Record that `prereq` comes before `slug`.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L541"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.update

``` python
def update(
    slug:str, **kv
)->fastcore.basics.AttrDict:
```

*Change the state of a concept and do not embed its text again.*

### Move old slugs

abhyasa 0.0.5 and older made slugs in a different way. A name in
Devanagari got a random slug, and C, C++ and C# got the same slug, `c`.
`reslug` finds each concept that has an old slug. It moves the concept
to the slug that
[`slugify`](https://vedicreader.github.io/abhyasa/learner.html#slugify)
gives now. It also changes the slug in each place that points to it: the
links of other concepts, attempts, sessions, walks and demos. A slug
that a source sets, such as `sa-term-karaka`, does not change.

`reslug` finds the moves first. With `dry_run` it stops there and writes
nothing.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L576"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.reslug

``` python
def reslug(
    dry_run:bool=False
)->dict:
```

*Move each concept with an old-style slug to the slug from
[`slugify`](https://vedicreader.github.io/abhyasa/learner.html#slugify),
and every slug that points to it. Returns `{old: new}`.*

### Groups

A concept can sit in a group under its subject. For example, Magha sits
at jyotisha › nakshatras › Magha. The path is a list of at most two
short names in the `group` field. `group_by` tells who set it: `hand`
for the learner, `model` for the tutor. The tutor never moves a concept
that the learner moved by hand. An old vault has no groups, so each
concept shows under “Not sorted yet”.

[`clean_group`](https://vedicreader.github.io/abhyasa/learner.html#clean_group)
makes a path safe to keep and to show. Each name has only letters,
digits, spaces and hyphens, and at most `GROUP_LEN` characters.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L603"
target="_blank" style="float:right; font-size:smaller">source</a>

### clean_group

``` python
def clean_group(
    path, subject:str=''
)->list:
```

*`path` as at most `GROUP_DEPTH` short plain names. A text splits at
`/`, `>` or `›`. Empty parts, repeats and a first part that is the
subject drop.*

`set_group` moves one concept. A move by hand marks it. A move by the
model does not change a concept with the mark. An empty path by hand
also keeps the mark: the learner chose no group, so the tutor does not
sort the concept again. `groups` lists the paths in use under one
subject, with their parents, so a new concept can go in a group that is
there.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L689"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.place

``` python
def place(
    slug:str, path
)->bool:
```

*Place a new concept in the group `path`, as the tutor. One step under a
lock: it looks at the concept again, and changes nothing when the
concept is gone, has a group, was kept out of the groups by hand, or was
undone. Return whether it placed the concept.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L681"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.unplace

``` python
def unplace(
    slug:str
)->fastcore.basics.AttrDict:
```

*Undo a placement by the tutor: no group, and the tutor does not place
the concept by itself again. A group that you set, or that a saved sort
set, stays.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L676"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.unsorted

``` python
def unsorted(
    subject:str=None
)->fastcore.foundation.L:
```

*The concepts that the tutor may place: no group, and not kept out of
the groups by hand.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L671"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.groups

``` python
def groups(
    subject:str=''
)->list:
```

*The group paths of `subject`: the kept ones and the ones in use, each
one time and with its parents, sorted.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L662"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.save_groups

``` python
def save_groups(
    subject:str='', groups:NoneType=None, replace:bool=False
)->list:
```

*Keep the group paths of `subject`: `groups` (default: the ones in use)
added to the ones kept before. A kept group is never dropped by the
tutor. With `replace`, the list becomes `groups` and the hand groups.
Return the list.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L657"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.hand_groups

``` python
def hand_groups(
    subject:str=''
)->list:
```

*The group paths of `subject` that you made or renamed. The tutor never
renames or drops them.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L651"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.saved_groups

``` python
def saved_groups(
    subject:str=''
)->list | None:
```

*The group paths kept for `subject` when a sort was saved; None when
none was.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L616"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.set_group

``` python
def set_group(
    slug:str, path, by:str='hand'
)->fastcore.basics.AttrDict:
```

*Put concept `slug` in the group `path`, set `by` the learner (`hand`)
or the tutor (`model`).*

#### Sort by hand

You can sort the concepts yourself. `rename` gives a concept a new name
and marks it `named_by='hand'`, so a new extract never renames it. The
old name stays as an alias, so the find box still finds it.
`rename_group` gives a group a new name. Each concept in the group, or
in a group below it, gets the new name and `group_by='hand'`. A name
that is the name of a group beside it, in any case and without marks,
joins the two groups. `new_group` adds an empty group. `hand_groups`
lists the groups that you made or renamed. “Sort everything again” keeps
them. `snapshot` and `restore` are the Undo of one change by hand:
`snapshot` keeps the fields that a change by hand can touch, and
`restore` writes them back.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L777"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.restore

``` python
def restore(
    snap
)->list:
```

*Write back the fields of `snap` (from `snapshot`). A concept that is
gone stays gone. Return the slugs written.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L771"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.snapshot

``` python
def snapshot(
    slugs:tuple=(), subjects:tuple=()
)->fastcore.basics.AttrDict:
```

*The fields that a change by hand can touch, for the concepts `slugs`
and the group lists of `subjects`, so `restore` can write them back.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L760"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.unmark_groups

``` python
def unmark_groups(
    subject:str, paths
)->list:
```

*Take the hand mark off the groups `paths` of `subject`, so the tutor
can sort them again. Return the hand groups.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L751"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.new_group

``` python
def new_group(
    subject:str, path
)->list:
```

*Add the empty group `path` to `subject`, made by hand. Return the group
list.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L736"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.rename_group

``` python
def rename_group(
    subject:str, path, name:str
)->list:
```

*Give the group `path` of `subject` the last name `name`, set by hand.
Its concepts and the groups below it move with it. A name of a group
beside it joins the two. Return the slugs changed.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L725"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.rename

``` python
def rename(
    slug:str, name:str
)->fastcore.basics.AttrDict:
```

*Give concept `slug` the name `name`, set by hand: a new extract never
renames it. The old name becomes an alias.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L704"
target="_blank" style="float:right; font-size:smaller">source</a>

### under

``` python
def under(
    g, path
)->bool:
```

*Whether the group `g` is the group `path` or a group below it. Case and
marks do not count.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L698"
target="_blank" style="float:right; font-size:smaller">source</a>

### fold

``` python
def fold(
    s
)->str:
```

*`s` in lower case with no marks (litesearch `fold_text`), so `Maghā`
and `MAGHA` are one name, and `संधि` gives `samdhi`.*

`set_subject` gives a subject to a concept.
[`readable_name`](https://vedicreader.github.io/abhyasa/learner.html#readable_name)
and `repair_names` make a readable name for a concept whose name is its
slug.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L813"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.repair_names

``` python
def repair_names()->list:
```

*One time for each shelf, give each concept whose name is its own slug,
with hyphens, a readable name. Return the slugs that changed. A name
that differs from the slug, or that you set by hand (`named_by`),
stays.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L805"
target="_blank" style="float:right; font-size:smaller">source</a>

### readable_name

``` python
def readable_name(
    name, statement:str=''
)->str:
```

*`name` as words: a name that is a slug with hyphens, or an empty name
(words from `statement`), becomes a short sentence-case name. Else
`name`.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L797"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.set_subject

``` python
def set_subject(
    slug:str, subject:str, by:str='model'
)->fastcore.basics.AttrDict:
```

*Give concept `slug` a subject, set `by` the learner (`hand`) or the
tutor (`model`). The tutor sets only an empty subject, and never on a
concept that you moved by hand.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L792"
target="_blank" style="float:right; font-size:smaller">source</a>

### clean_subject

``` python
def clean_subject(
    s
)->str:
```

*`s` as one short plain lower-case name; ’’ when it has none.*

[`tree`](https://vedicreader.github.io/abhyasa/learner.html#tree) puts
concepts in a tree for the Concepts page: a node for each subject, then
its groups, then their groups. A node has its `name`, its `path` under
the subject, `n` (the concepts in it and below it), `kids` (its groups
by name) and `cs` (its own concepts). A concept with no group is in `cs`
of its subject. Two names that differ only in case are one group.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L824"
target="_blank" style="float:right; font-size:smaller">source</a>

### tree

``` python
def tree(
    concepts, groups:NoneType=None
)->list:
```

*The subject nodes of `concepts`, sorted by name, each with its groups
and the count of concepts below it. `groups` (`{subject: paths}`) adds
groups with no concept, under a subject that shows.*

### Attempts

Each answer is one attempt document. Thus search can find what the
learner said. The tutor can also find old questions and not ask them
again.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L844"
target="_blank" style="float:right; font-size:smaller">source</a>

### attempt_text

``` python
def attempt_text(
    a
)->str:
```

*The searchable text of an attempt: the question, the answer, the
think-aloud and the feedback.*

`record` saves one attempt. The vault joins two sources with the same
text into one document. So `record` asks for a new document each time
(`force=True`): two attempts with the same words stay two records. Each
one has its own source, `learning://attempt/<id>`.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L852"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.record

``` python
def record(
    concept:str, question:str, answer:str='', thinking:str='', correct:bool=False, confidence:int=None, depth:int=1,
    kind:str='explain', hints:int=0, dont_know:bool=False, grade:int=None, feedback:str='', first_error:str='',
    misconception:str='', session:str='', **meta
)->fastcore.basics.AttrDict:
```

*File one attempt as a vault document, so the learner can search what
they said.*

`attempts` reads them back, with the newest first.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L867"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.attempts

``` python
def attempts(
    concept:str=None, session:str=None, limit:int=50, since:float=None
)->fastcore.foundation.L:
```

*Recent attempts, newest first, for one concept or one session; with
`since`, only the attempts from that time on.*

`mark_attempt` adds fields to a saved attempt, for example a lesson that
you asked for later. `add_gap` keeps a gap that you named on a concept.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L885"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.add_gap

``` python
def add_gap(
    slug:str, gap:str
):
```

*Record that you said you do not know concept `gap` while you learned
`slug`.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L878"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.mark_attempt

``` python
def mark_attempt(
    aid:str, **kv
)->dict:
```

*Merge `kv` into the meta of attempt `aid`.*

### Sessions

A session summary is one document. A new summary with the same id
replaces the old one.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L906"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.sessions

``` python
def sessions(
    limit:int=100, journey:str=None
)->fastcore.foundation.L:
```

*The filed sessions, the most recently changed first; with `journey`,
only the sessions of that walk.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L900"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.session

``` python
def session(
    sid:str
)->fastcore.basics.AttrDict | None:
```

*One filed session by id, as its meta.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L893"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.log_session

``` python
def log_session(
    sid:str, summary:str, **meta
):
```

*File a session, and replace an earlier one with the same id. The meta
changes also when the summary text does not.*

### Search and graph

`search` uses the hybrid search of the vault. Each hit has the kind,
title and meta of its document. The method stats, the marker of a
repair, a session with no answers and an open question stay out of it.
Two attempts with the same text show as one hit. `graph` gives the nodes
and links for the graph view.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L943"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.graph

``` python
def graph(
    subject:str=None
)->dict:
```

*Nodes and links (prereq, related, gap) for a graph view. A link to a
slug that has no concept is dropped.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L921"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.search

``` python
def search(
    q:str, kind:str=None, limit:int=10
)->fastcore.foundation.L:
```

*Hybrid search over the shelf. Each hit also has the `kind`, `title` and
`meta` of its document, so a page can show it* by kind. The method
stats, the marker of a repair, the history of the changes to a talk or a
walk, a session with no answers and an open question stay out: they have
no text that you wrote or learned, and an open question has its answer
key.

### A shelf for tests

Each test uses a vault in a temporary folder with the offline hashing
encoder. The tests never open the real vault of the learner.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L954"
target="_blank" style="float:right; font-size:smaller">source</a>

### temp_learning

``` python
def temp_learning(
    **kw
)->__main__.Learning:
```

*A
[`Learning`](https://vedicreader.github.io/abhyasa/learner.html#learning)
on a new vault in a temporary folder (vishalakshi `Vault.temp`), with
the offline hashing encoder.*

### Tests

`put` makes a concept with the start state and the slug from its name.

``` python
lr = temp_learning()
c = lr.put('Reciprocal rank fusion', 'Merge ranked lists by summing 1/(k + rank) for each item.', 'ml', prereqs=['ranking'], aliases=['RRF'])
test_eq(c.slug, 'reciprocal-rank-fusion')
test_eq(c.floor, 0)
test_eq(c.prereqs, ['ranking'])
```

[`update`](https://vedicreader.github.io/abhyasa/learner.html#update)
changes the state.

``` python
c = lr.update(c.slug, floor=3, stability=2.)
test_eq((c.floor, c.stability), (3, 2.))
```

A concept keeps its `core` and its wrong ideas. A later `put` with no
`core` keeps them. A wrong idea as a plain string becomes a dict.

``` python
wl = temp_learning()
c = wl.put('Token embeddings', 'Each token ID picks a row of a learned matrix.', 'ml', core='Each token gets a learned vector, so tokens used in the same way end up close.',
           wrong=['the ID number itself carries meaning', dict(think='the vectors are fixed', fact='training changes them')])
test_eq(c.wrong, [dict(think='the ID number itself carries meaning', fact=''), dict(think='the vectors are fixed', fact='training changes them')])
test_eq(wl.put('Token embeddings', 'Rows of E.').core, c.core)
```

A second `put` keeps the learner state and merges the lists, also when
the text changes.

``` python
c = lr.put('Reciprocal rank fusion', 'Merge ranked lists: score is the sum of 1/(k + rank).', related=['bm25'], aliases=['RRF', 'rank fusion'])
test_eq(c.floor, 3)
test_eq(c.subject, 'ml')
test_eq(c.aliases, ['RRF', 'rank fusion'])
test_eq(c.related, ['bm25'])
test_eq(len(lr.concepts()), 1)
```

`concepts` and `subjects` filter by subject. `graph` keeps only the
links between known concepts.

``` python
lr.put('Ranking', 'Order items by a score.', 'ml')
lr.put('Sandhi', 'Sounds change where words meet.', 'sanskrit')
test_eq(len(lr.concepts('ml')), 2)
test_eq({r['subject'] for r in lr.subjects()}, {'ml', 'sanskrit'})
g = lr.graph()
test_eq(g['links'], [dict(source='ranking', target='reciprocal-rank-fusion', kind='prereq')])
```

`record` saves an attempt. `attempts` and `search` find it again.

``` python
a = lr.record('reciprocal-rank-fusion', 'Why does RRF ignore raw scores?', answer='Because scores from different systems are not comparable.',
              thinking='bm25 and cosine have different scales', correct=True, confidence=80, depth=3)
test_eq(lr.attempts('reciprocal-rank-fusion')[0].id, a.id)
test_eq(lr.attempts('ranking'), [])
test(lr.search('scales comparable', kind='attempt'), [], lambda a,b: len(a) > 0)
```

`drop` removes a concept.

``` python
lr.drop('ranking')
test_is(lr.concept('ranking'), None)
```

#### Shelves of documents

A real vault also keeps a store of names, `entities`, with no `doc_id`
column. `shelf_names` leaves it out. It keeps the main shelf, the
learning shelf and a shelf of notebooks.

``` python
dv = temp_learning()
dv.vault.shelf('notebooks').add('Notes on softmax.', title='nbs/softmax', source='/x/softmax.md', kind='md')
dv.vault.db.get_graph('store', ann=False)   # the store of names, as vishalakshi makes it
assert 'entities' in dv.vault.db.table_names()   # the store of names is in the vault, though vishalakshi 0.1.36 no longer lists it as a shelf
test_eq(sorted(dv.vault.shelf_names()), ['learning', 'notebooks', 'store'])
```

`shelf_names` reads only the names of the stores. It does not count the
documents of each store, so SQLite logs no error for a store of names
with no `docs` table. A page can call it many times.

``` python
import logging
```

``` python
class _Seen(logging.Handler):
    def __init__(self): super().__init__(); self.got = []
    def emit(self, r): self.got.append(r.getMessage())
seen, root = _Seen(), logging.getLogger()
root.addHandler(seen)
try: test_eq(sorted(dv.vault.shelf_names()), ['learning', 'notebooks', 'store'])
finally: root.removeHandler(seen)
test_eq([m for m in seen.got if 'no such table' in m], [])
```

`doc_shelf` opens a shelf of documents. It gives None for the store of
names and for a name that is not in the vault, and it does not make that
shelf.

``` python
test_eq(dv.doc_shelf('notebooks').t.docs()[0]['title'], 'nbs/softmax')
test_is(dv.doc_shelf('store'), dv.vault)
test_is(dv.doc_shelf('entities'), None)
test_is(dv.doc_shelf('nope'), None)
assert 'nope' not in [s['store'] for s in dv.vault.shelves()]
```

The search fetches more hits until it has `limit` real hits, or the
shelf has no more. Here 30 sessions with no answers match the words
best. Three concepts match only one word. All three concepts come back.

``` python
sl = temp_learning()
for k in range(30): sl.log_session(f'empty{k}', f'zebra quagga okapi zebra quagga okapi {k}', start=1., log=[])
for k in range(3): sl.put(f'Giraffe {k}', f'A tall animal {k}, once next to an okapi.', 'zoo')
test_eq(len(sl.search('zebra quagga okapi', limit=3)), 3)
```

``` python
lr.log_session('s1', '1 of 2 passed.', start=1., updated=2., asked=2)
lr.log_session('s1', '1 of 2 passed.', start=1., updated=3., asked=2, ended=3.)   # the same text: the meta still changes
test_eq((lr.session('s1').updated, lr.session('s1').ended, lr.session('s1').summary), (3., 3., '1 of 2 passed.'))
test_eq(len(lr.sessions()), 1)
```

`sessions` lists the most recently changed session first.

``` python
lr.log_session('s2', 'Started.', start=5., updated=5.)
test_eq(lr.sessions().attrgot('id'), ['s2', 's1'])
test_is(lr.session('nope'), None)
```

With `journey`, `sessions` reads only the sessions of that walk, in the
store.

``` python
lr.log_session('s3', 'A walk.', start=6., journey='j1')
test_eq(lr.sessions(journey='j1').attrgot('id'), ['s3'])
test_eq(len(lr.sessions()), 3)
```

`search` leaves out a session with no answers and the method stats. The
session is still filed, so you can continue it. Each hit names its kind.

``` python
lr.log_session('s4', '0 of 0 passed. Concepts: none.', start=7., log=[])
lr.v.add('How you learn: method counts, passed.', title='How you learn', source='stats:test', kind='stats')
hs = lr.search('passed', limit=20)
test_eq([h for h in hs if h['kind'] == 'stats' or h['meta'].get('id') == 's4'], [])
test_eq('s1' in [h['meta'].get('id') for h in hs if h['kind'] == 'session'], True)   # a session with answers stays
test_eq(lr.session('s4').id, 's4')
```

The search fetches more hits until it has `limit` real hits, or the
shelf has no more. Here 30 sessions with no answers match the words
first, and three concepts match them too. All three concepts come back.

`concept_by_doc` gives the concept of a search hit. It gives None for a
hit that is not a concept.

``` python
cl = temp_learning()
cl.put('Reciprocal rank fusion', 'Merge ranked lists by summing 1/(k + rank).', 'ml')
cl.record('reciprocal-rank-fusion', 'Why ranks?', answer='scales differ')
test_eq(cl.concept_by_doc(cl.search('rank fusion', kind='concept')[0]['doc_id']).slug, 'reciprocal-rank-fusion')
test_is(cl.concept_by_doc(cl.search('scales differ', kind='attempt')[0]['doc_id']), None)
```

[`slugify`](https://vedicreader.github.io/abhyasa/learner.html#slugify)
reads Devanagari and Telugu, keeps C, C++ and C# apart, and gives the
same hash slug for the same unreadable name.

``` python
test_eq([slugify(s) for s in ('saṃdhi', 'संधि', 'గుణ', 'C', 'C++', 'C#', 'R&D')], ['samdhi', 'samdhi', 'guna', 'c', 'c-plus-plus', 'c-sharp', 'r-and-d'])
test_eq(slugify('学习'), slugify('学习'))
assert slugify('学习').startswith('c-')
```

This test makes a vault for `reslug` as abhyasa 0.0.5 left it: C# saved
as `c`, a Devanagari name with a random slug, and links, an attempt and
a session that use these slugs.

``` python
ol = temp_learning()
ol.put('C#', 'A language from Microsoft.', 'code', slug='c')
ol.put('संधि', 'Sounds change where words meet.', 'sanskrit', slug='a1b2c3d4')
ol.put('karaka term', 'A set slug.', 'sanskrit', slug='sa-term-karaka')
ol.put('Ranking', 'Order items by a score.', 'ml', prereqs=['c'], related=['a1b2c3d4', 'sa-term-karaka'])
ol.record('c', 'What is C#?', answer='c')
ol.log_session('s1', 'One question.', concepts=['c'], log=[dict(concept='c', question='What is C#?')], focus='a1b2c3d4')
```

A dry run lists the moves and changes nothing. A slug that a source sets
is not in the list.

``` python
test_eq(ol.reslug(dry_run=True), {'c': 'c-sharp', 'a1b2c3d4': 'samdhi'})
assert ol.concept('c') is not None and ol.concept('c-sharp') is None
```

The real run moves the concepts and every slug that points to them. An
answer that is the text `c` stays as it is. A second run has nothing to
do.

``` python
test_eq(ol.reslug(), {'c': 'c-sharp', 'a1b2c3d4': 'samdhi'})
test_eq((ol.concept('c'), ol.concept('a1b2c3d4')), (None, None))
test_eq((ol.concept('c-sharp').name, ol.concept('samdhi').statement), ('C#', 'Sounds change where words meet.'))
r = ol.concept('ranking')
test_eq((r.prereqs, r.related), (['c-sharp'], ['samdhi', 'sa-term-karaka']))
a = ol.attempts('c-sharp')[0]
test_eq((a.concept, a.answer), ('c-sharp', 'c'))
s = ol.session('s1')
test_eq((s.concepts, s.log[0]['concept'], s.focus), (['c-sharp'], 'c-sharp', 'samdhi'))
test_eq(ol.reslug(), {})
```

`mark_attempt` changes a saved attempt. `add_gap` keeps each gap one
time, and `graph` shows it as a `gap` link.

``` python
gl = temp_learning()
gl.put('Functions and the neuron', 'z = w . x + b.', 'ml'); gl.put('Weighted sum', 'Add each input times its weight.', 'ml')
a = gl.record('functions-and-the-neuron', 'Why does z double?', answer='2z')
test_eq(gl.mark_attempt(a.id, taught=True)['taught'], True)
test_eq(gl.attempts('functions-and-the-neuron')[0].taught, True)
gl.add_gap('functions-and-the-neuron', 'weighted-sum'); gl.add_gap('functions-and-the-neuron', 'weighted-sum')
test_eq(gl.concept('functions-and-the-neuron').gaps, ['weighted-sum'])
assert dict(source='weighted-sum', target='functions-and-the-neuron', kind='gap') in gl.graph()['links']
```

Two attempts with the same text are two records. The vault does not join
them, so each one counts in your history and in the misses that the
tutor counts. A field added later goes on the right attempt.

``` python
ld = temp_learning(); ld.put('Ranking', 'Order items by a score.', 'ml')
a1, a2 = [ld.record('ranking', 'What is a rank?', 'a score', feedback='Look at positions.', final=True) for _ in range(2)]
test_eq(ld.attempts('ranking').attrgot('id'), [a2.id, a1.id])
ld.mark_attempt(a2.id, taught=1)
test_eq([a.get('taught') for a in ld.attempts('ranking')], [1, None])
```

A search shows the same attempt text only once.

``` python
test_eq(len([h for h in ld.search('What is a rank', kind='attempt') if h['kind'] == 'attempt']), 1)
```

### Groups

An old vault has no groups. A concept loads with an empty path, and no
one set it.

``` python
gv = temp_learning()
m = gv.put('Magha', 'The tenth nakshatra.', 'jyotisha')
test_eq((m.get('group'), m.get('group_by')), (None, None))
```

[`clean_group`](https://vedicreader.github.io/abhyasa/learner.html#clean_group)
keeps at most two short plain names. It drops marks, empty parts, a
repeat and a first part that is the subject. A text splits at `/`, `>`
or `›`.

``` python
test_eq(clean_group(['Nakshatras', 'Padas', 'More']), ['Nakshatras', 'Padas'])
test_eq(clean_group(' nakshatras  /  padas! '), ['nakshatras', 'padas'])
test_eq(clean_group(['Nakshatras!', '  deva   gana? ']), ['Nakshatras', 'deva gana'])
test_eq(clean_group(['jyotisha', 'nakshatras'], 'jyotisha'), ['nakshatras'])
test_eq(clean_group(['', 'Nakshatras', 'nakshatras', 3]), ['Nakshatras'])
test_eq(clean_group('saṃdhi › स्वर'), ['saṃdhi', 'स्वर'])
test_eq(len(clean_group(['x' * 90])[0]), GROUP_LEN)
test_eq((clean_group(None), clean_group({'a': 1}), clean_group('')), ([], [], []))
```

`set_group` by the model moves a concept. A move by hand sets
`group_by='hand'`. After that, a move by the model changes nothing.

``` python
test_eq(gv.set_group('magha', ['nakshatras'], by='model').group, ['nakshatras'])
m = gv.set_group('magha', 'Nakshatras / Gana')
test_eq((m.group, m.group_by), (['Nakshatras', 'Gana'], 'hand'))
test_eq(gv.set_group('magha', ['rasis'], by='model').group, ['Nakshatras', 'Gana'])
```

A new `put` with a group, as from a new source, does not move a concept
that you moved by hand. It keeps the learner state.

``` python
m = gv.put('Magha', 'The tenth nakshatra, ruled by Ketu.', 'jyotisha', group=['rasis'], group_by='model')
test_eq((m.group, m.group_by, m.statement), (['Nakshatras', 'Gana'], 'hand', 'The tenth nakshatra, ruled by Ketu.'))
```

An empty path by hand takes the concept out of its group and keeps the
mark. So the tutor does not sort it again, and a move by the model
changes nothing.

``` python
m = gv.set_group('magha', '')
test_eq((m.group, m.group_by), ([], 'hand'))
test_eq(gv.set_group('magha', ['rasis'], by='model').group, [])
```

`groups` lists the paths in use under one subject, with their parents. A
path in another subject does not show.

`set_subject` gives a subject to a concept that has none. A move by the
tutor never changes a concept that you moved by hand, and it never
replaces a subject. A move by hand always works.

``` python
sj = temp_learning()
sj.put('Alpha', 'One.'); sj.put('Beta', 'Two.'); sj.put('Gamma', 'Three.', 'ml')
sj.set_group('beta', 'mine')
test_eq(sj.set_subject('alpha', ' Star  Maps! ').subject, 'star maps')
test_eq(sj.concept('alpha').subject_by, 'model')
test_eq(sj.set_subject('beta', 'stars').subject, '')                 # moved by hand: the tutor does not touch it
test_eq(sj.set_subject('gamma', 'stars').subject, 'ml')              # it has a subject
test_eq(sj.set_subject('gamma', 'stars', by='hand').subject, 'stars')
```

A name that is its own slug, with hyphens, gets a readable name one
time. A name that differs from the slug stays. The empty name of the
finder gets words from the statement.

``` python
test_eq(readable_name('attention-heads-and-anaphora-resolution'), 'Attention heads and anaphora resolution')
test_eq(readable_name('Softmax'), 'Softmax'); test_eq(readable_name('softmax'), 'softmax'); test_eq(readable_name('my-Note'), 'my-Note')
test_eq(readable_name('', 'A rank is a position in a list. More.'), 'A rank is a position in')
rn = temp_learning()
rn.put('attention-heads-and-anaphora-resolution', 'Heads.', 'ml', slug='attention-heads-and-anaphora-resolution')
rn.put('my-note', 'Mine.', 'ml', slug='note')
rn.put('foo-bar', 'Typed.', 'ml', slug='foo-bar', named_by='hand')
def old_vault(l):
    "Forget the mark of the repair, as in a vault from before it."
    l.v.forget(first(l.v.t.docs(where='source=?', where_args=['learning://marker/names']))['id'])
old_vault(rn)
test_eq(Learning(rn.vault).concept('attention-heads-and-anaphora-resolution').name, 'Attention heads and anaphora resolution')
test_eq((rn.concept('note').name, rn.concept('foo-bar').name), ('my-note', 'foo-bar'))   # a name that differs from the slug, or that you typed, stays
test_eq(rn.repair_names(), [])
```

The repair runs one time: a mark on the shelf says that it ran. A vault
that cannot be read or changed does not break the load. A change of name
or subject also makes the search text and the title again.

``` python
from unittest.mock import patch as mock_patch
```

``` python
rn.put('late-slug-name', 'Late.', 'ml', slug='late-slug-name')
test_eq(rn.repair_names(), [])                                        # the mark is set: it does not look again
test_eq(rn.concept('late-slug-name').name, 'late-slug-name')
old_vault(rn)
with mock_patch.object(Learning, 'repair_names', side_effect=OSError('read only')): Learning(rn.vault)   # the load goes on
test_eq(rn.repair_names(), ['late-slug-name'])
doc = lambda l, s: first(l.v.t.docs(where='source=?', where_args=[f'learning://concept/{s}']))
test_eq(doc(rn, 'late-slug-name')['title'], 'Late slug name')
sj.set_subject('gamma', 'cosmos', by='hand')
test_eq(sj.concept('gamma').subject, 'cosmos')
assert sj.concept('gamma').doc_id in [h['doc_id'] for h in sj.v.search('cosmos', 3)]   # the search text has the new subject
```

The mark of the repair is a document, but search never gives it.

``` python
mt = temp_learning(); mt.put('Names', 'Names of concepts repaired.', 'ml')
assert first(mt.v.t.docs(where='kind=?', where_args=['marker']))
test_eq([h['kind'] for h in mt.search('names concepts repaired')], ['concept'])
test_eq(mt.search('Names repaired', kind='marker'), [])
```

#### Saved groups stay

When you save a sort, the tutor keeps the group list of each subject and
does not move a sorted concept again. `save_groups` keeps the list.
`groups` gives the saved groups and the groups in use. A saved group
stays when no concept is in it. `unsorted` lists the concepts that the
tutor may still place: no group, and not set by hand.

``` python
cl = temp_learning()
for n in ('Ashvini', 'Bharani', 'Krittika'): cl.put(n, f'{n}.', 'jyotisha')
test_is(cl.saved_groups('jyotisha'), None)
cl.set_group('ashvini', ['Stars'], by='sort')
test_eq(cl.save_groups('jyotisha'), [['Stars']])
cl.set_group('ashvini', [], by='hand'); cl.set_group('bharani', ['Signs'], by='sort')
test_eq(cl.save_groups('jyotisha'), [['Signs'], ['Stars']])           # a kept group stays when no concept is in it
test_eq(cl.groups('jyotisha'), [['Signs'], ['Stars']])
test_eq(cl.saved_groups('JYOTISHA'), [['Signs'], ['Stars']])
test_eq(cl.concept('bharani').group_by, 'sort')
```

A concept with a group is sorted. A concept with no group is not sorted,
except when you set it by hand to no group. `unplace` is the Undo of a
placement by the tutor: the concept has no group, and the tutor does not
place it by itself again.

``` python
test_eq(cl.unsorted('jyotisha').attrgot('slug'), ['krittika'])         # ashvini: kept out by hand; bharani: sorted
cl.set_group('krittika', ['Stars'], by='placed')
test_eq(cl.unplace('krittika').group_by, 'undone')
test_eq((cl.concept('krittika').group, cl.unsorted('jyotisha').attrgot('slug')), ([], ['krittika']))
test_eq(cl.unplace('bharani').group, ['Signs'])                         # only a placement by the tutor can be undone
test_eq(cl.unplace('ashvini').group_by, 'hand')
```

A name with fewer than two characters, or with no letter or digit, is
not an idea:
[`usable_name`](https://vedicreader.github.io/abhyasa/learner.html#usable_name)
says so,
[`idea_name`](https://vedicreader.github.io/abhyasa/learner.html#idea_name)
is the rule for the name of a new concept: a letter or a digit, and not
one small letter such as `x`. One capital letter, such as `C`, is a
name. `put` refuses a new concept with another name, so no tool, plan or
text can file one. A plan or a gap has no source text, so it also needs
[`real_idea`](https://vedicreader.github.io/abhyasa/learner.html#real_idea):
a statement of 3 words or more. Library and the graph leave out a
concept that breaks the name rule or has a statement of one word
([`listed`](https://vedicreader.github.io/abhyasa/learner.html#listed)),
such as the `x` that an older tool filed. The marker and the group list
never show in search.

``` python
assert usable_name('Ab') and usable_name('x2') and usable_name('C#') and not usable_name('x') and not usable_name(' - ') and not usable_name(None)
test_fail(lambda: cl.put('x', 'x', 'code'), contains='not the name of an idea')         # one small letter
test_fail(lambda: cl.put('z', 'z is a letter for a value.', 'code'), contains='not the name of an idea')
test_eq(cl.put('C', 'C is a compiled systems language.', 'code').name, 'C')            # one capital letter
assert cl.concept('x') is None and listed(cl.concept('c')) and not listed(dict(name='Bias', statement='x'))
test_eq((real_idea('Bias', 'x'), real_idea('C', 'C is a language.')), (False, True))   # with no source text, a statement of 3 words or more
cl.drop('c')
test_eq(cl.search('Groups of jyotisha', kind='groups'), [])
assert not [h for h in cl.search('Groups of jyotisha') if h['kind'] == 'groups']
```

``` python
gv.put('Ashvini', 'The first nakshatra.', 'jyotisha', group=['nakshatras', 'deva gana'])
gv.put('Mesha', 'The first rasi.', 'jyotisha', group=['Rasis'])
gv.put('Sandhi', 'Sounds change where words meet.', 'sanskrit', group=['phonology'])
test_eq(gv.groups('jyotisha'), [['nakshatras'], ['nakshatras', 'deva gana'], ['Rasis']])
test_eq(gv.groups('ml'), [])
```

[`tree`](https://vedicreader.github.io/abhyasa/learner.html#tree) puts
the concepts under their subject, then their groups. Each node has the
count of the concepts in it and below it. A concept with no group is an
item of its subject. A group name in another case is the same group.

``` python
gv.put('Bharani', 'The second nakshatra.', 'jyotisha', group=['Nakshatras'])
t = tree(gv.concepts())
test_eq([(s.name, s.n) for s in t], [('jyotisha', 4), ('sanskrit', 1)])
j = t[0]
test_eq([(k.name, k.n) for k in j.kids], [('nakshatras', 2), ('Rasis', 1)])
test_eq([c.name for c in j.cs], ['Magha'])
test_eq([(k.path, [c.name for c in k.cs]) for k in j.kids[0].kids], [(['nakshatras', 'deva gana'], ['Ashvini'])])
test_eq([c.name for c in j.kids[0].cs], ['Bharani'])
```

#### Sort by hand

A renamed group takes its concepts with it. They and the group get the
hand mark, so the tutor never moves them.

``` python
hs = temp_learning()
for n in ('Ashvini', 'Bharani'): hs.put(n, f'{n}.', 'jyotisha', group=['nakshatras'])
hs.save_groups('jyotisha')
test_eq(sorted(hs.rename_group('jyotisha', ['Nakshatras'], 'Lunar mansions')), ['ashvini', 'bharani'])
test_eq({(tuple(c.group), c.group_by) for c in hs.concepts('jyotisha')}, {(('Lunar mansions',), 'hand')})
test_eq((hs.hand_groups('jyotisha'), hs.groups('jyotisha')), ([['Lunar mansions']], [['Lunar mansions']]))
assert not hs.unsorted('jyotisha') and not hs.place('ashvini', ['Other'])
```

A name that is the name of a group beside it, in another case, joins the
two groups. The group below a renamed group moves with it.

``` python
hs.put('Pada', 'A quarter.', 'jyotisha', group=['Stars', 'padas']); hs.put('Pada two', 'A quarter.', 'jyotisha', group=['Stars x'])
hs.rename_group('jyotisha', ['Stars x'], 'STARS')
test_eq(hs.concept('pada-two').group, ['Stars'])
hs.rename_group('jyotisha', ['Stars'], 'Asterisms')
test_eq((hs.concept('pada').group, hs.concept('pada-two').group), (['Asterisms', 'padas'], ['Asterisms']))
assert ['Stars'] not in hs.groups('jyotisha') and ['Stars x'] not in hs.groups('jyotisha')
test_fail(lambda: hs.rename_group('jyotisha', ['Asterisms'], '!!!'), contains='letters or digits')
```

`new_group` adds an empty group that stays. `save_groups(replace=True)`
makes the list again and keeps the hand groups. `unmark_groups` takes
the mark off.

``` python
hs.new_group('jyotisha', 'Rasis')
assert ['Rasis'] in hs.groups('jyotisha') and ['Rasis'] in hs.hand_groups('jyotisha')
test_eq(hs.save_groups('jyotisha', [['Grahas']], replace=True), [['Asterisms'], ['Grahas'], ['Lunar mansions'], ['Rasis']])
assert ['Asterisms', 'padas'] in hs.groups('jyotisha')   # a group in use still shows
hs.unmark_groups('jyotisha', [['rasis']])
assert ['Rasis'] not in hs.hand_groups('jyotisha') and ['Rasis'] in hs.groups('jyotisha')
```

A name that you set stays when a new extract puts the concept again. The
old name becomes an alias.

``` python
hs.put('Self-attention', 'x', 'ml'); c = hs.rename('self-attention', 'Attention over one sequence')
test_eq((c.name, c.named_by, c.aliases), ('Attention over one sequence', 'hand', ['Self-attention']))
test_eq(hs.put('Self-attention', 'y', 'ml').name, 'Attention over one sequence')
test_fail(lambda: hs.rename('self-attention', ' x '), contains='two letters')
test_eq(hs.rename('self-attention', 'Self\x00 at\x1b[31mtention\n' + 'x' * 200).name, 'Self at [31mtention ' + 'x' * 100)   # no control characters, and a cap
hs.rename('self-attention', 'Attention over one sequence')
test_eq(hs.put('Bad\x07 name', 'x', 'ml').name, 'Bad name')
```

`snapshot` and `restore` undo one change by hand: the groups of the
concepts, their names and the group list.

``` python
snap = hs.snapshot(['pada', 'self-attention'], ['jyotisha'])
hs.rename_group('jyotisha', ['Asterisms'], 'Stars again'); hs.rename('self-attention', 'Self focus'); hs.new_group('jyotisha', 'Extra')
test_eq(sorted(hs.restore(snap)), ['pada', 'self-attention'])
test_eq((hs.concept('pada').group, hs.concept('self-attention').name), (['Asterisms', 'padas'], 'Attention over one sequence'))
assert ['Extra'] not in hs.groups('jyotisha') and ['Asterisms'] in hs.saved_groups('jyotisha')
snap = hs.snapshot([], ['ml']); test_is(hs.saved_groups('ml'), None)
hs.new_group('ml', 'Heads'); hs.restore(snap)
test_is(hs.saved_groups('ml'), None)   # Undo takes away a list that was not there
```

## The learner model

The learner model: how deep you know each concept, when to review it,
and what to ask next.

This module holds the learner model as plain functions with no model
calls and no storage. It finds what an answer means, updates the state
of a concept, finds the next depth and picks the next concept. The tutor
calls these functions and the store keeps their results.

Import the tools that this module uses.

### Depth

Each concept has a depth from 0 to 5. The tutor asks each question at
one depth.

<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr>
<th>depth</th>
<th>name</th>
<th>a question at this depth asks you to</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td><code>unseen</code></td>
<td>(no question)</td>
</tr>
<tr>
<td>1</td>
<td><code>recognise</code></td>
<td>pick the correct statement from options</td>
</tr>
<tr>
<td>2</td>
<td><a
href="https://vedicreader.github.io/abhyasa/learner.html#recall"><code>recall</code></a></td>
<td>state the idea or the rule</td>
</tr>
<tr>
<td>3</td>
<td><a
href="https://vedicreader.github.io/abhyasa/show.html#explain"><code>explain</code></a></td>
<td>say why it is true, in your own words</td>
</tr>
<tr>
<td>4</td>
<td><code>apply</code></td>
<td>use it to solve a problem</td>
</tr>
<tr>
<td>5</td>
<td><code>transfer</code></td>
<td>use it in a new case or link it to another subject</td>
</tr>
</tbody>
</table>

The floor is the highest depth that you pass reliably. The ceiling is
the lowest depth that you failed (6 until you fail). The next question
goes between them. Thus each answer moves one edge of this range.

These constants name the depths, give the question kind at each depth,
and set the goal and the high-confidence line.

### Outcome

Correctness and confidence together tell what an answer means.

<table>
<thead>
<tr>
<th></th>
<th>high confidence</th>
<th>low confidence</th>
</tr>
</thead>
<tbody>
<tr>
<td>correct</td>
<td><code>solid</code></td>
<td><code>fragile</code></td>
</tr>
<tr>
<td>wrong</td>
<td><code>misconception</code></td>
<td><code>gap</code></td>
</tr>
</tbody>
</table>

“I don’t know” is a `gap`. A correct answer that used two or more hints
is `fragile`.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L964"
target="_blank" style="float:right; font-size:smaller">source</a>

### outcome

``` python
def outcome(
    correct:bool, confidence:int=None, hints:int=0, dont_know:bool=False
)->str:
```

*One of `solid`, `fragile`, `misconception` or `gap`. With no
`confidence`, only the answer and the hints count.*

A correct answer is `solid` with high confidence. Two hints or low
confidence make it `fragile`.

``` python
test_eq(outcome(True, 80), 'solid')
test_eq(outcome(True, 80, hints=2), 'fragile')
test_eq(outcome(True, 30), 'fragile')
```

With no confidence given, a clean pass is `solid` and a wrong answer is
a `gap`, never a `misconception`.

``` python
test_eq([outcome(True), outcome(True, hints=2), outcome(False)], ['solid', 'fragile', 'gap'])
```

A wrong answer is a `misconception` with high confidence, else a `gap`.
“I don’t know” is always a `gap`.

``` python
test_eq(outcome(False, 90), 'misconception')
test_eq(outcome(False, 20), 'gap')
test_eq(outcome(True, 90, dont_know=True), 'gap')
```

### Memory

Recall falls with time as in FSRS:
$R(t) = (1 + \frac{19}{81}\frac{t}{S})^{-1/2}$. The time *t* and the
stability *S* are in days. At *t* = *S*, recall is 90%. Thus the due
date is the last review plus *S* days.

[`recall`](https://vedicreader.github.io/abhyasa/learner.html#recall)
gives the recall now.
[`grow`](https://vedicreader.github.io/abhyasa/learner.html#grow) gives
the new stability after an outcome.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L978"
target="_blank" style="float:right; font-size:smaller">source</a>

### grow

``` python
def grow(
    s:float, r:float, kind:str
)->float:
```

*The new stability after an outcome, from the old stability `s` and the
recall `r` at the time of the answer.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L972"
target="_blank" style="float:right; font-size:smaller">source</a>

### recall

``` python
def recall(
    c, now:float=None
)->float:
```

*The chance, from 0 to 1, that you still recall concept `c` now.*

Recall is 90% after *S* days, and 0 for a concept with no review.

``` python
test_close(recall(dict(stability=2., last=0.), now=2*DAY), .9, eps=1e-6)
test_eq(recall(dict(stability=0, last=None)), 0.)
```

A pass grows stability. A harder recall grows it more. A gap shrinks it.

``` python
test_eq(grow(0, 0, 'solid'), 1.)
assert grow(4, .7, 'solid') > grow(4, .99, 'solid') > 4   # a review that was harder to recall teaches more
assert grow(4, .9, 'gap') < 4
```

### Update

[`update`](https://vedicreader.github.io/abhyasa/learner.html#update)
returns the fields to change after one answer at depth `d`.

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr>
<th>outcome</th>
<th>change</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>solid</code></td>
<td>The floor rises to <code>d</code>. The streak grows.</td>
</tr>
<tr>
<td><code>fragile</code></td>
<td>The floor stays. The concept is <code>shaky</code> at
<code>d</code>, so the next question is at <code>d</code> again.</td>
</tr>
<tr>
<td><code>misconception</code></td>
<td>The ceiling falls to <code>d</code>. The floor falls below
<code>d</code>. The misconception is kept.</td>
</tr>
<tr>
<td><code>gap</code></td>
<td>The ceiling falls to <code>d</code>. The floor falls below
<code>d</code>.</td>
</tr>
</tbody>
</table>

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L986"
target="_blank" style="float:right; font-size:smaller">source</a>

### update

``` python
def update(
    c, depth:int, correct:bool, confidence:int=None, hints:int=0, dont_know:bool=False, misconception:str='',
    now:float=None
)->fastcore.basics.AttrDict:
```

*The state changes for concept `c` after one answer at `depth`, with the
outcome in `kind`.*

A `solid` pass at depth 3 on a new concept raises the floor to 3. The
review is due in one day (the setting `review.solid`).

``` python
c = AttrDict(floor=0, ceiling=6, stability=0., last=None, streak=0, misses=0, seen=0)
u = update(c, 3, True, 80, now=0.)
test_eq((u.kind, u.floor, u.ceiling, u.streak, u.due), ('solid', 3, 6, 1, DAY))
c.update(u)
```

A confident wrong answer at depth 4 lowers the ceiling to 4 and keeps
the misconception.

``` python
u = update(c, 4, False, 85, misconception='thinks RRF needs normalised scores', now=DAY)
test_eq((u.kind, u.floor, u.ceiling, u.misses), ('misconception', 3, 4, 1))
test_eq(u.misconceptions, ['thinks RRF needs normalised scores'])
c.update(u)
```

A miss below the floor lowers the floor. A low-confidence pass makes the
concept `shaky` at that depth.

``` python
u = update(c, 2, False, 10, now=DAY)    # a miss below the floor: the floor falls
test_eq((u.kind, u.floor, u.ceiling), ('gap', 1, 2))
test_eq(update(c, 4, True, 30).shaky, 4)
```

### The next depth

Ask in the middle of the range between floor and ceiling, so one answer
cuts the range in half. Ask a `shaky` concept at the same depth again.
When the ceiling is the floor plus 1, ask at the ceiling, because the
work is at that edge.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1004"
target="_blank" style="float:right; font-size:smaller">source</a>

### next_depth

``` python
def next_depth(
    c
)->int:
```

*The depth for the next question on concept `c`.*

A new concept starts at depth 3 (explain). An open range gives its
middle.

``` python
test_eq(next_depth(dict(floor=0, ceiling=6)), 3)     # a new concept: start at explain, not at the bottom
test_eq(next_depth(dict(floor=3, ceiling=6)), 4)
test_eq(next_depth(dict(floor=0, ceiling=3)), 1)
test_eq(next_depth(dict(floor=1, ceiling=3)), 2)
```

A closed range gives the ceiling. Depth 5 is the top. A `shaky` depth
comes first.

``` python
test_eq(next_depth(dict(floor=2, ceiling=3)), 3)
test_eq(next_depth(dict(floor=5, ceiling=6)), 5)
test_eq(next_depth(dict(floor=1, ceiling=6, shaky=4)), 4)
```

### What to ask next

[`pick`](https://vedicreader.github.io/abhyasa/learner.html#pick)
chooses the concept. Code makes the choice, not the model, so the app
can show the reason.

1.  A `focus` concept, when you chose one and did not learn it yet.
2.  A review that is due, with the lowest recall first.
3.  The frontier: concepts below the goal with all prereqs at depth 3 or
    more. Concepts that you started come first.

After two misses in a row on a concept,
[`pick`](https://vedicreader.github.io/abhyasa/learner.html#pick) goes
back to its weakest prereq that is below the goal.

[`learned`](https://vedicreader.github.io/abhyasa/learner.html#learned)
and [`ready`](https://vedicreader.github.io/abhyasa/learner.html#ready)
test one concept.
[`_back`](https://vedicreader.github.io/abhyasa/learner.html#_back)
finds the prereq to go back to. After a demo, it waits for the demo
ladder (see below).

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1016"
target="_blank" style="float:right; font-size:smaller">source</a>

### ready

``` python
def ready(
    c, by:dict
)->bool:
```

*Whether each prereq of `c` that is a known concept is at depth 3 or
more.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1012"
target="_blank" style="float:right; font-size:smaller">source</a>

### learned

``` python
def learned(
    c, goal:int=None
)->bool:
```

*Whether concept `c` is at depth `goal` or more; the default is the
setting `checks.goal`.*

[`pick`](https://vedicreader.github.io/abhyasa/learner.html#pick)
returns the concept, the depth and the reason.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1035"
target="_blank" style="float:right; font-size:smaller">source</a>

### pick

``` python
def pick(
    concepts, focus:str=None, subject:str=None, goal:int=None, now:float=None, skip:tuple=(), near:tuple=(),
    review:bool=True
):
```

*The next `(concept, depth, why)`, or None when no review is due and the
frontier is empty. Concepts in `near` come first on the frontier.
`review=False` skips the review of due concepts.*

### Strengths and weak spots

[`understanding`](https://vedicreader.github.io/abhyasa/learner.html#understanding)
reads one concept and tells how well you know it: the floor, the
ceiling, the recall now, and the reasons that it is weak. It uses only
the state of the concept. Thus a page of all concepts does not need to
read the attempts.
[`weak`](https://vedicreader.github.io/abhyasa/learner.html#weak) is
true when a reason says that you need help now. `strong` needs depth 3
or more, recall of 80% or more, and no reasons.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1076"
target="_blank" style="float:right; font-size:smaller">source</a>

### weak

``` python
def weak(
    c, now:float=None
)->bool:
```

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1060"
target="_blank" style="float:right; font-size:smaller">source</a>

### understanding

``` python
def understanding(
    c, now:float=None
)->fastcore.basics.AttrDict:
```

*How well you know concept `c`: floor, ceiling, recall, the reasons it
is weak,
[`weak`](https://vedicreader.github.io/abhyasa/learner.html#weak),
`strong`, one `state`, and a score to sort by.*

A concept with a misconception and two misses is weak, with both
reasons. A deep, fresh concept with no misses is strong.

``` python
now = 1e6
u = understanding(dict(slug='a', floor=1, ceiling=3, misses=2, misconceptions=['scores add'], seen=3, stability=5., last=now), now)
test_eq((u.weak, u.strong, u.reasons[:2]), (True, False, ['a misconception: scores add', 'missed the last 2 times']))
s = understanding(dict(slug='b', floor=4, ceiling=6, seen=5, stability=30., last=now - DAY), now)
test_eq((s.weak, s.strong, s.reasons), (False, True, []))
test_eq(understanding(dict(slug='g', floor=0, provenance=[dict(src='gap')]), now).reasons, ['you said you did not know it'])
assert understanding(dict(slug='f', floor=3, seen=4, stability=1., last=now - 30 * DAY), now).reasons[0].startswith('recall is down to')
test_eq([u.state, s.state, understanding(dict(slug='n')).state], ['weak', 'strong', 'unseen'])
```

Three concepts: `b` needs `a`. The first pick is `a`, because `b` waits
for its prereq.

``` python
def C(slug, **kw): return AttrDict(slug=slug, name=slug, **{**dict(floor=0, ceiling=6, seen=0, misses=0, prereqs=[]), **kw})
cs = [C('a', order=0), C('b', prereqs=['a'], order=1), C('c', order=2)]
test_eq(pick(cs)[0].slug, 'a')                          # b waits for a
```

A started concept below the goal comes first. When `a` is learned, `b`
is ready.

``` python
cs[0].update(floor=3, seen=1, due=10*DAY, stability=10., last=0.)
test_eq(pick(cs, now=DAY)[0].slug, 'a')                 # a was started and is below the goal
cs[0].update(floor=4)
test_eq(pick(cs, now=DAY)[0].slug, 'b')                 # a is learned; b is ready
```

A due review comes before the frontier, at the floor of the concept.

``` python
r = pick(cs, now=11*DAY)
test_eq((r[0].slug, r[1]), ('a', 4))                    # a is due: a review at its floor
assert 'due for review' in r[2]
# a due concept with two misses goes back to its prereq; the why still says that it is a review, so the daily review limit counts it
r = pick([dict(slug='p', name='P', prereqs=[], floor=0), dict(slug='q', name='Q', prereqs=['p'], floor=2, seen=3, due=1., misses=2)], now=5*DAY)
test_eq(r[0]['slug'], 'p')
assert r[2].startswith('due for review: two misses on Q')
```

Two misses go back to the weakest prereq. A learned concept that is not
due gives no question.

``` python
cs[1].update(misses=2, seen=2, floor=1, ceiling=2); cs[0].update(floor=3, due=None)
test_eq(pick(cs, focus='b', now=DAY)[0].slug, 'a')      # two misses on b: step back to a
test_eq(pick([C('x', floor=4, seen=1, due=1e12)]), None)
```

After you saw a demo of a concept, `after_demo` on the concept counts
your misses since then (the demo module keeps it). While the demo ladder
runs,
[`_back`](https://vedicreader.github.io/abhyasa/learner.html#_back)
waits: it steps back only after the third miss since the demo. Here, `b`
has 4 misses in all.

The tests below need
[`set_val`](https://vedicreader.github.io/abhyasa/learner.html#set_val).

``` python
by = {c['slug']: c for c in cs}
test_eq([_back({**cs[1], 'misses': 4, 'after_demo': n}, by, 4)[0]['slug'] for n in (0, 1, 2, 3)], ['b', 'b', 'b', 'a'])
assert 'after its demo' in _back({**cs[1], 'misses': 4, 'after_demo': 3}, by, 4)[1]
```

With no demo in play, or with the setting `checks.demo_ladder` off, two
misses step back, as before.

``` python
test_eq(_back({**cs[1], 'misses': 2, 'after_demo': None}, by, 4)[0]['slug'], 'a')
set_val('checks.demo_ladder', False)
try: test_eq(_back({**cs[1], 'misses': 2, 'after_demo': 1}, by, 4)[0]['slug'], 'a')
finally: set_val('checks.demo_ladder', True)
```

A concept in `near` comes first on the frontier, before a concept you
started elsewhere.

``` python
cs = [C('far', order=0, seen=1), C('next', order=5)]
test_eq(pick(cs)[0].slug, 'far')
r = pick(cs, near={'next'})
test_eq(r[0].slug, 'next')
assert 'next to the concept you chose' in r[2]
```

A gap that you named on a concept comes first, at the easiest depth.
After you pass it, or after two questions on it, the concept comes back.

``` python
nn = dict(slug='neuron', name='Neuron', floor=0, ceiling=6, gaps=['wsum'])
ws = dict(slug='wsum', name='Weighted sum', floor=0, ceiling=6, seen=0)
c, d, why = pick([nn, ws], focus='neuron')
test_eq((c['slug'], d), ('wsum', 1)); assert 'you said you did not know Weighted sum' in why
test_eq(pick([nn, dict(ws, floor=1, seen=1)], focus='neuron')[0]['slug'], 'neuron')
test_eq(pick([nn, dict(ws, seen=2)], focus='neuron')[0]['slug'], 'neuron')
```

## Ways to learn

The ways you learn: six text ways (code, maths, analogies, worked
examples, diagrams and plain words), and demos, ordering, playgrounds,
boards and timelines. This module finds which ways work for you, and
when to try a new one.

Each question, hint and lesson uses one way to teach: its `method`. The
tutor picks the method before it writes the prompt. So the tag is known,
not guessed. When a question closes, its outcome counts for its method:
solid 1, shaky 0.5, a miss 0.

The counts give each method a Beta posterior, for each subject and over
all subjects.
[`choose`](https://vedicreader.github.io/abhyasa/learner.html#choose)
uses them in two ways:

- Drill: a weak concept gets your best method.
- Explore: a concept that is not weak sometimes gets another method. You
  set the chance (`explore` in the profile). First
  [`choose`](https://vedicreader.github.io/abhyasa/learner.html#choose)
  tries a method that got better recently. Else it uses a random draw (a
  Thompson sample).

The counts are in one `stats` document on the shelf. This document is
only a cache: `method_stats` builds it again from the attempts.

### The methods

`METHODS` maps each method to the line that the prompt gets.
`EXAMPLES_TO` maps the profile’s “examples that help” to methods, to set
the prior.

The text of each method is a prompt that you can change in Settings (see
`prompts`).
[`way`](https://vedicreader.github.io/abhyasa/learner.html#way) gives
the text in use: your saved text, else the line in `METHODS`.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1103"
target="_blank" style="float:right; font-size:smaller">source</a>

### way

``` python
def way(
    m:str
)->str:
```

*The text of method `m` now: your saved text, else the line in
`METHODS`; ’’ for an unknown method.*

With no saved text,
[`way`](https://vedicreader.github.io/abhyasa/learner.html#way) gives
the line in `METHODS`.

``` python
test_eq([way(m) for m in METHODS], list(METHODS.values()))
test_eq(way('nope'), '')
```

### The stats document

`stats` reads the stats document. It gives empty counts when there is no
document. `set_stats` writes the document. A lock makes sure that two
closes at the same time do not lose a count.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1117"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.set_stats

``` python
def set_stats(
    **kv
):
```

*Change fields of the stats document.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1111"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.stats

``` python
def stats()->fastcore.basics.AttrDict:
```

*The cached stats: method counts for each subject (`*` is all), recent
credits, and calibration.*

### Credit

[`credit`](https://vedicreader.github.io/abhyasa/learner.html#credit)
adds one outcome to the counts and returns the new stats. It keeps the
last 8 credits of each method, to find a method that gets better.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1135"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.credit_method

``` python
def credit_method(
    method:str, subject:str, kind:str, w:float=1.0
):
```

*Count outcome `kind` for `method` with the weight `w` in the stats
document.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1123"
target="_blank" style="float:right; font-size:smaller">source</a>

### credit

``` python
def credit(
    st, method:str, subject:str, kind:str, w:float=1.0
)->dict:
```

*The stats `st` with one outcome `kind` added for `method` with the
weight `w`, in `subject` and in `*`. Only a full outcome (`w=1`) goes in
the recent credits.*

Solid adds 1 to `a`. A miss adds 1 to `b`. Shaky adds half to each. An
unknown outcome or method changes nothing.

``` python
st = credit(credit(credit(EMPTY, 'code', 'ml', 'solid'), 'code', 'ml', 'fragile'), 'maths', 'ml', 'gap')
test_eq(st['methods']['ml'], {'code': [1.5, .5], 'maths': [0., 1.]})
test_eq(st['methods']['*'], st['methods']['ml'])
test_eq(st['recent'], {'code': [1., .5], 'maths': [0.]})
test_eq(credit(st, 'code', 'ml', 'closed'), st)
```

#### Feedback counts a little

The row under a demo or a lesson also counts for the way of that demo or
lesson, with a small weight from `FEEDBACK`. “Not clear” adds 0.25 of a
miss, so four of them count as one miss, and one click does not decide
the way. “Just right” adds 0.1 of a pass. “Too simple” adds nothing: the
way worked, but the step was too small. A part of an outcome does not go
in the recent credits, so it does not make a way look better lately.

``` python
st = credit(EMPTY, 'analogy', 'ml', *FEEDBACK['unclear'])
test_eq(st['methods']['ml']['analogy'], [0., .25])
test_eq(st['recent'], {})
test_eq(credit(st, 'analogy', 'ml', *FEEDBACK['right'])['methods']['ml']['analogy'], [.1, .25])
```

### The posterior

[`posterior`](https://vedicreader.github.io/abhyasa/learner.html#posterior)
gives `(a, b)` for a method: Beta(1, 1), plus 2 on `a` for a method in
the profile’s examples. For a subject, it adds the subject counts and
the `*` counts. It scales the `*` counts down to at most 4
pseudo-counts. So a new subject starts from what works for you in other
subjects.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1152"
target="_blank" style="float:right; font-size:smaller">source</a>

### improving

``` python
def improving(
    st, m:str
)->tuple | None:
```

*`(right, n)` over the last 5 credits of `m`, when they are at least 0.2
above its mean over all; else None.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1150"
target="_blank" style="float:right; font-size:smaller">source</a>

### mean

``` python
def mean(
    ab
)->float:
```

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1140"
target="_blank" style="float:right; font-size:smaller">source</a>

### posterior

``` python
def posterior(
    st, m:str, subject:str='', prior:tuple=()
)->tuple:
```

*The Beta `(a, b)` of method `m` in `subject`.*

With no counts, the prior decides.
[`improving`](https://vedicreader.github.io/abhyasa/learner.html#improving)
finds a method that got better recently.

``` python
test_eq(posterior(EMPTY, 'code'), (1., 1.))
test_eq(posterior(EMPTY, 'code', prior=['code']), (3., 1.))
gm = dict(methods={'*': {'maths': [2., 4.]}}, recent={'maths': [0., 0., 1., 1., 1., 1.]})
test_eq(improving(gm, 'maths'), (4, 5))
test_is(improving(dict(gm, recent={'maths': [1., 0., 0., 0.]}), 'maths'), None)
```

### Choose a method

[`choose`](https://vedicreader.github.io/abhyasa/learner.html#choose)
returns `(method, note)`. The note goes in the line that says why you
get this question.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1166"
target="_blank" style="float:right; font-size:smaller">source</a>

### choose

``` python
def choose(
    st, subject:str='', weak:bool=False, rng:NoneType=None, explore:str='sometimes', prior:tuple=(),
    allowed:NoneType=None
)->tuple:
```

*The method for the next question, and a note on why, from the `allowed`
ways (default `TEXT_WAYS`). A weak concept gets the best method; else,
sometimes, another one.*

A weak concept gets the best method. With `explore='never'`,
[`choose`](https://vedicreader.github.io/abhyasa/learner.html#choose)
always gives the best method. With `sometimes`, it tries a method that
got better at times. It still gives the best method most of the time.

``` python
cs = dict(methods={'*': {'code': [7., 2.], 'maths': [2., 4.]}}, recent={'maths': [0., 0., 1., 1., 1., 1.]})
m, note = choose(cs, 'ml', weak=True)
test_eq(m, 'code'); assert note.startswith('drill with code: 7 of 9')
test_eq({choose(cs, 'ml', rng=random.Random(i), explore='never')[0] for i in range(30)}, {'code'})
picks = [choose(cs, 'ml', rng=random.Random(i)) for i in range(60)]
assert any(n.startswith('trying maths: you got better') for _, n in picks)
test_eq(sum(m == 'code' for m, _ in picks) > 30, True)   # with `sometimes`, the best method still comes most of the time
```

### The ways that fit

A session cannot show every way.
[`choose`](https://vedicreader.github.io/abhyasa/learner.html#choose)
takes `allowed`: the ways it may pick from. The default is `TEXT_WAYS`,
the ways that every session can show. A list with no known way gives
`TEXT_WAYS` too.

``` python
rng = random.Random(0)
assert {'demo', 'order', 'playground', 'board', 'timeline'} <= set(METHODS) and 'way.timeline' in PROMPTS
assert all(choose(EMPTY, rng=rng)[0] in TEXT_WAYS for _ in range(200))          # a session never gets a way it cannot show
assert all(choose(EMPTY, rng=rng, allowed=['demo', 'maths'])[0] in ('demo', 'maths') for _ in range(200))
assert choose(EMPTY, rng=rng, allowed=['board'])[0] in METHODS                   # an allowed list keeps its own ways
assert choose(EMPTY, rng=rng, allowed=['nope'])[0] in TEXT_WAYS                  # no known way: back to the text ways
```

### The way for a check

[`check_for`](https://vedicreader.github.io/abhyasa/learner.html#check_for)
picks the way for one check of one idea. It looks only at the ways of
the idea that a walk can show. It does not repeat the way of the last
check when it has another. The kind of the check follows the way, and
the depth follows the kind. A short check is depth 3 or more: it asks
for the idea and why it holds, never for a fact to recall. It is depth 4
when the floor is 3 or more.

A hands-on way of the idea (a key of `CHECK_KIND`: demo, order or code)
comes before its text ways. The model lists a hands-on way only for an
idea that fits it: a process, a sequence, or code. A code check is a
notebook that runs in Leela. Without this rule, the learner stats and
the profile pick a text way almost every time, because the profile names
only text ways. Then nearly every check is a short answer.

Only the idea’s own ways can give a hands-on check. An idea with no way
that a walk can show gets a short answer in a text way. A walk passes
`hands=False` when it has its share of hands-on checks (see
[`hands_most`](https://vedicreader.github.io/abhyasa/path.html#hands_most)
in `journey`).

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1188"
target="_blank" style="float:right; font-size:smaller">source</a>

### check_for

``` python
def check_for(
    c, st, rng:NoneType=None, explore:str='sometimes', prior:tuple=(), last:str='', hands:bool=True
)->fastcore.basics.AttrDict:
```

*The way, kind, depth and reason for one check of concept `c`. A
hands-on way of the idea comes before its text ways; `hands=False`
leaves the hands-on ways out.*

A check takes a way of its idea. An idea with no way that a walk can
show gets a text way.

``` python
c = AttrDict(ways=['code', 'maths'], floor=0)
k = check_for(c, EMPTY, rng=random.Random(1))
assert (k.way, k.kind, k.depth) in {('code', 'notebook', 4), ('maths', 'short', 3)} and k.why
assert check_for(AttrDict(ways=['demo'], floor=0), EMPTY).kind == 'demo'
assert check_for(AttrDict(ways=['timeline'], floor=3), EMPTY).way in TEXT_WAYS   # no timeline view yet
assert check_for(AttrDict(ways=['words'], floor=0), EMPTY).depth == 3   # a new idea: say the idea and why, not a fact
assert check_for(AttrDict(ways=['words'], floor=3), EMPTY).depth == 4
assert all(check_for(AttrDict(ways=['demo', 'code'], floor=0), EMPTY, rng=rng, last='demo').way == 'code' for _ in range(20))
```

An idea that lists a hands-on way (demo, order or code) gets a check of
that kind. This is true when a text way comes first in the list, and
when the profile likes a text way. An idea with only text ways still
gets a short answer.

``` python
assert all(check_for(AttrDict(ways=['words', 'example', 'order'], floor=0), EMPTY, random.Random(k), prior=['example']).kind == 'order' for k in range(50))
assert all(check_for(AttrDict(ways=['maths', 'demo'], floor=0), EMPTY, random.Random(k)).kind == 'demo' for k in range(50))
assert all(check_for(AttrDict(ways=['words', 'analogy'], floor=0), EMPTY, random.Random(k)).kind == 'short' for k in range(50))
```

An idea with no ways, or with no way that a walk can show, gets a short
answer, never a code check. With `hands=False` (the walk has its share
of hands-on checks), an idea gets a short answer in one of its text
ways, else in any text way.

``` python
assert all(check_for(AttrDict(ways=w, floor=0), EMPTY, random.Random(k)).kind == 'short' for w in ([], None, ['timeline']) for k in range(50))
assert all(check_for(AttrDict(ways=['code', 'maths'], floor=0), EMPTY, random.Random(k), hands=False).way == 'maths' for k in range(20))
assert all(check_for(AttrDict(ways=['demo'], floor=0), EMPTY, random.Random(k), hands=False).kind == 'short' for k in range(20))
```

### In plain words, and from the attempts

[`methods_text`](https://vedicreader.github.io/abhyasa/learner.html#methods_text)
says what works, for a prompt and for the page. `method_stats` builds
the counts again from the attempts, in order.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1209"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.method_stats

``` python
def method_stats(
    n:int=2000
)->dict:
```

*Count every final attempt with a method again, oldest first, with the
feedback on each attempt, and save the counts. Return the stats.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1200"
target="_blank" style="float:right; font-size:smaller">source</a>

### methods_text

``` python
def methods_text(
    st, subject:str='', n:int=3
)->str:
```

*The methods with counts, best first, as one line; empty when nothing is
counted.*

The counts that `credit_method` keeps match a rebuild from the attempts.

``` python
ml_ = temp_learning(); ml_.put('Ranking', 'Order items by a score.', 'ml')
for m, k in (('code', 'solid'), ('code', 'fragile'), ('maths', 'gap')):
    ml_.record('ranking', f'{m} {k}?', final=True, method=m, outcome=k); ml_.credit_method(m, 'ml', k); time.sleep(.01)
inc = ml_.stats()
test_eq(ml_.method_stats()['methods'], inc.methods)
assert methods_text(inc, 'ml').startswith('Ways that work for you: code 2 of 2')
```

## The profile

What abhyasa knows about you: your background, goals, interests, the
examples that help you, your pace, and what confuses you.

The tutor asks better questions when it knows the learner. A coder gets
code examples. A Sanskrit reader gets a Sanskrit example. This module
keeps one `profile` document on the learning shelf. The profile gets
data in three ways: a short first conversation, edits by the learner,
and changes that the tutor makes from evidence. The module keeps each
change with who made it and why. So the learner can see each change and
undo it. The profile stays in the vault on this machine.

### The fields

A field is a list of short items, or one value. `PACES` names the three
paces. `EXAMPLES` names the kinds of example a learner can like.

[`profile_text`](https://vedicreader.github.io/abhyasa/learner.html#profile_text)
gives the profile as lines for a prompt. It is empty when the profile
has no data. So the prompt for a new learner does not change.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1232"
target="_blank" style="float:right; font-size:smaller">source</a>

### profile_text

``` python
def profile_text(
    p
)->str:
```

*The profile as prompt lines; empty when nothing is known.*

[`clean`](https://vedicreader.github.io/abhyasa/learner.html#clean)
makes a value fit its field. A list field takes a list or lines of text.
The pace must be a known pace. The session length stays between 3 and
40.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1240"
target="_blank" style="float:right; font-size:smaller">source</a>

### clean

``` python
def clean(
    k, v
):
```

*The value `v` made to fit field `k`.*

### The document

`profile` reads the document. It uses the defaults for the fields that
are not set. `known` is False until the profile has a saved value.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1252"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.profile

``` python
def profile()->fastcore.basics.AttrDict:
```

*The learner profile, with its list of changes, newest first.*

`set_profile` changes fields. It keeps each change with the old value,
the new value, who made it (`you`, `onboarding` or
[`tutor`](https://vedicreader.github.io/abhyasa/tutor.html#tutor)) and
why.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1260"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.set_profile

``` python
def set_profile(
    by:str='you', why:str='', **kv
)->list:
```

*Change fields of the profile and keep each change with who made it and
why. Return the changes.*

`undo_profile` puts back the old value of one change. It finds the
change by its id. Two changes in one save can have the same time, so the
time can not tell them apart. A change from before the ids has none, so
for it the exact time is used. The undo is a change too, so the list
shows all the changes.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1284"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.clear_profile

``` python
def clear_profile():
```

*Remove the profile and its changes.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1274"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.undo_profile

``` python
def undo_profile(
    ref
)->list:
```

*Put back the old value of the change `ref`: its id, or for an older
change with no id, its exact time `at`.*

### The first conversation

The learner answers four questions, by voice or by text. One model call
turns the answers into fields.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1304"
target="_blank" style="float:right; font-size:smaller">source</a>

### onboard

``` python
def onboard(
    lr:__main__.Learning, ask, answers:dict
)->list:
```

*Fill the profile from the onboarding answers. Return the changes.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1297"
target="_blank" style="float:right; font-size:smaller">source</a>

### onboard_prompt

``` python
def onboard_prompt(
    answers:dict
)->str:
```

*The prompt that turns the onboarding answers into profile fields.*

### The tutor learns from evidence

[`observe`](https://vedicreader.github.io/abhyasa/learner.html#observe)
reads the recent answers: right or not, confidence, hints,
misconceptions and think-aloud. A small model call proposes at most
three changes, each with its reason.
[`observe`](https://vedicreader.github.io/abhyasa/learner.html#observe)
applies them immediately and keeps each one with its reason. So the
learner sees them and can undo them.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1333"
target="_blank" style="float:right; font-size:smaller">source</a>

### observe_prompt

``` python
def observe_prompt(
    p, ev:str, st:NoneType=None
)->str:
```

*The prompt that proposes changes to profile `p` from the evidence `ev`
and the method counts in the stats `st`.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1317"
target="_blank" style="float:right; font-size:smaller">source</a>

### evidence

``` python
def evidence(
    lr:__main__.Learning, n:int=12
)->str:
```

*The last `n` final answers as short lines for a prompt: the outcome,
the method, the first error, gaps you named, lessons you asked for, and
what you said.*

[`observe`](https://vedicreader.github.io/abhyasa/learner.html#observe)
applies the changes. A list field takes
[`add`](https://vedicreader.github.io/abhyasa/learner.html#add) and
[`remove`](https://vedicreader.github.io/abhyasa/models.html#remove).
The pace takes `value`. The goals and the notes are for the learner
only, so
[`observe`](https://vedicreader.github.io/abhyasa/learner.html#observe)
drops a change to them.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1341"
target="_blank" style="float:right; font-size:smaller">source</a>

### observe

``` python
def observe(
    lr:__main__.Learning, ask, n:int=12
)->list:
```

*Change the profile from the recent answers. Return the changes, each
with its reason.*

### Who the words are for

Each text that explains an idea is for a human, and humans differ. One
learner is new to the idea, and another knows it well. A text that only
says the definition again teaches little to both. So each teaching
prompt gets two more parts:

- `teach.human`, one shared prompt with the rules on how to teach. It
  comes from the research on how people learn: adapt to the level (the
  expertise reversal effect), a concrete case before the abstract rule
  (concreteness fading), worked steps for a new learner, an analogy with
  its limits, the reason why, and the common wrong idea.
- [`level_line`](https://vedicreader.github.io/abhyasa/learner.html#level_line),
  one short line on how well this learner knows the idea now. It comes
  from the learner model: the state of the idea
  ([`understanding`](https://vedicreader.github.io/abhyasa/learner.html#understanding)),
  the reasons it is weak, the needs that are not in place, and the
  profile.

The rules say what to do for each level. Shaky, a gap or a missing need:
keep it simple, one idea at a time, a concrete case first, step by step.
Solid: be short, with the exact terms and fewer worked steps.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1370"
target="_blank" style="float:right; font-size:smaller">source</a>

### teach_sp

``` python
def teach_sp(
    name:str
)->str:
```

*The prompt `name` with the shared rules on how to teach (`teach.human`)
after it.*

[`level_line`](https://vedicreader.github.io/abhyasa/learner.html#level_line)
turns the learner model into one short line. A need is a prereq or a gap
that is not at depth 3 yet; a walk can give its own list in `missing`.
The profile adds the background, the examples that help, the pace and
what confuses them.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1395"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.level_of

``` python
def level_of(
    c, missing:NoneType=None, profile:bool=True
)->str:
```

*The level line of concept `c` (a slug or a dict) for this learner, with
the needs on the shelf and, unless `profile=False`, the profile. A
question takes no profile: it uses the real terms of the subject.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1378"
target="_blank" style="float:right; font-size:smaller">source</a>

### level_line

``` python
def level_line(
    c, by:dict=None, p:NoneType=None, missing:NoneType=None
)->str:
```

*One short line on how well the learner knows concept `c` now, for a
teaching prompt: the state and why, the needs that are not in place, the
profile, and what to do. ’’ with no concept.*

A new idea with no profile asks for the simple form.

``` python
test_eq(level_line(dict(name='Dot product')), "The learner's level on this idea: new to this idea. So: keep it simple.")
test_eq(level_line(None), '')
```

A solid idea asks for the short form, with its depth.

``` python
import time as _t
solid = dict(name='Dot product', floor=4, seen=5, stability=30, last=_t.time())
test_eq(level_line(solid), "The learner's level on this idea: solid, at the apply level. So: be short.")
```

A need that is not in place makes it simple, even for an idea that is
not weak. The profile adds its lines.

``` python
by = dict(dot=dict(slug='dot', name='Dot product', floor=1))
l = level_line(dict(name='Attention', prereqs=['dot'], floor=3, seen=2, stability=30, last=_t.time()), by, dict(background=['writes Python'], pace='gentle'))
assert 'Needs that are not in place yet: Dot product.' in l and 'Background: writes Python.' in l and l.endswith('So: keep it simple.')
```

A gap that the learner named, and an idea with misses, say why they are
weak.

``` python
assert 'a gap' in level_line(dict(name='Softmax', provenance=[dict(src='gap')]))
assert 'shaky (missed the last 2 times' in level_line(dict(name='Softmax', floor=1, seen=3, misses=2))
```

[`teach_sp`](https://vedicreader.github.io/abhyasa/learner.html#teach_sp)
puts the shared rules after a prompt.

``` python
assert teach_sp('profile.observe').endswith(prompt('teach.human')) and 'Never only say the definition again' in HUMAN
```

[`plainly`](https://vedicreader.github.io/abhyasa/learner.html#plainly)
makes a text that the tutor wrote plain. It is rupaka’s `plain` when the
setting `checks.readable` is on: a text that fails rupaka’s `plain_why`
is written again once, and the new text stays only when it is better.
Each place that shows a model text to the learner uses it.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1403"
target="_blank" style="float:right; font-size:smaller">source</a>

### plainly

``` python
def plainly(
    ask, x, terms:tuple=(), keep:tuple=()
):
```

*`x` (a text, or a dict of texts) through rupaka’s `plain` with the
model `ask`, when `checks.readable` is on; else `x`.*

A plain text makes no model call. A hard text makes one call, and the
plainer text stays. With the setting off, nothing changes.

``` python
calls = []
def fake_ask(p, sp=''): calls.append(p); return 'In a DDPM, the amount of noise at each step is set before training. Which DDPM rule breaks when the network learns it?'
hard = 'You ask the network to learn the noise recipe as it trains. Why does this depart from a DDPM simplifying assumption?'
test_eq(plainly(fake_ask, 'The cat sat.'), 'The cat sat.'); test_eq(calls, [])
assert plainly(fake_ask, hard).startswith('In a DDPM') and len(calls) == 1
set_val('checks.readable', False)
try: test_eq(plainly(fake_ask, hard), hard)
finally: set_val('checks.readable', True)
```

### How well you know what you know

A good learner knows what they know. When you feel sure and you are
often wrong, you stop checking your own thinking. When you feel unsure
and you are often right, you hold back ideas that work. The “Sure?”
choice on each answer gives the data.
[`calibration`](https://vedicreader.github.io/abhyasa/learner.html#calibration)
counts the first try of each question. It counts how many times you felt
sure (60% or more), and how many of those answers were right. It counts
the same for unsure. An answer with no confidence, a “don’t know” and a
demo do not count.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1408"
target="_blank" style="float:right; font-size:smaller">source</a>

### calibration

``` python
def calibration(
    lr:__main__.Learning, n:int=300
)->fastcore.basics.AttrDict:
```

*How often the first try of a question was right, when you felt sure and
when you felt unsure.*

[`calibration_text`](https://vedicreader.github.io/abhyasa/learner.html#calibration_text)
gives the calibration in plain words, with one line about what it means.
With fewer than five answers, it gives an empty text.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1421"
target="_blank" style="float:right; font-size:smaller">source</a>

### calibration_text

``` python
def calibration_text(
    r
)->str:
```

*The calibration `r` in plain words; empty with fewer than five
answers.*

### Tests

A new shelf has an empty profile, and its prompt text is empty.

``` python
from abhyasa.models import Fake
```

``` python
lr = temp_learning()
p = lr.profile()
test_eq((p.known, p.pace, p.background, profile_text(p)), (False, 'steady', [], ''))
```

`set_profile` keeps each change, with who made it. Lines of text become
a list.

``` python
ch = lr.set_profile(background='codes in Python\nknows Sanskrit grammar', pace='gentle')
test_eq([c['field'] for c in ch], ['background', 'pace'])
p = lr.profile()
test_eq((p.known, p.background, p.changes[0]['by']), (True, ['codes in Python', 'knows Sanskrit grammar'], 'you'))
assert 'Background: codes in Python; knows Sanskrit grammar' in profile_text(p) and 'Pace: gentle' in profile_text(p)
```

When a value stays the same, `set_profile` makes no change. A bad pace
becomes `steady`, and the session length stays in its range.

``` python
test_eq(lr.set_profile(pace='gentle'), [])
test_eq((clean('pace', 'warp'), clean('session_len', '200'), clean('session_len', 'x')), ('steady', 40, 12))
```

`undo_profile` puts back the old value, and keeps the undo as a change.

``` python
cid = [c for c in lr.profile().changes if c['field'] == 'pace'][0]['id']
lr.undo_profile(cid)
test_eq((lr.profile().pace, lr.profile().changes[0]['why']), ('steady', 'you undid a change'))
```

Two changes in one save can have the same time. Each change has its own
id, so the undo changes only the one that you pick.

``` python
import time as _t
```

``` python
real = _t.time
_t.time = lambda: 1000.0   # every change of this save gets the same time
try: lr.set_profile(pace='fast', session_len=20)
finally: _t.time = real
two = lr.profile().changes[:2]
test_eq(two[0]['at'], two[1]['at'])
lr.undo_profile([c for c in two if c['field'] == 'pace'][0]['id'])
test_eq((lr.profile().pace, lr.profile().session_len), ('steady', 20))
```

[`onboard`](https://vedicreader.github.io/abhyasa/learner.html#onboard)
fills fields from the answers with one model call.

``` python
fake = Fake({'make a short learner profile': dict(goals=['understand LLM papers'], examples=['code', 'maths'], session_len=8, pace='steady', notes='')})
ch = onboard(lr, fake, dict(want='LLMs, to read papers', how='code please'))
p = lr.profile()
test_eq((p.goals, p.examples, p.session_len), (['understand LLM papers'], ['code', 'maths'], 8))
test_eq({c['by'] for c in ch}, {'onboarding'})
```

[`observe`](https://vedicreader.github.io/abhyasa/learner.html#observe)
changes the profile from attempts and keeps the reason. It drops a
change to a field that it must not change.

``` python
lr.put('Attention', 'Weights from query-key dot products.', 'ml')
lr.record('attention', 'What shape is QK^T?', answer='d by d', correct=False, confidence=80, depth=3, misconception='mixes up the axes', final=True)
fake.rules['You keep a short profile'] = dict(changes=[dict(field='confuses', add='tensor shapes in attention', why='You missed a shape question with high confidence.'),
                                                     dict(field='goals', add='hack the planet', why='no')])
ch = observe(lr, fake)
test_eq([c['field'] for c in ch], ['confuses'])
test_eq((lr.profile().confuses, lr.profile().changes[0]['by']), (['tensor shapes in attention'], 'tutor'))
assert 'misconception: mixes up the axes' in fake.calls[-1][0]
```

With no answers there is no evidence, and no model call.

``` python
n = len(fake.calls)
test_eq(observe(temp_learning(), fake), [])
test_eq(len(fake.calls), n)
```

[`calibration`](https://vedicreader.github.io/abhyasa/learner.html#calibration)
counts the first try of each question. A pass after a wrong try counts
as wrong. A “don’t know” and an answer with no confidence do not count.

``` python
kl = temp_learning(); kl.put('Attention', 'Weights from query-key dot products.', 'ml')
def rec(q, ok, conf, **kw): kl.record('attention', q, correct=ok, confidence=conf, session='s1', **kw)
rec('q1', False, 90, final=False); rec('q1', True, 90, final=True)   # sure, but wrong on the first try
rec('q2', True, 90); rec('q3', True, 30); rec('q4', False, 10)
rec('q5', False, None, dont_know=True); rec('q6', True, None)
test_eq(calibration(kl), dict(sure=2, sure_right=1, unsure=2, unsure_right=1))
test_eq(calibration_text(calibration(kl)), '')                       # four answers: too few to say
```

With enough answers, the text gives the counts and one line about what
they mean.

``` python
test_eq(calibration_text(AttrDict(sure=4, sure_right=1, unsure=1, unsure_right=0)),
        'When you felt sure, you were right in 1 of 4. When you felt unsure, you were right in 0 of 1. '
        'You are often sure and wrong. Before you answer, ask: how could this be wrong?')
assert calibration_text(AttrDict(sure=1, sure_right=1, unsure=4, unsure_right=4)).endswith('Trust your first idea a little more.')
```

[`evidence`](https://vedicreader.github.io/abhyasa/learner.html#evidence)
reads the full conversation of a question. It gets the first error from
a nudge, and the ideas that you said you did not know. It also gets a
lesson that you asked for, the method, and each thing you said.

``` python
el = temp_learning(); el.put('Neuron', 'z = w . x + b.', 'ml')
th = [dict(who='you', text="I don't know what the loop means"), dict(who='tutor', kind='nudge', text='A loop adds products.', first_error='You use squares.'),
      dict(who='you', text='2(w1x1 + w2x2 + b)')]
el.record('neuron', 'Why does z double?', answer='2z', final=True, outcome='fragile', method='maths', thread=th, gaps=['weighted sum as a loop'], taught=1, teach_method='code')
ev = evidence(el)
for s in ('taught with maths', 'first error: You use squares.', 'said they did not know: weighted sum as a loop', 'asked for a lesson after it (code)', "I don't know what the loop means / 2(w1x1"):
    assert s in ev, s
```

[`observe`](https://vedicreader.github.io/abhyasa/learner.html#observe)
keeps the calibration in the stats document. So each new question can
read it, and does not have to read all attempts.

``` python
observe(el, Fake({'keep a short profile': dict(changes=[])}))
test_eq(set(el.stats().calib), {'sure', 'sure_right', 'unsure', 'unsure_right'})
test_eq((clean('explore', 'often'), clean('explore', 'always')), ('often', 'sometimes'))
```

`clear_profile` removes the full profile and its list of changes.

``` python
lr.clear_profile()
test_eq((lr.profile().known, lr.profile().changes), (False, []))
```

## Background jobs

One stored row for each background job: what runs, what ended, and where
to see it. The activity chip, the event stream and the Mac app read
these rows.

abhyasa does slow work in the background: it looks for sources, makes a
walk from a link or a goal, files a folder, plans a course, and writes
the notebook of a step. Each such job has one row here. A row has a
`kind`, a `state` (`running`, `done`, `failed`, `stopped` or `closed`),
a [`said`](https://vedicreader.github.io/abhyasa/show.html#said) line,
the `start` and `end` times, a `title`, and a `url`: the page of its
object.

The rows are in vishalakshi’s job `Ledger` (`Vault.ledger`), a table in
the vault file, so they survive a restart. This module is thin glue over
it, with the names that the blocks use. The pool future of a job stays
in memory; the row is the truth for the pages. At a start, each row that
still says `running` becomes `closed`: no job lives across a restart.

Each running job has Stop. Stop asks the job to end at its next safe
point, and the job ends as “You stopped it.” A job that keeps its inputs
on its row (`again`) has Do it again when it ended: the block that runs
it starts the same job with the same inputs.

The rows live in the vault file, next to the concepts and the walks.

### The row

`ENDED` are the states of a job that does not run. `FRESH` is how long
an ended job shows in the activity chip, until you open its page. `KEEP`
is how long an ended row stays at all: the ledger deletes it at the next
start of a job.

abhyasa 0.0.15 kept all the rows in the meta of one document
(`abhyasa:jobs:all`). The first read of the ledger moves those rows to
it one time, and forgets the document. A row that said `running` is
`closed`, as its job did not live across the restart. A row that abhyasa
cannot read is left out: an old row never stops the app.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1468"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.job

``` python
def job(
    key:str
)->fastcore.basics.AttrDict | None:
```

*The job row `key`, or None.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1463"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.jobs

``` python
def jobs()->dict:
```

*Each job row by its key, as an `AttrDict` with its `key`.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1453"
target="_blank" style="float:right; font-size:smaller">source</a>

### ledger

``` python
def ledger(
    lr:__main__.Learning
):
```

*The job ledger of the vault of `lr` (`Vault.ledger`). The first read
moves the rows of abhyasa 0.0.15 to it.*

A change merges into one row. The ledger counts each change in
`version`, also a change from another process.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1478"
target="_blank" style="float:right; font-size:smaller">source</a>

### job_version

``` python
def job_version(
    lr:__main__.Learning
)->int:
```

*A count that goes up at each change of a row.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1474"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.put_job

``` python
def put_job(
    key:str, **kv
)->fastcore.basics.AttrDict:
```

*Merge `kv` into the job row `key` and save it.*

### Start, end, and a pool job

[`job_start`](https://vedicreader.github.io/abhyasa/learner.html#job_start)
writes a running row. `obj` is the object of the job, such as
`walk:<id>` or `course:<id>`; its page marks the end as seen. `quiet`
keeps a job out of the chip, such as the research of a side walk of a
talk.
[`job_end`](https://vedicreader.github.io/abhyasa/learner.html#job_end)
closes it. `again` keeps the inputs that start the same job again, for
Do it again. A new start clears the Stop of the last run.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1487"
target="_blank" style="float:right; font-size:smaller">source</a>

### job_end

``` python
def job_end(
    lr:__main__.Learning, key:str, state:str='done', why:str='', **kv
)->fastcore.basics.AttrDict | None:
```

*Note that the job `key` ended now in `state`, with `why` for a failure.
`kv` can change its
[`said`](https://vedicreader.github.io/abhyasa/show.html#said), `url` or
`obj`. None when there is no row.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1483"
target="_blank" style="float:right; font-size:smaller">source</a>

### job_start

``` python
def job_start(
    lr:__main__.Learning, key:str, kind:str, title:str='', said:str='', url:str='', obj:str='', quiet:bool=False,
    again:dict=None
)->fastcore.basics.AttrDict:
```

*Note that the job `key` of `kind` runs now, with its `title`, its
[`said`](https://vedicreader.github.io/abhyasa/show.html#said) line, the
`url` of its page, its object `obj` and the inputs `again` for Do it
again. Ended rows older than `KEEP` go.*

### Stop and Do it again

vishalakshi’s ledger has the state `stopped`, but no way to ask a job to
stop. So abhyasa keeps the ask on the row:
[`stop_job`](https://vedicreader.github.io/abhyasa/learner.html#stop_job)
writes `halt`. The job looks at it at its safe points
([`check_stop`](https://vedicreader.github.io/abhyasa/learner.html#check_stop)),
such as between two model calls or two pages. A model call that runs
when you press Stop can end, but the job uses no result of it: it ends
as `stopped`, with the line `STOPPED`.
[`JobStopped`](https://vedicreader.github.io/abhyasa/learner.html#jobstopped)
is a `BaseException`, so an `except Exception` in the job does not catch
it.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1507"
target="_blank" style="float:right; font-size:smaller">source</a>

### check_stop

``` python
def check_stop(
    lr:__main__.Learning, key:str
):
```

*A safe point of the job `key`: after Stop, its row ends as `stopped`
and
[`JobStopped`](https://vedicreader.github.io/abhyasa/learner.html#jobstopped)
ends the job.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1503"
target="_blank" style="float:right; font-size:smaller">source</a>

### halted

``` python
def halted(
    lr:__main__.Learning, key:str
)->bool:
```

*Whether the job `key` runs and you asked it to stop.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1497"
target="_blank" style="float:right; font-size:smaller">source</a>

### stop_job

``` python
def stop_job(
    lr:__main__.Learning, key:str
)->bool:
```

*Ask the running job `key` to end at its next safe point: its row gets
`halt`. Whether it runs.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1494"
target="_blank" style="float:right; font-size:smaller">source</a>

### JobStopped

``` python
def JobStopped(
    *args, **kwargs
):
```

*Ends a job at a safe point after you pressed Stop.*

Do it again needs the inputs of the job. A row with no `again` has no
way to run again, so it gets no button.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1514"
target="_blank" style="float:right; font-size:smaller">source</a>

### can_again

``` python
def can_again(
    r
)->bool:
```

*Whether the job row `r` ended and kept the inputs (`again`) that start
it again.*

[`track`](https://vedicreader.github.io/abhyasa/learner.html#track)
gives a pool future its row: running now, then `done` or `failed` when
it ends. `ok` maps the result to more fields of the row, such as the
link of the walk that the job made, or `state='failed'` for a job that
ended with nothing. `fut` can also be a function that submits the job:
[`track`](https://vedicreader.github.io/abhyasa/learner.html#track)
calls it after the row starts, so a fast job never writes to a row that
is not there yet. A row that ended before its future, such as a job that
you stopped, keeps its state. A job that you stopped and that ends with
no safe point after Stop also ends as `stopped`: its result is not used.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1519"
target="_blank" style="float:right; font-size:smaller">source</a>

### track

``` python
def track(
    lr:__main__.Learning, key:str, fut, kind:str, ok:NoneType=None, why:NoneType=None, again:dict=None, **kw
):
```

*Start the row `key` of the pool future `fut` (or of the future that the
function `fut` gives), and end it when the future ends:* `ok(result)`
gives more fields (and can set `state`), `why(error)` the line of a
failure. A row that ended first stays. After Stop, the row ends
`stopped`. `again` keeps the inputs for Do it again. Return the future.

A job that runs long says what it does now.
[`job_said`](https://vedicreader.github.io/abhyasa/learner.html#job_said)
writes a new line to a running row.
[`Said`](https://vedicreader.github.io/abhyasa/learner.html#said) is the
state of one pool job, and a new
[`said`](https://vedicreader.github.io/abhyasa/show.html#said) goes to
its row too, so a page that reads the row sees the step. Each new line
is also a safe point: after Stop, the job ends there.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1535"
target="_blank" style="float:right; font-size:smaller">source</a>

### Said

``` python
def Said(
    lr:__main__.Learning, key:str, **kw
):
```

*The state of one pool job, for its card; `update(said=…)` also writes
the line to the row `key` of `lr`.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1530"
target="_blank" style="float:right; font-size:smaller">source</a>

### job_said

``` python
def job_said(
    lr:__main__.Learning, key:str, said:str
)->bool:
```

*Write the line
[`said`](https://vedicreader.github.io/abhyasa/show.html#said) to the
row `key` while its job runs. A row that cannot be written never breaks
the job. Whether it changed.*

### Seen, closed, and the activity

Opening the page of an object marks its ended jobs as seen. At a start
of the app, a row that says `running` has no job any more: it becomes
`closed`.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1547"
target="_blank" style="float:right; font-size:smaller">source</a>

### close_jobs

``` python
def close_jobs(
    lr:__main__.Learning
)->int:
```

*At a start: each row that says `running` becomes `closed`, as its job
did not live across the restart. Return the count.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1543"
target="_blank" style="float:right; font-size:smaller">source</a>

### seen_jobs

``` python
def seen_jobs(
    lr:__main__.Learning, obj:str
)->int:
```

*Mark each ended job of the object `obj` as seen. Return the count that
changed.*

[`activity`](https://vedicreader.github.io/abhyasa/learner.html#activity)
is what the chip lists: each job that runs, then each ended job that is
not seen and less than `FRESH` old. A job that you stopped shows too, so
its Do it again is near. (vishalakshi’s `Ledger.activity` leaves stopped
jobs out, so abhyasa reads the rows itself.) A quiet job never shows.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1563"
target="_blank" style="float:right; font-size:smaller">source</a>

### chip_words

``` python
def chip_words(
    rows
)->str:
```

*The words of the chip: `2 running` while a job runs, else
`Ready: {title}`, `Failed: {title}` or `Stopped: {title}`, with the
count of the others.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1555"
target="_blank" style="float:right; font-size:smaller">source</a>

### activity

``` python
def activity(
    lr:__main__.Learning, now:float=None
)->fastcore.foundation.L:
```

*The rows for the activity chip: the running jobs, newest first, then
the ended jobs that are new to you.*

### Tests

A temporary vault for the tests.

A job starts with a running row and no end; the row is in the chip.

``` python
lr = temp_learning()
r = job_start(lr, 'walk:w1:research', 'research', title='Softmax', said='Finding sources', url='/w/w1#sources', obj='walk:w1')
test_eq((r.state, r.end, r.key), ('running', None, 'walk:w1:research'))
test_eq([x.key for x in activity(lr)], ['walk:w1:research'])
test_eq(chip_words(activity(lr)), '1 running')
```

Its end is news until you open the walk.

``` python
v = job_version(lr)
job_end(lr, 'walk:w1:research', said='2 of 3 ideas have a source')
assert job_version(lr) > v
test_eq(chip_words(activity(lr)), 'Ready: Softmax')
test_eq(seen_jobs(lr, 'walk:w1'), 1)
test_eq(activity(lr), [])
```

An old end and a quiet job do not show. A job that you stopped shows, so
you can do it again.

``` python
job_start(lr, 'k2', 'build', title='Old'); job_end(lr, 'k2')
test_eq(activity(lr, now=time.time() + FRESH + 1), [])
job_start(lr, 'k3', 'research', title='Talk', quiet=True)
job_start(lr, 'k4', 'research', title='Mine'); job_end(lr, 'k4', 'stopped')
test_eq([x.key for x in activity(lr)], ['k4', 'k2'])   # k2 is new now, k4 ended last; k3 is quiet
lr.put_job('k4', seen=True)
test_eq(job_end(lr, 'nope'), None)
```

A restart closes each running row, and the chip says so.

``` python
job_start(lr, 'k5', 'build', title='Diffusion')
test_eq(close_jobs(lr), 2)   # k3 and k5
test_eq(lr.job('k5').state, 'closed')
test_eq(chip_words(activity(lr)), 'Stopped: Diffusion and 1 more')
```

[`track`](https://vedicreader.github.io/abhyasa/learner.html#track) ends
the row of a pool future: `done` with the fields of `ok`, or `failed`
with the line of `why`. `ok` can also make it `failed`.

``` python
import threading
from concurrent.futures import ThreadPoolExecutor
```

``` python
pool = ThreadPoolExecutor(1)
track(lr, 'b1', pool.submit(lambda: 'w9'), 'build', ok=lambda jid: dict(url=f'/w/{jid}', obj=f'walk:{jid}'), title='A link').result()
pool.shutdown(wait=True)
test_eq((lr.job('b1').state, lr.job('b1').url, lr.job('b1').obj), ('done', '/w/w9', 'walk:w9'))
pool = ThreadPoolExecutor(1)
f = track(lr, 'b2', pool.submit(lambda: 1 / 0), 'build', why=lambda e: 'No walk.', title='Bad')
pool.shutdown(wait=True)
test_eq((lr.job('b2').state, lr.job('b2').why), ('failed', 'No walk.'))
pool = ThreadPoolExecutor(1)
track(lr, 'b4', lambda: pool.submit(lambda: None), 'demo', ok=lambda d: None if d else dict(state='failed', why='No demo.'), title='Late').result()
pool.shutdown(wait=True)
test_eq((lr.job('b4').state, lr.job('b4').why), ('failed', 'No demo.'))   # a function that submits; `ok` sets the state
pool, gate = ThreadPoolExecutor(1), threading.Event()
f = track(lr, 'b5', lambda: pool.submit(gate.wait, 5), 'sort', title='Sort')
job_end(lr, 'b5', 'stopped'); gate.set(); f.result(); pool.shutdown(wait=True)
test_eq(lr.job('b5').state, 'stopped')   # you stopped it: its late end does not make it done
```

A new line of a running job goes to its row.

``` python
job_start(lr, 'b3', 'build', title='Paper', said='Reading the paper…')
st = Said(lr, 'b3', said='Reading the paper…')
st.update(said='Reading 2 of 3 pages…')
test_eq((st.said, lr.job('b3').said), ('Reading 2 of 3 pages…', 'Reading 2 of 3 pages…'))
test_eq((job_said(lr, 'b3', 'Reading 2 of 3 pages…'), job_said(lr, 'nope', 'x')), (False, False))   # no change, no row
```

An ended row older than `KEEP` goes at the next start of a job.

``` python
lr.put_job('b2', end=time.time() - KEEP - 1)
job_start(lr, 'b6', 'build', title='Next')
assert lr.job('b2') is None and lr.job('b3') is not None
```

A slow fake job does three steps, with a safe point before each. Stop
while it does the first step: it ends at the next safe point.

``` python
pool, done, at, go = ThreadPoolExecutor(1), [], threading.Event(), threading.Event()
def slow():
    for k in range(3): check_stop(lr, 's1'); at.set(); go.wait(5); done.append(k)
    return 'made'
f = track(lr, 's1', lambda: pool.submit(slow), 'demo', title='Slow', again=dict(slug='softmax'))
at.wait(5)   # it does the first step
test_eq((stop_job(lr, 's1'), halted(lr, 's1')), (True, True))
go.set(); pool.shutdown(wait=True)
test_eq((done, type(f.exception())), ([0], JobStopped))
```

The stopped job says so, in the chip too.

``` python
test_eq((lr.job('s1').state, lr.job('s1').said), ('stopped', STOPPED))
assert 's1' in activity(lr).attrgot('key')
test_eq(stop_job(lr, 's1'), False)   # it does not run now
```

A job with no safe point after Stop ends `stopped` too, and the fields
of its result do not go on the row.

``` python
pool, gate = ThreadPoolExecutor(1), threading.Event()
f = track(lr, 's2', lambda: pool.submit(lambda: (gate.wait(5), 'w1')[1]), 'build', ok=lambda jid: dict(url=f'/w/{jid}'))
stop_job(lr, 's2'); gate.set(); pool.shutdown(wait=True)
test_eq((lr.job('s2').state, lr.job('s2').url), ('stopped', ''))
```

A new line of a job that you stopped is a safe point:
[`Said`](https://vedicreader.github.io/abhyasa/learner.html#said) ends
the job there.

``` python
job_start(lr, 's3', 'build', title='Paper')
st = Said(lr, 's3'); stop_job(lr, 's3')
try: st.update(said='Reading 1 of 3 pages…'); raise AssertionError('no stop')
except JobStopped: pass
test_eq(lr.job('s3').state, 'stopped')
```

Do it again: the stopped job kept its inputs, so it can run again. A job
with no inputs, or a job that runs, cannot.

``` python
test_eq((can_again(lr.job('s1')), lr.job('s1').again), (True, dict(slug='softmax')))
test_eq((can_again(lr.job('s2')), can_again(None)), (False, False))
job_start(lr, 's1', 'demo', title='Slow', again=dict(slug='softmax'))   # the same job again
test_eq((can_again(lr.job('s1')), halted(lr, 's1'), lr.job('s1').halt), (False, False, False))   # it runs, and the last Stop is gone
```

An old jobs document of abhyasa 0.0.15 moves to the ledger at the first
read. Its running row is `closed`, its other rows keep their state, and
a row that is not a dict is left out. The document goes, so the move is
one time.

``` python
old = temp_learning()
rows = {'walk:w7:research': dict(kind='research', state='running', title='Old walk', start=time.time() - 60, obj='walk:w7'),
        'build:b7': dict(kind='build', state='done', title='Old link', start=time.time() - 90, end=time.time() - 30, url='/w/w7', seen=False),
        'bad': 'not a row'}
old.v.add('The background jobs of abhyasa.', title='abhyasa jobs', source=OLD, kind='jobs', meta=dict(rows=rows))
test_eq((old.job('walk:w7:research').state, old.job('build:b7').url, old.job('bad')), ('closed', '/w/w7', None))
test_eq(sorted(old.jobs()), ['build:b7', 'walk:w7:research'])
assert old.v.by_source(OLD) is None   # the old document is gone
test_eq(chip_words(activity(old)), 'Stopped: Old walk and 1 more')   # the closed row ended last
```

A jobs document with meta that is not JSON of rows does not stop the
app: it has no rows, and it goes.

``` python
odd = temp_learning()
odd.v.add('The background jobs of abhyasa.', title='abhyasa jobs', source=OLD, kind='jobs', meta=dict(rows='not a dict'))
test_eq(odd.jobs(), {})
assert odd.v.by_source(OLD) is None
```

## Stored walks

> A walk is a vault document of kind `journey`. This part reads and
> saves it, finds the current idea, and counts the steps for the rail.

The planner that makes a walk is in `abhyasa.path`. The tutor, the web
app and Leela read a walk with the names here.

### The document

A walk is one vault document of kind `journey`. Its meta has the goal, a
title, a subject, and the steps. Each step has a slug, a status (`todo`,
`known` or `skip`) and the reason that it is in the path.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1607"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.drop_journey

``` python
def drop_journey(
    jid:str
):
```

*Remove a walk. Its concepts and their state stay.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1602"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.journeys

``` python
def journeys()->fastcore.foundation.L:
```

*All walks, the most recently changed first.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1596"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.journey

``` python
def journey(
    jid:str
)->fastcore.basics.AttrDict | None:
```

*One walk by id.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1586"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.put_journey

``` python
def put_journey(
    j:dict
)->fastcore.basics.AttrDict:
```

*Save walk `j`, and replace an earlier one with the same id.*

[`current`](https://vedicreader.github.io/abhyasa/learner.html#current)
gives the slug of the idea to learn now in walk `j`, from
[`walk_state`](https://vedicreader.github.io/abhyasa/learner.html#walk_state):
the first idea with no outcome. An idea in `skip` waits, unless no other
idea is left. A session on a walk asks about it.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1612"
target="_blank" style="float:right; font-size:smaller">source</a>

### current

``` python
def current(
    lr:__main__.Learning, j, skip:tuple=(), goal:int=None
)->str | None:
```

*The slug of the first idea of walk `j` that is to do and not in `skip`;
else the first idea that is to do.*

### Change a step, read the rail

`walk_step` merges changes into one step and saves the walk.
`merge_journey` merges changes into the fields of the walk, such as
`sitting`. Both hold the lock of the walk
([`keyed_lock`](https://vedicreader.github.io/abhyasa/learner.html#keyed_lock),
one lock for each kind and key), so two requests on one walk do not lose
a change.
[`walk_state`](https://vedicreader.github.io/abhyasa/learner.html#walk_state)
gives each step a dot for the rail: `done` (outcome `solid`), `shaky`
(outcome `fragile`, `gap` or `misconception`), `skipped`, `known`, `now`
(the first step with no outcome and the status `todo`) or `todo`.

The sections of a walk from a paper or a link follow the document: a new
document or a new section name starts a section. A walk from a topic or
a goal has no source document (`source` is empty). Its sections are the
sections of the plan (the `section` of a step), so a passage from
another document does not start a section, and “Section done” shows only
at the end of a plan section.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1681"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.walk_slug

``` python
def walk_slug(
    jid:str, slug:str, **kv
)->fastcore.basics.AttrDict | None:
```

*Merge `kv` into the step on concept `slug` of walk `jid`, wherever it
is now, under the lock of the walk. None when the walk or the step is
gone. A job that runs while you move steps uses this, never a place.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1662"
target="_blank" style="float:right; font-size:smaller">source</a>

### walk_state

``` python
def walk_state(
    j, by:dict=None
)->fastcore.basics.AttrDict:
```

*The steps with a `dot`, the index of the current step, the counts, and
the sections as `(name, [indexes])`. With `by` (slug -\> concept),* a
step on a concept that you learned in a session or in another walk is
`known`, with `elsewhere`, and it is not the current step.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1640"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.merge_journey

``` python
def merge_journey(
    jid:str, **kv
)->fastcore.basics.AttrDict:
```

*Merge the fields `kv` into walk `jid` and save it, under the lock of
the walk. The other fields stay as they are.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1630"
target="_blank" style="float:right; font-size:smaller">source</a>

### Learning.walk_step

``` python
def walk_step(
    jid:str, i:int, **kv
)->fastcore.basics.AttrDict:
```

*Merge `kv` into step `i` of walk `jid` and save it, under the lock of
the walk.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/abhyasa/blob/main/abhyasa/learner.py#L1620"
target="_blank" style="float:right; font-size:smaller">source</a>

### keyed_lock

``` python
def keyed_lock(
    kind:str, key, rlock:bool=True, weak:bool=False
):
```

*The one lock of `key` in `kind` (such as a walk id or a concept slug),
so that two writers of one thing take turns. With `rlock`,* the thread
that holds it can take it again. With
[`weak`](https://vedicreader.github.io/abhyasa/learner.html#weak), a
lock that no caller holds goes away: use it only with `with`.

[`keyed_lock`](https://vedicreader.github.io/abhyasa/learner.html#keyed_lock)
gives one lock for each kind and key. The same kind and key give the
same lock, and another kind gives another lock. A weak lock goes away
when no caller holds it:

``` python
import gc
```

``` python
a = keyed_lock('walk', 'k1'); test_is(keyed_lock('walk', 'k1'), a); test_ne(keyed_lock('concept', 'k1'), a)
test_eq(type(keyed_lock('concept', 'k2', rlock=False)), type(threading.Lock()))
with keyed_lock('open-q', 'q1', weak=True): assert ('open-q', 'q1') in _WEAK
gc.collect(); assert ('open-q', 'q1') not in _WEAK
```

`walk_slug` finds the step by its concept. A step that moved still gets
the change, and a step that left the walk gets nothing.

``` python
ws = temp_learning(); ws.put_journey(dict(id='wsl', title='W', mode='walk', steps=[dict(slug='a'), dict(slug='b')]))
test_eq(ws.walk_slug('wsl', 'b', note='N').steps[1]['note'], 'N'); test_is(ws.walk_slug('wsl', 'zz', note='N'), None)
```

With `by`,
[`walk_state`](https://vedicreader.github.io/abhyasa/learner.html#walk_state)
reads the concepts too. An idea that you learned in a session or in
another walk is `known (learned elsewhere)`: it counts as done, and the
current step is the next idea that you do not know.

``` python
kn = temp_learning(); kn.put('A', 'a', 'ml'); kn.put('B', 'b', 'ml'); kn.update('a', floor=4, seen=1)
kj = dict(id='k', source=None, steps=[dict(slug='a', status='todo'), dict(slug='b', status='todo')])
test_eq(walk_state(kj).current, 0)   # with no concepts, step a is still to do
ks = walk_state(kj, {c.slug: c for c in kn.concepts()})
test_eq([s.dot for s in ks.steps], ['known', 'now']); test_eq((ks.current, ks.done, ks.steps[0].elsewhere), (1, 1, True))
```
