Documentação

Harness de avaliação

O laya.evals transforma um conjunto de dados etiquetado num score repetível, e uma linha de base num gate de passa/falha, para que uma alteração de qualidade seja um diff revisível em vez de uma verificação manual.

A matemática das métricas e o parser do conjunto de dados são Python puro mais numpy e nunca importam a torch, por isso correm sem pesos. Correr um conjunto de dados contra um checkpoint precisa do checkpoint e leva o seu tempo de carregamento normal.

Início rápido

# check the format without a model
laya-evals validate research/evals/fixture.jsonl

# score a labelled set on one checkpoint, with thresholds and a baseline
laya-evals run data.jsonl --model english --device cpu \
    --min-accuracy 0.8 --max-ece 0.05 --score-within 0.25 --slice language \
    --json report.json --markdown report.md

# compare a saved report to a baseline
laya-evals compare report.json --baseline baseline.json --tolerance choice_accuracy=0.02

laya eval ... é a mesma coisa através da CLI principal, por isso laya eval validate data.jsonl também funciona.

Códigos de saída: 0 em caso de sucesso, 1 quando um limiar ou uma tolerância de linha de base falha, 2 num erro de utilização. O run imprime as métricas globais e quaisquer fatias pedidas para o stdout, e escreve o relatório completo e um sumário Markdown quando são dados --json / --markdown.

Atribuir erros de shortlist

Para um conjunto de escolhas etiquetado de alta cardinalidade, laya.evals_shortlist.evaluate_shortlist usa o caminho predict_shortlist existente e o harness de avaliação regular. Responde a duas perguntas separadas: a recuperação manteve a etiqueta de ouro, e o Laya escolheu-a quando ela estava presente? Esta é uma API Python opt-in para etiquetas de choice; os relatórios comuns de laya-evals run não mudam.

import laya
from laya.evals import Dataset
from laya.evals_shortlist import evaluate_shortlist
from laya.shortlist import embed_fn_from_agent

agent = laya.load()
dataset_path = "intents.jsonl"
dataset = Dataset.from_jsonl(dataset_path)
report = evaluate_shortlist(
    agent, dataset, embed_fn_from_agent(agent), k=20,
    checkpoint_id="my-checkpoint@revision", embedder_id="my-encoder@revision",
    dataset_path=dataset_path,
)
print(report.overall)
print(report.cases[0]["shortlist_status"])

Use a mesma função de embedding e o mesmo checkpoint da implementação que está a ser medida. Os dois identificadores são fornecidos pelo autor da chamada e devem nomear revisões imutáveis; o relatório não consegue inferir os pesos por trás de um callable arbitrário. dataset_path regista o SHA256 do ficheiro ao lado da impressão digital de perguntas existente. Cada caso mantém as etiquetas de shortlist reais e um de entre correct, retrieval_miss ou decision_miss. shortlist_recall_at_k é a fração de etiquetas de ouro retidas. shortlist_accuracy_on_recalled são as decisões corretas divididas pelos casos retidos; é omitido quando nenhum foi retido. O choice_accuracy existente continua a ser a exatidão de ponta a ponta sobre todos os casos, incluindo falhas de recuperação. As métricas de shortlist aparecem nas mesmas fatias de idioma, modelo, pergunta e tag. A latência do pedido inclui o embedding e a chamada de decisão; o relatório não isola os tempos de cada etapa. Com k >= n, a pergunta original passa diretamente e a recuperação tem recall 1 sem chamar o embedder.

Isto não reproduz os resultados do BANKING77 na issue #102: esses números dependem do conjunto de dados, do checkpoint e do bi-encoder dela. Esta API torna o mesmo tipo de diagnóstico repetível num conjunto etiquetado do próprio autor da chamada.

Avaliar uma exportação ONNX

run --onnx PATH pontua um modelo ONNX exportado através do ONNXAgent em vez do Router torch, por isso uma implementação ONNX (incluindo uma cópia INT8 de scripts/export_onnx.py --quantize) é sujeita aos mesmos limiares e linhas de base que o caminho torch:

python scripts/export_onnx.py --model convaiinnovations/laya --output laya.onnx --quantize
laya-evals run data.jsonl --onnx laya.int8.onnx --max-ece 0.05

