Foi em Roma, há cerca de um ano, depois de um dia particularmente exaustivo, que conheci uma dessas tréguas […]. Não vou senão raramente à cidade, onde procuro cumprir, num só dia, o maior número possível de obrigações. O dia fora desagradavelmente sobrecarregado: uma sessão do Senado seguira-se de outra no tribunal e de uma discussão interminável com um dos magistrados das finanças, e, finalmente, de uma cerimônia religiosa impossível de ser abreviada, durante a qual a chuva caiu sem cessar. Eu próprio programara tantas atividades diferentes sem intervalos, deixando entre elas o menor espaço de tempo possível para as importunações e bajulações inúteis.

Marguerite Yourcenar, Memórias de Adriano

A cada vez que reclamar da correria, vou tentar me lembrar que imperadores romanos também tinham dias assim.

Iluminado no Todoist

Acho que vocês, leitores, compartilham da minha jornada em ser mais produtivo, pois é nesse blog que eu documento minha batalha para fazer melhor minhas tarefas.

Não que quantidade de tarefas seja algo significativo, mas com o tempo aprendi a filtrar o que coloco no Todoist como significativo para mim. Cada vez que eu completo uma tarefa, alguma coisa melhorou na minha vida.

Quem sabe isso me motiva a colocar mais tarefas relacionadas a este blog (incluindo meus truques para usar Todoist)…

Como ler artigos científicos e extrair o máximo de informação

Ler artigos científicos (ou papers) faz parte do meu trabalho regular como pesquisador e, com a prática de anos, desenvolvi um método que me serve bem, e que documento aqui. Para ser franco, acho que extrair o máximo de informação útil de um paper é uma das maiores habilidades, e por isso acho que este texto pode ajudar o leitor.

Minha prática é um misto de ideias de Como Ler Livros e do método Zettelkasten, e consiste de 3 etapas:

  1. Leitura inspecional
  2. Leitura analítica
  3. Processamento de notas

Leitura inspecional do paper

Na leitura inspecional de um paper, estou me familiarizando com o trabalho.:

  • Sobre o que é?
  • Que problemas ele investiga e resolve?
  • Como ele me ajuda nos meus projetos atuais?

A leitura inspecional é para ser rápida, o que significa que eu não paro se eu não entendo alguma equação, figura ou método sendo descrito. Procuro fazer em uma única sessão de trabalho, e geralmente me dou uma hora. Dependendo do tamanho do artigo, é o suficiente para ler superficialmente todo o texto; às vezes, leio o resumo, introdução e conclusão com muito mais atenção, e apenas examino o restante do artigo, à procura de algo interessante.

Dependendo do meu objetivo ao ler este paper em específico, uma leitura inspecional basta. Se quero compilar quais autores estudaram tal assunto, e a leitura inspecional me mostra que Fulano fez este estudo e concluiu isto (através da leitura mais atenciosa da conclusão, vide parágrafo acima), a leitura está concluída. Muitas vezes, aliás, essa primeira leitura me mostra que um determinado artigo não serve para mim, e o papel é transformado em rascunho.

Falando nisso, para extrair o máximo de informação de um artigo, como eu estou propondo neste texto, para mim é fundamental imprimir o artigo e ler analogicamente. Pode parecer contra o bom uso de recursos naturais, mas tento minimizar esses efeitos aproveitando os papeis como rascunho após a leitura e reciclando-os. Além disso, se eu ler no computador, há o gasto adicional de energia para renderizar o PDF (quando muitas vezes a tela do computador está apagada enquanto estou lendo no papel); se eu comprar um iPad para isso, de onde vem os recursos naturais e humanos para fabricar cada peça?

Nada substitui a experiência de se isolar, talvez apenas com uma música de fundo, e ler um pedaço de papel com canetas para fazer anotações. Na leitura inspecional, essas anotações são rápidas e pontuais.

Leitura analítica do paper

A leitura analítica do paper é demorada e detalhada. Dependendo do assunto ou do nível de profundidade de que preciso (para implementar algum modelo ou para escrever sobre esse assunto, por exemplo), algumas vezes a leitura analítica requer múltiplas sessões de trabalho profundo de 1h30min ou 2h por alguns dias.

Nessa etapa, muitas notas são escritas — às vezes em notas adesivas, às vezes em notas soltas anexadas, às vezes no verso do paper. Se o meu objetivo é entender a fundo o que está sendo abordado, preciso expressar com minhas próprias palavras.