--model nomeia o checkpoint de onde veio a exportação — um id do Hub ou caminho local, e não um nome curto do Router como english, já que não há Router neste caminho (predefinição convaiinnovations/laya). A sua config e o tokenizer são carregados a partir daí. O agente serve um só checkpoint, por isso uma linha do conjunto de dados cujo campo model nomeie outro falha com um erro claro em vez de ser respondida silenciosamente pelo modelo errado; --device não se aplica. --batch-size usa a API de lotes do agente quando ele tem uma e cai para uma chamada por estado caso contrário; --sort-by-length é encaminhado para essa API de lotes; o fallback por estado não tem grupo para reordenar. Passe --calibration PATH para carregar um mapa de calibração ajustado no ONNXAgent, para que os gates de calibração como --max-ece avaliem contra probabilidades calibradas. O bloco config do relatório regista o caminho onnx e o caminho calibration (quando definido).

Medido em research/evals/fixture.jsonl (12 linhas etiquetadas, checkpoint inglês, CPU):

runner choice_acc noul_acc score_mae ece mean_conf p50 ms
torch Router 0.75 1.00 1.3418 0.1596 0.7304 116.8
--onnx fp32 0.75 1.00 1.3418 0.1596 0.7304 66.3
--onnx int8 0.75 1.00 1.3512 0.1658 0.7304 46.3

A exportação fp32 reproduz exatamente os números da torch, e a cópia quantizada move score_mae em 0.009 e ece em 0.006 — o tipo de desvio que o compare --tolerance se destina a barrar.

Formato do conjunto de dados

Um objeto JSON por linha (JSONL). As linhas em branco e as linhas que começam com # são ignoradas.

campo obrigatório significado
state sim texto, email, ticket ou documento JSON sobre o qual decidir
questions sim um dict de pergunta do Laya, exatamente como o Router.predict aceita
expected sim verdade de referência indexada por id de pergunta: uma etiqueta para choice, um número para score, true/false para noul
tags não strings pelas quais fatiar
language não um código pelo qual fatiar
model não força um checkpoint para esta linha; --model substitui-o. Uma linha que não força nada é etiquetada com o checkpoint com que o Router respondeu

O research/evals/dataset.template.jsonl tem um exemplo comentado.

Métricas

Cada métrica é calculada por resposta sempre que se aplica e agregada sobre o conjunto de dados:

métrica aplica-se a significado
choice_accuracy choice fração cuja etiqueta escolhida coincide
noul_accuracy noul fração cujo booleano (probabilidade >= 0.5) coincide
score_mae score erro absoluto médio
score_within_<tol> score fração dentro de uma tolerância absoluta
ece qualquer resposta com confiança erro de calibração esperado, 15 bins, calculado sobre answer["answer_confidence"], a probabilidade calibrada que o Laya reporta em todos os tipos de resposta
brier qualquer resposta com confiança e etiqueta conhecida Brier score da confiança como P(correct), mean((confidence - correct)**2); menor é melhor
aurc qualquer resposta com confiança e etiqueta conhecida área sob a curva risco–cobertura: um valor de risco por nível de confiança distinto, cada um ponderado pelas respostas que esse nível abrange; menor é melhor, e recompensa uma confiança que ordena certo e errado, em vez de estar apenas calibrada
selective_accuracy@50, selective_accuracy@80 qualquer resposta com confiança e etiqueta conhecida exatidão sobre as respostas que um limiar de confiança no ponto de cobertura de 50% / 80% aceita – o que abster-se da cauda menos confiante compra. Um limiar não consegue dividir um grupo de confianças iguais, por isso isto pode cobrir mais do que a fração nomeada; vê cortes de cobertura
mean_confidence qualquer resposta com confiança média do answer["answer_confidence"] reportado
latency_p50_ms, latency_p95_ms por pedido tempo de relógio que cada pedido esperou, informativo – vê processamento em lote
cost_per_decision_p50_ms, cost_per_decision_p95_ms por decisão o tempo de relógio de uma chamada dividido pelas linhas que transportou, informativo

Cortes de cobertura e empates