O resultado de um artigo lido com atenção
Às vezes o básico da Termodinâmica é o mais difícil de entender
Como vêem, eu não economizo em escrever até as ideias mais básicas
Sim, um doutor em Engenharia Mecânica às vezes precisa converter RPM para hertz.

Processamento de notas

Essas notas soltas, escritas na pressa e muitas vezes sucintas, precisam ser transformadas em textos coerentes, que possam ser usados como base para meus próprios artigos, para Teses, para relatórios etc.

Como citei no início deste post, meu método é bastante inspirado no método Zettelkasten. Basicamente, para cada paper lido e suas notas, crio minhas notas em versão digital. Cada “nota” (Zettel, em alemão) é um arquivo .txt em uma pasta no Dropbox. Importante: cada nota corresponde a um assunto, e não a um resumo de um paper. Um mesmo artigo pode trazer diferentes assuntos, e um mesmo assunto pode ser abordado por diferentes autores.

Meu sistema é complexo mas funciona bem para mim. Uso um editor de texto arcaico chamado Emacs, voltado à programação e de um paradigma diferente do Word, com suas figuras e fontes; no Emacs, tudo é basicamente texto.

Dentro do Emacs, há um pacote chamado Deft. Com um atalho de teclado, a minha pasta de notas é carregada e o Emacs me apresenta uma lista das minhas notas, onde então posso selecionar e abrir uma para editar. O recurso-chave desse pacote, porém, é que ele me permite buscar por palavras dentro das notas. Isso representa um avanço sobre simplesmente ter a pasta de notas aberta no Explorador de Arquivos e abrir cada arquivo individualmente; no Deft, estamos lidando com “objetos” notas, e não com arquivos.

Interface de lista de notas no Emacs, usando Deft

Cada nota é escrita em Markdown, usando a versão Pandoc (veja abaixo).

Uma nota no Emacs

Também configurei o Emacs+Deft para, com um outro atalho de teclado que cria uma nova nota, me pedindo o título e as palavras-chaves. Esse atalho cria um arquivo com um template pré-configurado, conforme aparece na Figura.

O leitor quer mais informações sobre esse template de notas e minha configuração de Emacs+Deft? Deixe nos comentários!

Meu fluxo de trabalho funciona da seguinte forma: após ler alguns artigos analiticamente, tenho um conjunto de notas em paper. Reviso-os e separo-os por temas, pensando em novas notas que podem ser criadas. Repare que muitas vezes a leitura de um paper me faz atualizar alguma nota que foi escrita anos atrás, com novas descobertas empíricas ou uma melhor explicação de alguma teoria. Processo então todas as notas, escrevendo novos textos ou revisando notas antigas; isso gera novas ideias de textos, que são facilmente criados usando os atalhos acima.

Quando quero escrever um documento final, geralmente em LaTeX, posso usar o Deft para ver todas as minhas notas sobre um assunto. Como estes textos são meus textos, escritos com as minhas palavras, muitas vezes copio notas inteiras direto para o corpo de um manuscrito. A ferramenta Pandoc é muito útil para converter entre Markdown e LaTeX.

Conclusões

Este é o meu ciclo de extrair informações de artigos. Não é nada novo ou revolucionário; apenas estou descrevendo algumas práticas particulares minhas e as ferramentas que uso.

Trato esse processo com alto respeito por achar que isto é o cerne da minha atividade de pesquisador.

O leitor acha que esse é um tema interessante de ser explorado mais a fundo aqui no blog? Deixe sua opinião nos comentários!

Quando usar notebooks ou scripts para analisar dados?

Um de meus tópicos favoritos recentemente em podcasts e blogs é a discussão sobre usar notebooks ou scripts em contexto de análise de dados e computação numérica.

Se você mal chegou neste texto e não está entendendo nada, vamos por partes. Tudo que vou falar aqui se aplica ao meu contexto de computação numérica: usar computadores para resolver equações e modelos matemáticos e analisar e plotar os dados resultantes, usando gráficos e ferramentas estatísticas. Nesse tipo de ambiente, é comum usar esses dois tipos de ferramentas, conforme vou ilustrar.

Neste texto vou usar exemplos em Python, mas ambas as ferramentas podem ser usadas com várias linguagens de programação.

Nos notebooks Jupyter, eu escrevo códigos usando um ambiente interativo no navegador, com todos os recursos visuais que isso me permite.

Um exemplo de código Python, gráfico e notas em um notebook Jupyter, segundo meu uso

Um “caderno” em Jupyter é divido em células independentes, que podem conter código, imagens, ou texto. Quando uma célula de código é executada, ela pode gerar um resultado que é impresso na tela, na forma de um gráfico ou de mensagens de texto (ambos os usos aparecem na imagem acima). Além disso, a execução de uma célula depende de células que foram executadas antes dela, onde podem ter sido definidas variáveis e funções – mas isso não precisa seguir a ordem “de cima para baixo” de um caderno Jupyter, o que pode gerar cenários confusos. Por exemplo, suponha que eu executa todas as células nessa ordem vertical (até a última célula embaixo), e depois queira voltar e arrumar aquele gráfico mostrado ali; agora, a célula do gráfico vai ser influenciada por código que “teoricamente” foi escrito depois dela, já que os últimos blocos já foram executados. Já vamos falar sobre soluções para isso.

A outra abordagem é escrever um programa na forma de script, que é executado como uma unidade única. Embora alguns editores atuais até permitam isso, em geral não existe o conceito de células; as linhas de código em um script em Python vão sendo executadas individualmente de cima para baixo até o fim.

Um script de Python no Visual Studio Code sendo editado (parte de cima) e executado (parte de baixo)

Então, voltando à pergunta: quando uso um tipo e quando uso outro?

Em geral, começo minhas ideias de análise em um notebook, considerando que é para isso que ele foi criado. Notebooks no seu estágio inicial são caóticos; vou criando células, volto para trás, edito, testo novas ideias. À medida que descubro a melhor maneira de implementar alguma análise, começo então a documentar e organizar o caderno – aliás, a possibilidade de ter texto formatado junto com código é uma das principais vantagens de Jupyter. Quando ele fica “maduro”, ele serve como um relatório interativo, que pode ser constantemente atualizado.

Uso scripts para trabalhos mais pesados: já testei alguma ideia como um notebook, agora quero executar esse procedimento diversas vezes com diferentes condições. Usar um bom editor como o Visual Studio Code me permite usar bons atalhos e funções para escrever código mais rapidamente. Quando o script fica maduro, ele pode ser incorporado a alguma biblioteca e testado.

Os leitores já devem saber que sou um grande entusiasta de explorar melhor minha criatividade, mesmo em um trabalho científico. Faço sempre um esforço sobre-humano para não me deixar cair rotina de reuniões e preenchimento de relatórios de bolsa. Usar essas diferentes ferramentas de programação (e falar sobre elas) me permite brincar, conhecer a minha forma preferida de programar, descobrir novas maneiras de desenvolver meus projetos.

É como diz Austin Kleon: as ferramentas importam e as ferramentas não importam.

Uso básico de pytest e suas fixtures

Em primeiro lugar, o que diabos é pytest e por que eu me importo com isso?

A biblioteca pytest é um pacote de testes em Python. Para que serve? Como eu sei que tenho muitos leitores na área acadêmica, acho que vou me fazer entender: você escreve seus projetos de programação em Python, para simulação numérica de algum problema físico, para processamento de dados, para automatizar alguma tarefa, para gerar gráficos etc. Você vai aumentando o seu programa até que não tem mais certeza de como uma parte conecta com outra. Você adiciona uma função inofensiva, e então percebe que outra, “aparentemente” não relacionada, parou de funcionar. Os arquivos que eram para ser gerados não o são mais. Os gráficos saem esquisitos. Os resultados numéricos não fazem sentido.

Testes, nesse contexto de programação, são funções adicionais que asseguram o funcionamento correto do código. Se uma determinada função, quando executada, deve sempre gerar um arquivo results.txt, então você escreve um teste que executa a função e assegura que esse arquivo existe. A cada modificação no seu programa, você executa seu teste; se ele falha, é sinal de que é preciso parar, avaliar e corrigir erros.

Neste ambiente acadêmico em que estou, não vejo muita ênfase sendo colocada em testar o seu código, o que é um contrassenso. Testes têm um sentido científico: se eles passam, então a hipótese de que o você programou representa a realidade se torna mais forte. Eles não são “perda de tempo”; é bem fácil escrever rapidamente um programa que não funciona.

Tudo o que vou falar aqui se baseia nessa minha experiência: um pesquisador que usa de programas em Python como uma ferramenta para produzir conhecimento científico.

Instalação e uso básico

Para aplicações acadêmicas, especialmente em Windows, a melhor maneira de usar Python é através da distribuição Anaconda. A biblioteca pytest já está incluida.

No espírito acadêmico de transparência, vou compartilhar exatamente como tenho usado pytest para melhorar minhas práticas de programação e, com isso, produzir resultados científicos mais confiáveis.