Ambas as métricas de cobertura cortam num limiar de confiança, e um limiar aceita toda a resposta na sua própria confiança. Por isso um corte nunca divide um grupo de respostas que partilham uma: quando coverage * n cai dentro desse grupo, todos os membros do grupo são aceites. O número de respostas por trás do valor é, portanto, o limite superior do grupo, e não a fração nomeada – selective_accuracy@50 sobre uma fatia cujas confianças são todas iguais é a exatidão da própria fatia, não a melhor metade dela. A contagem que o gate imprime (n= na mensagem de falha de uma regra) é o tamanho da fatia, não o tamanho aceite, por isso um grupo muito largo não é visível só pela mensagem.

Empates são o caso normal, e não um canto: uma temperatura ajustada pode deixar um bucket a reportar uma massa pontual, que laya.common.answer_confidence regista do choice:11+ distribuído, e um checkpoint real produziu um grupo de seis linhas exatamente em 1.0 de doze respostas. Cortar num índice de linha em vez disso fez ambas as métricas dependerem da ordem em que o conjunto de dados chegou – as mesmas linhas, baralhadas, moveram selective_accuracy@50 entre 0.000 e 1.000.

aurc integra um valor de risco por nível distinto, ponderado pelas respostas que esse nível abrange, por isso continua a ser uma área sob a curva risco–cobertura, e não uma média de pontos de tamanhos desiguais.

Duas consequências a planear:

  • Um número pode mover-se em qualquer direção, por mais do que uma reordenação poderia. Onde um grupo atravessa o corte, a leitura por limiar difere de toda a leitura por índice de linha dos mesmos dados: medido em 400,001 conjuntos de dados em forma de empate, até 0.500 para selective_accuracy@50 e 0.351 para aurc. Na forma de doze respostas acima – seis corretas, todas com confiança 1.0 – aurc move-se 0.327 (0.173 a 0.500). Um gate que estava a passar pode falhar, e um que estava a falhar pode passar; o veredicto anterior dependia da ordem das linhas, incluindo para um limite min ou max absoluto, que nada recusa porque lê uma única execução.
  • Regenere as linhas de base cometidas. config.coverage_metric_definition regista qual definição produziu um relatório. Comparar uma métrica de cobertura entre duas definições é recusado em ambos os gates que subtraem uma linha de base – --baseline --tolerance (EvalReport.compare) e uma regra relativa (max_drop / max_increase) sob --gate-policy – e em qualquer dos lados estar desatualizado, não apenas a linha de base: um candidato produzido por um laya mais antigo transporta um artefacto de ordem de linha que pode ler melhor do que a verdade, por isso barrá-lo contra uma linha de base regenerada corretamente deixaria passar uma regressão que o relatório pontuado corretamente reprova. Sem essa recusa, uma linha de base desatualizada esconde uma regressão real: uma fatia registada em 0.033 sob a definição antiga lê 0.517 sob esta, por isso um candidato que caiu genuinamente 0.217 passaria um max_drop de 0.05. ece e brier não cortam e continuam comparáveis.

Sem empates nos dados há um nível por resposta, e ambas as métricas são exatamente o que sempre foram – idênticas bit a bit, não apenas próximas.

Acrescenta ScoreWithin(0.25) à lista de avaliadores para uma métrica de tolerância; o conjunto predefinido é choice_accuracy, noul_accuracy, score_mae, mean_confidence, mais ece. A partir da CLI a mesma coisa é um só flag: laya-evals run data.jsonl --score-within 0.25 reporta score_within_0.25 a par das predefinições, e o flag repete-se, por isso --score-within 0.25 --score-within 0.5 reporta ambos.

Uma métrica de tolerância precisa de uma resposta score com uma etiqueta numérica, por isso num conjunto de dados sem uma não tem valor: o run nomeia a métrica que não conseguiu calcular em vez de publicar um zero silencioso, e um gate --min / --max que nomeie essa métrica falha como em falta. As tolerâncias que foram pedidas a uma execução são registadas no bloco config do relatório, por isso uma linha de base revista diz que colunas espera.

Processamento em lote e temporização

--batch-size N pontua até N linhas consecutivas que partilham um checkpoint e um esquema de perguntas numa só chamada. Ambas as métricas de temporização vêm das mesmas medições e respondem a perguntas diferentes: cada linha de um lote regressa quando o lote regressa, por isso a sua latency é toda a chamada, enquanto o seu cost_per_decision é 1/N dela. O processamento em lote, portanto, aumenta latency_* e baixa cost_per_decision_* num conjunto de decisões inalterado, e --max latency_p50_ms=... pergunta se os pedidos foram servidos depressa, não se a execução foi barata. Sem --batch-size as duas coincidem.