Tenho um projeto chamado magnet3Dpolomag. Não se preocupem sobre o que é, apenas entendam como minha plataforma de testes está organizada:

Lista de diretórios de um projeto usando pytest
A propósito, esse screenshot é do Visual Studio Code

No meu diretório raiz, além de vários arquivos, existem essas duas pastas importantes: uma pasta com o pacote em Python, com seus vários módulos, e uma pasta de testes. O diretório do pacote Python é declarado como “importável” ao conter o arquivo de inicialização __init__.py. Assim, quando esse pacote é instalado, eu posso digitar simplesmente:

import magnet3Dpolomag

e tudo que estiver no arquivo __init__.py dentro da pasta magnet3Dpolomag vai ser carregado.

A convenção de testes em Python é usar nomes de arquivos que comecem com test_, e no momento eu só tenho um arquivo geral: dentro dele existem funções que testam o correto funcionamento de magnet3Dpolomag quando parâmetros válidos são fornecidos (testar se os parâmetros passados ao programa são válidos ou não é outra história).

Como um exemplo de teste, aqui está um pedaço do meu pacote: uma classe Magnet3DModel, com uma função que faz uma transformação de dados de simulações e retorna dois vetores:

import numpy as np

class Magnet3DModel:

    def get_profile_data(self, ):
        """Return phi, B vectors at the central plane"""
        
        middle_plane = self.z_profile_1q == 0
        phi_middle_plane_1q = self.phi_profile_1q[middle_plane]
        B_middle_plane_1q = self.B_profile_1q[middle_plane]
    
        phi_vector = np.concatenate((phi_middle_plane_1q,
                                 phi_middle_plane_1q + 90,
                                 phi_middle_plane_1q + 180,
                                 phi_middle_plane_1q + 270))

        B_profile = np.concatenate((B_middle_plane_1q,
                                B_middle_plane_1q[::-1],
                                B_middle_plane_1q,
                                B_middle_plane_1q[::-1]))

        return phi_vector, B_profile

Uma breve explicação: esse “modelo”, quando é executado, armazena uma nuvem de dados nos vetores z_profile_1q, phi_profile_1q e B_profile_1q. Para esse método em particular, estou apenas interessado nos dados que correspondem ao “plano” z_profile_1q == 0. O sufixo 1q indica que se trata de dados “no primeiro quadrante”; como os dados são periódicos, eu retorno os dados “expandidos”.

Eu quero me assegurar que, quando esse modelo é executado, essa função vai retornar esses vetores conforme esperado, com valores apenas numéricos:

import numpy as np
import magnet3Dpolomag

def test_can_access_magnetic_profile():

    m = magnet3Dpolomag.Magnet3DModel()
    # executa mais instruções para simular o modelo

    phi_profile, B_profile = m.get_profile_data()

    assert (
        np.isfinite(phi_profile).all() and 
        np.isfinite(B_profile).all()
    )

Repare na semântica do teste: ao escrever esse código, eu não tenho ideia de como get_profile_data() funciona; eu apenas sei que, quando eu executar o modelo, quero poder chamar uma função que me retorne esses vetores e que estes não tenham valores não-numéricos (que poderia surgir de algum erro). Para esse fim, eu uso a função isfinite() da biblioteca NumPy, que retorna um vetor booleano que checa exatamente isso; então, eu uso o método all() de vetores da biblioteca NumPy, que vai retornar True se todos os elementos são True.

A chave de uma função de teste é o comando assert; se o seu argumento é falso, então o programa para (ou, no caso, a função de teste falha). A biblioteca pytest tem como grande característica usar esse comando, que faz parte da linguagem Python “pura”, e reportar exatamente o que deu errado, no caso de uma falha.

Testes são executadas invocando pytest em um terminal. A partir do diretório onde é executado e varrendo os subdiretórios em todos os níveis, esse comando primeiro “descobre” os testes, arquivo que começam com test e, dentro deles, as funções e classes que começam com test_. Como quase tudo em se tratando de pytest, esse processo de descoberta de testes é configurável. A biblioteca então executa todos as funções e métodos de teste e reporta, de maneira muito inteligente, os testes que passaram e os que falharam.

Este exemplo pode parecer meio tolo, então quero enfatizar o grande propósito dessa minha discussão: escrever testes me ajuda a entender um código em Python que o meu de anos atrás escreveu, e me faz pensar sobre o que ele deve fazer.

Além disso, eu não vou estar nesse laboratório para sempre, e estes programas que eu crio para auxiliar no nosso projeto ficarão de legado para a minha equipe. Cada hora gasta trabalhando numa plataforma de testes pode não gerar resultados práticos agora, mas tem consequências futuras. Os financiadores do meu projeto atual não poderiam se importar menos se o meu código está bem testado, e talvez até achem que eu deveria estar fazendo outra coisa; mas aposto que eles ficam felizes quando a próxima pessoa que trabalhar nisso (eu mesmo eu, daqui a algumas semanas) não perder dias de trabalho tentando resolver um problema que poderia ter sido solucionando mais rapidamente executando a plataforma de testes e vendo qual parte do sistema está defeituosa.

Anteriormente escrevi que um das grandes sacadas de pytest é “turbinar” o comando `assert`. Outra grande sacada é o uso de *fixtures*: funções que auxiliam os testes. Não quero nesse post fornecer um tutorial de Python ou mesmo de pytest; quero apenas [documentar de maneira resumida como estou *aprendendo*][show] a escrever testes e motivar pesquisadores a fazer o mesmo. Além da documentação da biblioteca, [*Python Testing with pytest*][pytestbook] é bem bom. No exemplo que usei acima, eu apresentei uma função de teste que simula a execução do modelo e confere alguns dados gerados por ele. E se, com base na mesma execução do modelo, eu queira testar outras condições, como outros vetores e matrizes gerados, arquivos que devem ser gravados, parâmetros que devem ser armazenados? Esse é um exemplo clássico de *fixture*: uma função que não é um teste, mas que é executada antes dos testes, e que passa informações “compartilhadas” entre eles. Além disso, é possível repetir o mesmo teste várias vezes, checando por diferentes “coisas”. Por exemplo:

import numpy as np
import magnet3Dpolomag
import pytest
import math

@pytest.fixture(scope='module')
def run_model():

    m = magnet3Dpolomag.Magnet3DModel()
    m.run()

    return m

def test_can_access_magnetic_profile(run_model):

    m = run_model
    # executa mais instruções para simular o modelo

    phi_profile, B_profile = m.get_profile_data()

    assert (
        np.isfinite(phi_profile).all() and 
        np.isfinite(B_profile).all()
    )

@pytest.mark.parametrize(
     'result',
     [
         "Demagnetized fraction[%]",
         "Saturated fraction[%]",
         "V_gap[l]",
         "V_magnet[l]",
         "V_demag[l]",
         "V_magnetic_steel[l]",
         "V_sat_magnetic_steel[l]",
         "V_stator[l]",
         "V_rotor[l]"
     ]
 )
 def test_access_results(self, result, run_model):
    m = run_model
    r = m.get_results_series()[result]

   assert ( (math.isfinite(r)) and (r >= 0))

A função no início é uma fixture que simplesmente cria e executa o meu modelo e retorna esse objeto, já povoado com os dados de simulação. Essa função é decorada com pytest.fixture, com um argumento scope que define que ela é executada por módulo. Ou seja, para todos os testes que usam essa fixture e que estão definidos nesse módulo (script), o modelo só é executado uma vez e passa o seu estado para todos os testes. O primeiro teste mostrado repete a lógica do exemplo da seção anterior; o segundo é outro exemplo onde quero testar que uma função que retorna uma Series tem de fato todos os registros. Essa função é decorada com pytest.mark.parametrize, onde especifico que o parâmetro result da função de teste assume cada um dos valores especificados na lista. Quando é invocado, pytest faz o loop automaticamente, chamando esse teste tantas vezes quantos são os elementos da lista que define o parâmetro.

Conclusões

Leitores podem estar confusos com a quantidade de informações técnicas. Peço desculpas (mas não muitas); esse post foi a melhor maneira que eu encontrei para consolidar todo meu aprendizado recente no assunto.

O que os leitores acham de textos nesse estilo?

Usando mapas mentais para planejar papers

Entenderam?

Sempre me espanta que mais pessoas não usem mapas mentais para planejar papers (e outros documentos escritos). É só a minha cabeça que contém ideias confusas demais e que precisam ser organizadas de alguma forma?

A imagem acima é do excelente MindNode (que eu acesso através do serviço Setapp), que infelizmente só existe para dispositivos Apple. Para Windows (para qualquer plataforma, na verdade), há o também bom MindMeister.

Meu fluxo de ficar desenvolvendo ideias até elas assumirem uma forma mais definida é totalmente influenciada pelo Presentations Field Guide, do David Sparks, e pelo episódio maravilhosamente chamado de Cooking Ideas do seu podcast Mac Power Users.