O compare ignora qualquer métrica *_ms a menos que uma tolerância a nomeie, por isso estas nunca falham uma linha de base por ruído de temporização. O que o harness fez de facto – o tamanho de lote pedido, a forma de runner em que resolveu, quantas linhas partilharam uma chamada, e o maior bloco – é registado no config.timing do relatório, porque o flag sozinho não diz se algo foi processado em lote. Esses contadores registam as chamadas emitidas, não as chamadas que regressaram: com laya-evals run --on-error skip, um bloco cuja chamada levantou exceção ainda conta em rows_grouped e max_chunk, a par das suas entradas em config.errored. A predefinição é --on-error fail, que volta a levantar a exceção em vez de publicar um relatório cujas métricas cobrem apenas as chamadas que regressaram. As duas métricas *_ms contam apenas as chamadas que regressaram, por isso uma chamada falhada nunca contribui com uma latência que não mediu.

Agrupar as linhas dentro de um lote

--sort-by-length agrupa linhas de tamanho semelhante na mesma passagem direta, por isso cada passagem preenche até um máximo mais curto em vez de até à linha mais longa nela. É a forma das chamadas, não as suas respostas: os resultados voltam na mesma ordem e pontuam de forma idêntica, e é por isso que research/ pode reportar 2.15x sobre 10,000 tickets sem nenhuma decisão mudar.

Tem de haver mais do que uma passagem para reordenar, por isso só tem efeito com um --batch-size N abaixo do número de linhas que a execução agrupa. O config.timing mantém as duas afirmações separadas: sort_by_length é o que a linha de comandos disse, sort_by_length_sent é o que chegou ao runner. Uma execução sem --batch-size pede algo que não pode acontecer, e di-lo com sent: false; um runner cujo predict_batch é anterior a este botão é pontuado sem ordenação em vez de levantar TypeError a meio de uma execução longa.

O gate de abstenção num limiar

--min-confidence T encaminha o limiar de abstenção opt-in do core (#361) para cada chamada que a execução faz, por isso o Router e o ONNXAgent marcam as respostas cujo answer_confidence fica abaixo de T com low_confidence: True antes de o harness as ver. Ao contrário do agrupamento, isto muda as respostas que pontuam: a mesma execução com T=0 e T=0.7 é uma experiência diferente, e uma varredura de precision@coverage é uma série destas, não uma única linha de base à deriva.

O intervalo aceite é o laya.confidence.check_min_confidence do core – [0.0, 1.0], finito, não um bool – em vez de uma cópia aqui, por isso um valor que o próprio gate recusaria falha como erro de utilização (saída 2) antes de qualquer checkpoint carregar. 0.0 é um pedido legítimo: é o braço de controlo de uma varredura de precision@coverage, e uma verificação que o descartasse esconderia o próprio piso da varredura.

Um runner cujo predict ou (para uma execução em lote) cujo predict_batch é anterior ao gate é recusado com um EvalError nomeado, não pontuado sem o limiar. Descartar silenciosamente um controlo de pontuação é a classe de mentira que este harness existe para prevenir: o relatório publicaria um valor de precision@coverage para uma política que nunca correu. config.timing regista tanto o pedido como o facto: min_confidence é o limiar que foi solicitado, min_confidence_sent diz se alguma chamada desta execução o transportou de facto.

Fatias

O compare e o run reportam números globais e, para --slice language|model|qid|tag, as mesmas métricas por valor de fatia, por isso uma regressão num idioma ou numa pergunta é visível sem ler o agregado. A fatia model contém o checkpoint que respondeu a cada linha: a escolha do próprio Router por pedido, ou o model do runner para um runner que não encaminha.

Gates de fatia opt-in

O gate de linha de base global pode passar enquanto uma fatia mais pequena de idioma ou pergunta regride. Para tornar uma fatia revista um requisito de CI, guarde uma política JSON como gates.json:

{
  "version": 1,
  "rules": [
    {"slice": {"language": "zh"}, "metric": "choice_accuracy",
     "min_count": 50, "max_drop": 0.05},
    {"slice": {"qid": "intent"}, "metric": "ece",
     "min_count": 50, "max": 0.10}
  ]
}
laya-evals run data.jsonl --baseline baseline.json --tolerance choice_accuracy=0.02 \
    --gate-policy gates.json --json report.json
laya-evals compare report.json --baseline baseline.json \
    --tolerance choice_accuracy=0.02 --gate-policy gates.json

Cada regra seleciona exatamente um valor de language, model, qid ou tag e nomeia a métrica exatamente como aparece no relatório de fatia. Tem um min_count positivo e exatamente um limite: min ou max verifica o valor do candidato; max_drop permite no máximo essa diminuição em relação à linha de base; max_increase permite no máximo esse aumento. Os dois últimos exigem --baseline. A contagem é o número de respostas pontuadas para essa métrica na fatia selecionada, em ambos os relatórios para uma regra relativa. Para ece, é o número de respostas com confiança finita e valor correct booleano. Uma fatia ou métrica em falta, poucas respostas pontuadas, ou casos ignorados/com erro reprovam o gate opt-in. As regras relativas também exigem que ambos os relatórios transportem identidades de execução correspondentes, para que evidência em falta não possa aparecer como uma aprovação. Uma regressão medida reporta a fatia, a métrica, as contagens, os valores e o limite. Sintaxe de política inválida sai com 2 antes de um checkpoint carregar; uma falha de qualidade sai com 1. A política é registada em config.gate_policy de um relatório de run --json. O compare --gate-policy aplica a política fornecida nessa linha de comandos às medições guardadas. Se for diferente da política registada no relatório, o compare di-lo; uma reverificação explícita sob uma nova política não altera a política sob a qual a execução original foi feita.

A comparação global regular ainda se aplica, incluindo a sua tolerância e o comportamento de linha de base legada. Sem --gate-policy, o relato e a comparação de fatias comportam-se como antes.

Identidade da execução

O run registra o que mediu no bloco config do relatório, por isso o artefacto que um revisor lê é revisível por si só:

chave significado
schema a forma do relatório, laya-evals-report/1, para que um consumidor possa recusar um que não consiga ler
dataset o caminho tal como foi escrito – um nome, não um hash
dataset_sha256 o sha256 dos bytes do conjunto de dados que foram analisados
questions_sha256 uma impressão digital do esquema de perguntas: o id, tipo, instructions e criteria de cada pergunta, sobre todo o conjunto de dados
laya_version o laya que calculou os números
coverage_metric_definition qual definição de aurc / selective_accuracy@* produziu este relatório (vê cortes de cobertura). Uma regra de gate relativa sobre qualquer um deles recusa uma linha de base registada sob uma definição diferente, em vez de subtrair números que não significam a mesma coisa
thresholds o gate que esta execução aplicou: min, max e baseline_tolerance
gate_policy a política opcional de gate de fatia aplicada por run --gate-policy
revisions o commit a partir do qual foi carregado cada checkpoint que respondeu (vê abaixo)

dataset é um caminho, e um caminho não é uma identidade: um conjunto de dados pode ser editado no local, movido, ou reobtido com o mesmo nome, e uma cache de CI pode entregar a duas execuções o mesmo nome de ficheiro e bytes diferentes. questions_sha256 cobre o que foi pedido em vez de quantas linhas havia, por isso acrescentar estados a um conjunto de perguntas inalterado deixa a impressão digital intacta – dataset_sha256 continua a mudar, e acrescentar uma linha é uma alteração aos dados, não à pergunta.

Cobre também instructions, porque o texto da instrução é o prompt. O build_sequence renderiza "<type> question: <instructions>" para a cabeça tokenizada, o Agent recusa uma pergunta sem uma («acrescenta o texto que o modelo deve responder»), e a própria identidade de pergunta do Laya já a conta: o Router._question_schema e o agrupamento em lote deste harness baseiam-se ambos no dict completo de perguntas, e tests/test_router_batch.py fixa que reformular apenas as instructions move uma linha para o seu próprio grupo de lote. Então uma instrução reformulada continua a comparar igual a uma linha de base? Não – e é esse o ponto. «Julga se um reembolso se justifica» e «Sê conservador e aprova apenas pedidos de reembolso explícitos» fazem perguntas diferentes, e o gate de métricas só consegue notar quando a diferença, por acaso, move um número mais do que a tolerância que nomeaste. Nomear uma opção choice é o mesmo argumento: criteria é o espaço de decisão, e as verificações metamórficas em research/eval/metamorphic.py existem porque renomear uma etiqueta muda respostas.

Nada no texto da instrução é normalizado, exceto o único passo que o próprio motor aplica: um instructions que não seja string é hasheado como json.dumps(ins, ensure_ascii=False), correspondendo a Agent._to_internal. Por isso o espaço em branco e a redação contam ambos, e uma reformulação que um humano considera uma revisão de texto é tratada como uma nova experiência. É a predefinição honesta – a alternativa é uma heurística de semelhança entre uma execução e a sua linha de base, e nenhum sistema de avaliação de uso comum tem uma.

Nada que transporte tempo é registado, por isso um relatório continua reproduzível byte a byte para um runner fixo.

REPORT_SCHEMA, questions_fingerprint(dataset) e file_fingerprint(path) são públicos, por isso um autor de chamadas que conduza laya.evals.evaluate diretamente obtém a mesma identidade que uma execução da CLI.

Linha de base e gate de CI

  • Mantém o conjunto de dados, um relatório de linha de base (a saída --json que reviste) e as tolerâncias juntos, em commit, para que uma alteração seja um diff revisível. --tolerance METRIC=VALUE é o desvio absoluto máximo permitido para essa métrica.
  • laya-evals run ... --baseline baseline.json --tolerance ... sai com código não-zero em caso de desvio, por isso encaixa na CI sem alterações. laya.evals.EvalReport.compare e assert_regression expõem a mesma lógica para testes.

O gate de métricas responde a «os números moveram-se». Não consegue responder a «eram os mesmos números», porque o compare lê overall e só overall – por isso uma linha de base registada contra um conjunto de dados passaria um candidato pontuado noutro, com aritmética idêntica. EvalReport.comparable_to fecha isso: compara schema, dataset_sha256 e questions_sha256, e run --baseline e compare continuam a imprimir cada delta, e depois falham com uma saída não-zero nomeando a chave e ambos os valores:

FAIL: baseline is not comparable: dataset_sha256 (dataset bytes): baseline is <sha>, this run is <sha>

Uma chave em falta de um dos lados é desconhecida, não um conflito, por isso todos os relatórios escritos antes de a identidade existir continuam a comparar exatamente como antes. Isso inclui a linha de base do gate agendado abaixo, que vem de research/eval/ e não tem nenhum config.schema.

Duas superfícies de CI usam isto:

  • uma tarefa sem pesos em .github/workflows/ci.yml corre tests/test_evals.py e tests/test_evals_api.py, por isso a matemática das métricas, o parsing do conjunto de dados e a CLI ficam cobertos em cada PR sem descarregar um checkpoint;
  • .github/workflows/evals.yml corre semanalmente, antes de uma publicação e a pedido: avalia o checkpoint inglês na suite MASSIVE inglesa e compara com research/results/eval_english_51_languages.json com as tolerâncias em research/evals/thresholds.json. Envia o relatório como artefacto e não bloqueia um PR.

O harness é determinístico para uma revisão de checkpoint fixa, por isso um relatório é reproduzível. O run registra o conjunto de dados, o modelo e o dispositivo, mais os factos de temporização da execução, no bloco config do relatório, e revisions: o commit a partir do qual foi efetivamente carregado cada checkpoint que respondeu. --revision <SHA> fixa esse commit para todos os checkpoints que a execução carrega, e --revision english=<SHA> fixa um checkpoint (repetível) — que é a forma que uma execução de encaminhamento automático quer, já que os três checkpoints são três repositórios e um commit não pode existir em todos eles. Deixado sem fixar, a execução toma o ramo predefinido do checkpoint e o relatório continua a dizer que commit respondeu, por isso um desvio de linha de base pode ser atribuído aos pesos ou ao código. laya/revisions.py publica SHAs de commit revistos em PINNED_REVISIONS para autores de chamadas que queiram optar por eles. Com --onnx, só um --revision <SHA> simples se aplica, ao download da config e do tokenizer.

Acrescentar o conjunto etiquetado real

Coloca um JSONL em research/evals/ e uma linha de base revista ao lado dele, e depois aponta um workflow (ou research/evals/check_regression.py) a ambos. O formato é o mesmo da fixture; nada no harness sabe do MASSIVE.