Code
Aula 8: Os dados do CERN: múons e massa invariante
The programs in the class folder, highlighted, each with its file to download.
animacoes.py
Os vídeos da aula 8, feitos com o Manim Community.
696 linesDownload
"""Os vídeos da aula 8, feitos com o Manim Community.
Rode, a partir da raiz do repositório, com o nome dos vídeos que quer gravar, ou
sem nome nenhum para gravar todos:
python aulas/08-dados-do-cern/codigo/animacoes.py
python aulas/08-dados-do-cern/codigo/animacoes.py picos treino-massa
Os cinco vídeos, na ordem da aula:
direcao de lado e de frente: o momento transversal p_T, o η e o φ de um múon
massa-invariante um J/ψ se desfaz em dois múons; quando ele anda, a energia e o momento
de cada múon mudam, e a massa dos dois fica a mesma
picos do evento ao histograma: três eventos com a conta, quarenta caindo nas
faixas, depois 100 mil, e os picos aparecem
compressao o asinh(E / 5) leva a energia de 300 múons para outra linha e espalha
os números pequenos
treino-massa a massa da rede contra a da fórmula, das épocas 0 a 80: a nuvem vai
para a diagonal
Precisa do Manim Community (pip install manim), do MiKTeX e do ffmpeg, como os
vídeos das outras aulas. Cada vídeo sai em dois arquivos:
aulas/08-dados-do-cern/videos/<nome>.mp4 1920 por 1080, 30 quadros por segundo, sem som
aulas/08-dados-do-cern/figuras/<nome>-quadro.png o último quadro, que o slide mostra parado
Uma opção grava só o último quadro, para conferir a composição: --quadros
As regras dos vídeos da aula 2 valem aqui. Sem título próprio, porque o título
está no slide; texto com pelo menos 30 pontos do Manim, equação com pelo menos
38. Todo texto entra escrito e toda forma entra desenhada. Nada se sobrepõe: cada
texto tem um lugar fixo, fora dos dados e fora do caminho dos pontos que se
movem. E nada é sorteado: os eventos são os do arquivo, na ordem dele, e a rede é
a de mlp_massa.py, com os retratos de oito épocas gravados em resultados/massa.json.
"""
import json
import math
import shutil
import subprocess
import sys
import time
from pathlib import Path
import numpy as np
from manim import (DOWN, LEFT, RIGHT, UP, Arc, Arrow, Brace, Circle, Create, DashedLine, DecimalNumber, Dot,
FadeIn, FadeOut, GrowArrow, GrowFromCenter, Indicate, LaggedStart, Line, MathTex, Rectangle,
Scene, Square, Succession, SurroundingRectangle, TexTemplate, Text, Transform, TransformFromCopy,
ValueTracker, VGroup, Write, always_redraw, config, linear, tempconfig)
from manimpango import register_font
RAIZ = Path(__file__).resolve().parents[3]
AULA = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(Path(__file__).resolve().parent))
from dimuon import MASSA_DO_MUON, carrega # noqa: E402
# As fontes do curso, as mesmas dos slides, guardadas em infra/fontes/ com as licenças
for arquivo in ("IBMPlexSans-Regular.ttf", "IBMPlexSans-SemiBold.ttf", "SourceSerif4-Semibold.ttf"):
register_font(str(RAIZ / "infra" / "fontes" / arquivo))
SANS = "IBM Plex Sans"
# A paleta de infra/slides.py e das figuras
FUNDO, ESCURO, TEXTO, SUAVE, CINZA = "#F6F5F1", "#14213D", "#3B4658", "#5E6A7D", "#8A94A6"
AZUL, LARANJA, CLARO = "#1F5FAD", "#A8500F", "#C9D7EA"
# As equações em STIX Two, a fonte das equações dos slides (infra/equacoes.py)
MODELO = TexTemplate(preamble="\\usepackage[T1]{fontenc}\n\\usepackage{amsmath}\n\\usepackage{stix2}\n")
config.tex_template = MODELO
JPSI = "\\mathrm{J}/\\psi" # nome de partícula, em letra reta
MASSA_JPSI = 3.0969 # GeV, a massa do J/ψ, a mesma do histograma e das notas
def texto(s, tamanho=30, cor=TEXTO):
"""Texto corrido, na fonte dos slides."""
return Text(s, font=SANS, font_size=tamanho, color=cor)
def eq(*partes, tamanho=40, cor=ESCURO):
"""Equação em LaTeX; cada parte vira um pedaço separado, que se pode transformar sozinho."""
return MathTex(*partes, font_size=tamanho, color=cor, tex_template=MODELO)
def frase(*pedacos, tamanho=30, cor=TEXTO):
"""Uma linha que mistura texto e equação. O pedaço que começa com $ é equação, um pouco maior que o texto,
para as letras terem a mesma altura; a vírgula depois de uma equação vai dentro dela, como em "$p_T,$"."""
partes = [eq(p.strip("$"), tamanho=round(tamanho * 1.25), cor=cor) if p.startswith("$") else texto(p, tamanho, cor)
for p in pedacos]
return VGroup(*partes).arrange(RIGHT, buff=0.16)
class Numero(DecimalNumber):
"""Número que muda na tela, com vírgula decimal."""
def _get_num_string(self, number):
return super()._get_num_string(number).replace(".", ",")
def br(v, casas=2):
"""Número decimal em LaTeX, com vírgula decimal e sem o espaço que o LaTeX põe depois da vírgula."""
return f"{v:.{casas}f}".replace(".", "{,}")
def mil(n):
"""Número inteiro com ponto de milhar: 100.000."""
return f"{n:,}".replace(",", ".")
def legenda(*pedacos):
"""A frase do alto do vídeo, que diz o que está acontecendo; cabe na largura com folga."""
f = frase(*pedacos).move_to(np.array([0, 3.45, 0]))
assert f.width < 13, pedacos
return f
def troca(cena, atual, *pedacos):
"""Apaga a frase do alto e escreve a seguinte."""
nova = legenda(*pedacos)
cena.play(FadeOut(atual), run_time=0.35)
cena.play(Write(nova), run_time=0.9)
return nova
def unitario(graus):
"""O vetor de tamanho 1 que faz esse ângulo, em graus, com o eixo horizontal."""
a = math.radians(graus)
return np.array([math.cos(a), math.sin(a), 0.0])
# ================================================================ 1. a direção do múon
class Direcao(Scene):
"""De lado, o ângulo θ com o feixe, as duas partes do momento e o η; de frente, o φ."""
def construct(self):
self.camera.background_color = FUNDO
O, R = np.array([-6.2, -1.8, 0.0]), 3.7 # o ponto da colisão, no painel de lado, e o tamanho da seta
teta = ValueTracker(50.0) # o ângulo com o feixe, em graus
feixe = Line(O + LEFT * 0.6, O + RIGHT * (R + 1.0), color=ESCURO, stroke_width=4)
nome_feixe = texto("feixe", 30, SUAVE).next_to(feixe.get_end(), DOWN, buff=0.15)
nome_feixe.align_to(feixe.get_end(), RIGHT)
colisao = Dot(O, radius=0.09, color=ESCURO)
lado = texto("de lado", 30, SUAVE).move_to(np.array([-4.3, -2.85, 0]))
def seta_muon(graus):
return Arrow(O, O + R * unitario(graus), buff=0, color=ESCURO, stroke_width=6,
max_tip_length_to_length_ratio=0.07, max_stroke_width_to_length_ratio=10)
leg = legenda("De lado: o múon sai da colisão num ângulo", "$\\theta$", "com o feixe")
self.play(Write(leg), Create(feixe), Write(nome_feixe), Write(lado), run_time=1.6)
self.play(GrowFromCenter(colisao), run_time=0.5)
muon = seta_muon(50)
self.play(GrowArrow(muon), run_time=1.0)
arco = Arc(radius=0.9, start_angle=0, angle=math.radians(50), arc_center=O, color=SUAVE, stroke_width=3)
rot_teta = eq("\\theta", cor=SUAVE).move_to(O + 1.3 * unitario(25))
self.play(Create(arco), Write(rot_teta), run_time=0.9)
self.wait(1.2)
# as duas partes do momento: ao longo do feixe e perpendicular a ele
ponta = O + R * unitario(50)
pe = np.array([ponta[0], O[1], 0.0])
perna_z = Line(O, pe, color=AZUL, stroke_width=8)
perna_t = Line(pe, ponta, color=LARANJA, stroke_width=8)
rot_z = eq("p_z", cor=AZUL).next_to(perna_z, DOWN, buff=0.15)
rot_t = eq("p_T", cor=LARANJA).next_to(perna_t, RIGHT, buff=0.15)
leg = troca(self, leg, "O momento: uma parte ao longo do feixe, outra perpendicular")
self.play(Create(perna_z), Create(perna_t), run_time=1.2)
self.play(Write(rot_z), Write(rot_t), run_time=0.8)
self.wait(1.8)
# o leque das linhas de η constante, e a seta girando por ele
leque, rot_leque = VGroup(), VGroup()
for e in (0, 1, 2, 3):
graus = math.degrees(2 * math.atan(math.exp(-e))) # θ = 2 arctan(e^(−η))
fim = O + (R + 0.35) * unitario(graus)
leque.add(Line(O, fim, color=CINZA, stroke_width=2.5))
rot_leque.add(eq(f"\\eta = {e}", tamanho=38, cor=SUAVE).next_to(fim, unitario(graus), buff=0.12))
leitura = VGroup(eq("\\theta ="), Numero(50, num_decimal_places=0, font_size=40, color=ESCURO),
texto("graus"), eq("\\eta =").shift(RIGHT * 0.5),
Numero(-math.log(math.tan(math.radians(25))), num_decimal_places=2, font_size=40,
color=ESCURO))
leitura.arrange(RIGHT, buff=0.18).move_to(np.array([-4.2, -3.5, 0]))
leitura[3].shift(RIGHT * 0.5)
leitura[4].shift(RIGHT * 0.5)
leitura[1].add_updater(lambda m: m.set_value(teta.get_value()))
leitura[4].add_updater(lambda m: m.set_value(-math.log(math.tan(math.radians(teta.get_value()) / 2))))
leg = troca(self, leg, "O", "$\\eta$", "mede esse ângulo: 0 na perpendicular, e cresce perto do feixe")
self.play(FadeOut(perna_z, perna_t, rot_z, rot_t, arco, rot_teta), run_time=0.6)
self.play(Create(leque), Write(rot_leque), Write(leitura), run_time=1.6)
self.remove(muon)
muon = always_redraw(lambda: seta_muon(teta.get_value()))
self.add(muon)
self.play(teta.animate.set_value(90), run_time=1.4)
self.wait(0.6)
self.play(teta.animate.set_value(12), run_time=6, rate_func=linear)
self.wait(1.0)
# de frente, ao longo do feixe: o círculo do detector e o ângulo φ em volta do feixe
C, raio, comprimento = np.array([3.7, -0.3, 0.0]), 2.2, 1.9
fi = ValueTracker(0.9) # em radianos, como no arquivo
detector = Circle(radius=raio, color=SUAVE, stroke_width=3).move_to(C)
feixe_frente = VGroup(Circle(radius=0.16, color=ESCURO, stroke_width=4).move_to(C),
Dot(C, radius=0.05, color=ESCURO))
nota_feixe = texto("o feixe sai da tela", 30, SUAVE).move_to(C + DOWN * 0.6)
referencia = DashedLine(C, C + RIGHT * raio, color=CINZA, stroke_width=2.5)
frente = texto("de frente", 30, SUAVE).move_to(np.array([3.7, -2.85, 0]))
def seta_t(f):
return Arrow(C, C + comprimento * unitario(math.degrees(f)), buff=0, color=LARANJA, stroke_width=7,
max_tip_length_to_length_ratio=0.14, max_stroke_width_to_length_ratio=10)
def arco_fi(f):
return Arc(radius=0.65, start_angle=0, angle=f, arc_center=C, color=SUAVE, stroke_width=3)
rot_fi = eq("\\phi", cor=SUAVE)
rot_fi.add_updater(lambda m: m.move_to(C + 1.0 * unitario(math.degrees(fi.get_value() / 2))))
rot_pt = eq("p_T", cor=LARANJA)
rot_pt.add_updater(lambda m: m.move_to(C + 0.95 * unitario(math.degrees(fi.get_value()))
+ 0.42 * unitario(math.degrees(fi.get_value()) + 90)))
leitura_fi = VGroup(eq("\\phi ="), Numero(0.9, num_decimal_places=2, font_size=40, color=ESCURO),
texto("rad")).arrange(RIGHT, buff=0.18).move_to(np.array([3.7, -3.5, 0]))
leitura_fi[1].add_updater(lambda m: m.set_value(fi.get_value()))
leg = troca(self, leg, "De frente, ao longo do feixe: o ângulo em volta dele é o", "$\\phi$")
self.play(Create(detector), Create(feixe_frente), Create(referencia), Write(nota_feixe), Write(frente),
run_time=1.6)
seta = seta_t(0.9)
arco2 = arco_fi(0.9)
self.play(GrowArrow(seta), Create(arco2), run_time=1.0)
self.play(Write(rot_fi), Write(rot_pt), Write(leitura_fi), run_time=1.0)
self.remove(seta, arco2)
seta = always_redraw(lambda: seta_t(fi.get_value()))
arco2 = always_redraw(lambda: arco_fi(fi.get_value()))
self.add(seta, arco2)
self.play(fi.animate.set_value(2.6), run_time=3.5)
self.wait(0.8)
leg = troca(self, leg, "O tamanho", "$p_T$", "e os ângulos", "$\\eta$", "e", "$\\phi$",
"dão o momento do múon")
self.wait(3)
# ================================================================ 2. a massa invariante
P_ESTRELA = math.sqrt(MASSA_JPSI ** 2 / 4 - MASSA_DO_MUON ** 2) # o momento de cada múon com o J/ψ parado
def muons(P, angulo):
"""A energia e o momento (px, py) dos dois múons de um J/ψ que anda com momento P ao longo de x.
Com o J/ψ parado, os dois múons saem em sentidos opostos, cada um com a metade
da energia, M / 2, e momento P_ESTRELA, no ângulo pedido, em graus. Para quem
vê o J/ψ andar, vale a transformação de Lorentz ao longo de x, com
γ = E_J / M e βγ = P / M:
E' = γ E + βγ p_x p_x' = βγ E + γ p_x p_y' = p_y
Devolve [(E1, px1, py1), (E2, px2, py2)], em GeV.
"""
gama, beta_gama = math.hypot(P, MASSA_JPSI) / MASSA_JPSI, P / MASSA_JPSI
a, E = math.radians(angulo), MASSA_JPSI / 2
saida = []
for sinal in (1, -1):
px, py = sinal * P_ESTRELA * math.cos(a), sinal * P_ESTRELA * math.sin(a)
saida.append((gama * E + beta_gama * px, beta_gama * E + gama * px, py))
return saida
class MassaInvariante(Scene):
"""Um J/ψ se desfaz em dois múons. Ele anda, os múons saem em outro ângulo, e a massa dos dois é sempre a mesma."""
def construct(self):
self.camera.background_color = FUNDO
O, escala = np.array([-5.6, -0.3, 0.0]), 0.58 # o ponto do decaimento, e unidades da tela por GeV
P, angulo = ValueTracker(0.0), ValueTracker(60.0)
def estado():
return muons(P.get_value(), angulo.get_value())
def ponta(i):
_, px, py = estado()[i]
return O + escala * np.array([px, py, 0.0])
def seta(i, cor):
return Arrow(O, ponta(i), buff=0, color=cor, stroke_width=7, max_stroke_width_to_length_ratio=12)
def soma():
(E1, px1, py1), (E2, px2, py2) = estado()
return E1 + E2, math.hypot(px1 + px2, py1 + py2)
jpsi = Dot(O, radius=0.13, color=ESCURO)
nome_jpsi = eq(JPSI).next_to(jpsi, LEFT, buff=0.25)
leg = legenda("Um", f"${JPSI}$", "parado se desfaz em dois múons")
self.play(Write(leg), GrowFromCenter(jpsi), Write(nome_jpsi), run_time=1.4)
self.wait(1.0)
seta1, seta2 = seta(0, AZUL), seta(1, LARANJA)
rot1, rot2 = eq("\\mu^-", cor=AZUL), eq("\\mu^+", cor=LARANJA)
def segue(rotulo, i):
d = ponta(i) - O
rotulo.move_to(ponta(i) + 0.42 * d / max(np.linalg.norm(d), 1e-9))
segue(rot1, 0)
segue(rot2, 1)
self.play(FadeOut(jpsi, nome_jpsi), GrowArrow(seta1), GrowArrow(seta2), run_time=1.2)
self.play(Write(rot1), Write(rot2), run_time=0.7)
# a leitura: as duas energias, o tamanho da soma dos momentos e a massa
linhas = VGroup()
for i, (nome, cor) in enumerate((("E_1 =", AZUL), ("E_2 =", LARANJA), ("|\\vec{p}_1 + \\vec{p}_2| =", ESCURO))):
valor = Numero(0, num_decimal_places=2, font_size=44, color=cor)
linha = VGroup(eq(nome, tamanho=44, cor=cor), valor, texto("GeV")).arrange(RIGHT, buff=0.2)
linha.move_to(np.array([0, 2.0 - 0.95 * i, 0])).align_to(np.array([0.9, 0, 0]), LEFT)
linhas.add(linha)
linhas[0][1].add_updater(lambda m: m.set_value(estado()[0][0]))
linhas[1][1].add_updater(lambda m: m.set_value(estado()[1][0]))
linhas[2][1].add_updater(lambda m: m.set_value(soma()[1]))
massa = VGroup(eq("M =", tamanho=60), Numero(MASSA_JPSI, num_decimal_places=3, font_size=60, color=ESCURO),
texto("GeV", 36)).arrange(RIGHT, buff=0.22)
massa.move_to(np.array([0, -1.15, 0])).align_to(np.array([0.9, 0, 0]), LEFT)
massa[1].add_updater(lambda m: m.set_value(math.sqrt(soma()[0] ** 2 - soma()[1] ** 2)))
caixa = SurroundingRectangle(massa, color=LARANJA, buff=0.22, stroke_width=4)
formula = eq("M^2 = (E_1 + E_2)^2 - |\\vec{p}_1 + \\vec{p}_2|^2", tamanho=40, cor=SUAVE)
formula.next_to(caixa, DOWN, buff=0.45).align_to(caixa, LEFT)
self.play(Write(linhas), run_time=1.4)
self.play(Write(massa), Create(caixa), run_time=1.0)
self.play(Write(formula), run_time=1.0)
self.wait(1.5)
self.remove(seta1, seta2)
seta1, seta2 = always_redraw(lambda: seta(0, AZUL)), always_redraw(lambda: seta(1, LARANJA))
self.add(seta1, seta2)
rot1.add_updater(lambda m: segue(m, 0))
rot2.add_updater(lambda m: segue(m, 1))
leg = troca(self, leg, "Quando o", f"${JPSI}$", "anda, a energia e o momento de cada múon mudam")
self.play(P.animate.set_value(9.0), run_time=4.5)
self.play(Indicate(massa[1], color=LARANJA, scale_factor=1.15), run_time=1.2)
self.wait(0.8)
leg = troca(self, leg, "Os múons saem em outro ângulo, e a massa é a mesma")
self.play(angulo.animate.set_value(150), run_time=3.5)
self.wait(1.0)
leg = troca(self, leg, "Todo", f"${JPSI}$", "dá a mesma massa: por isso o pico do histograma")
self.play(P.animate.set_value(4.0), angulo.animate.set_value(100), run_time=2.5)
self.wait(3)
# ================================================================ 3. do evento ao histograma
ETAPAS = [500, 2000, 10000, 30000, 100000]
PICOS = [(JPSI, 3.0969), ("\\Upsilon", 9.46), ("\\mathrm{Z}", 91.188)]
DEVAGAR = [2, 47, 2102] # o primeiro evento do arquivo perto do J/ψ, o primeiro perto do Υ e o primeiro perto do Z
DEPRESSA = 40 # depois, os 40 primeiros eventos do arquivo caem um a um
class Picos(Scene):
"""Cada evento vira uma massa, e a massa cai na sua faixa; com 100 mil eventos, os picos são partículas."""
def construct(self):
self.camera.background_color = FUNDO
c = carrega()
M = c["M"]
bordas = np.logspace(math.log10(2), math.log10(110), 151)
canto, largura = np.array([-4.8, -2.3, 0.0]), 10.9
altura_eventos, altura_final = 3.8, 4.9 # a moldura cresce quando os quadrados dão lugar às barras
ymax = 5.0 # log10 do topo do eixo: 100 mil por faixa, para os nomes caberem acima dos picos
lado = 0.14 # o lado do quadrado de um evento
def x(m):
return canto[0] + (math.log10(m) - math.log10(2)) / (math.log10(110) - math.log10(2)) * largura
def y(contagem):
return canto[1] + (math.log10(contagem) / ymax) * altura_final if contagem >= 1 else canto[1]
def barras(n_eventos):
n, _ = np.histogram(M[:n_eventos], bins=bordas)
g = VGroup()
for k, cont in enumerate(n):
x0, x1 = x(bordas[k]), x(bordas[k + 1])
h = max(y(cont) - canto[1], 0.001)
g.add(Rectangle(width=x1 - x0, height=h, stroke_width=1.2, stroke_color=AZUL, fill_color=CLARO,
fill_opacity=1).move_to(np.array([(x0 + x1) / 2, canto[1] + h / 2, 0])))
return g, n
def moldura(altura):
r = Rectangle(width=largura, height=altura, color=ESCURO, stroke_width=2.5)
return r.move_to(canto + np.array([largura / 2, altura / 2, 0]))
quadro = moldura(altura_eventos)
marcas_x = VGroup()
for v in (2, 5, 10, 20, 50, 100):
b = np.array([x(v), canto[1], 0])
marcas_x.add(Line(b, b + UP * 0.12, color=ESCURO, stroke_width=2.5),
eq(str(v), tamanho=38, cor=TEXTO).next_to(b, DOWN, buff=0.14))
nome_x = texto("massa invariante dos dois múons, em GeV").next_to(quadro, DOWN, buff=0.7)
# as pilhas de quadrados: cada coluna tem a largura de um quadrado, e o quadrado cai em cima dos que já estão lá
pilhas = {}
def destino(m):
col = int((x(m) - canto[0]) // lado)
altura_pilha = pilhas.get(col, 0)
pilhas[col] = altura_pilha + 1
return np.array([canto[0] + (col + 0.5) * lado, canto[1] + (altura_pilha + 0.5) * lado, 0])
def quadrado(posicao):
return Square(side_length=lado * 0.9, stroke_width=0, fill_color=AZUL, fill_opacity=0.85).move_to(posicao)
leg = legenda("Um evento: dois múons, uma massa")
self.play(Write(leg), Create(quadro), Create(marcas_x[0::2]), Write(marcas_x[1::2]), Write(nome_x),
run_time=1.6)
quadrados = VGroup()
for k in DEVAGAR:
# Os quadrados, como no slide da conta à mão: a massa sai da diferença de dois números grandes, e
# arredondar as somas antes de elevar ao quadrado mudaria a segunda casa da massa. Com os quadrados
# em duas casas, a conta na tela confere com a coluna M até a terceira casa.
S2 = round((c["E1"][k] + c["E2"][k]) ** 2, 2)
P2 = round((c["px1"][k] + c["px2"][k]) ** 2 + (c["py1"][k] + c["py2"][k]) ** 2
+ (c["pz1"][k] + c["pz2"][k]) ** 2, 2)
m_tela = math.sqrt(S2 - P2) # a conta com os números que aparecem na tela
assert abs(m_tela - M[k]) < 0.0015, (k, m_tela, M[k])
def quadrado_tex(v):
"""O quadrado com duas casas e ponto de milhar, para dentro de uma equação: 24{.}401{,}56."""
return f"{v:,.2f}".replace(",", "X").replace(".", "{,}").replace("X", "{.}")
linha1 = frase(f"Evento {mil(k + 1)}:", f"$(E_1 + E_2)^2 = {quadrado_tex(S2)}$", "e",
f"$|\\vec{{p}}_1 + \\vec{{p}}_2|^2 = {quadrado_tex(P2)}$").move_to(np.array([0, 2.7, 0]))
linha2 = eq(f"M = \\sqrt{{{quadrado_tex(S2)} - {quadrado_tex(P2)}}} =", br(m_tela, 3), "\\ \\text{GeV}",
tamanho=44)
linha2.move_to(np.array([0, 1.95, 0]))
assert linha1.width < 13.5 and linha2.width < 13.5
q = quadrado(np.array([x(M[k]), canto[1] + altura_eventos - 0.25, 0]))
self.play(Write(linha1), run_time=1.2)
self.play(Write(linha2), run_time=1.2)
self.wait(0.6)
self.play(TransformFromCopy(linha2[1], q), run_time=0.8)
self.play(q.animate.move_to(destino(M[k])), run_time=0.7)
quadrados.add(q)
self.wait(0.4)
self.play(FadeOut(linha1, linha2), run_time=0.4)
leg = troca(self, leg, "Cada quadrado é um evento, na faixa da sua massa")
caindo = []
for k in [k for k in range(DEPRESSA) if k not in DEVAGAR]:
q = quadrado(np.array([x(M[k]), canto[1] + altura_eventos - 0.25, 0]))
caindo.append(Succession(FadeIn(q, run_time=0.15), q.animate(run_time=0.45).move_to(destino(M[k]))))
quadrados.add(q)
self.play(LaggedStart(*caindo, lag_ratio=0.12), run_time=6)
self.wait(1.2)
# milhares de eventos: os quadrados dão lugar às barras, e o eixo vertical passa a contar em potências de dez
marcas_y = VGroup()
for cont, s in ((10, "10"), (100, "100"), (1000, "1.000"), (10000, "10.000")):
b = np.array([canto[0], y(cont), 0])
marcas_y.add(Line(b, b + RIGHT * 0.12, color=ESCURO, stroke_width=2.5),
eq(s.replace(".", "{.}"), tamanho=38, cor=TEXTO).next_to(b, LEFT, buff=0.14))
quadro_final = moldura(altura_final)
nome_y = texto("eventos por faixa").rotate(math.pi / 2).next_to(VGroup(quadro_final, marcas_y), LEFT,
buff=0.3)
leg = troca(self, leg, "Com milhares de eventos, cada marca vale dez vezes a de baixo")
self.play(FadeOut(quadrados), Transform(quadro, quadro_final), run_time=1.2)
self.play(Create(marcas_y[0::2]), Write(marcas_y[1::2]), Write(nome_y), run_time=1.2)
atual, _ = barras(ETAPAS[0])
leg_n = legenda(f"{mil(ETAPAS[0])} eventos")
self.play(FadeOut(leg), run_time=0.35)
leg = leg_n
self.play(Write(leg), Create(atual), run_time=1.4)
self.wait(1.0)
n_final = None
for n_ev in ETAPAS[1:]:
novas, n_final = barras(n_ev)
fim = n_ev == ETAPAS[-1]
nova_leg = legenda(f"{mil(n_ev)} eventos" + (": os picos são partículas" if fim else ""))
self.play(FadeOut(leg), run_time=0.3)
leg = nova_leg
self.play(Write(leg), Transform(atual, novas), run_time=1.6)
self.wait(0.9)
# os nomes dos três picos, acima de cada um, com uma linha fina até ele
nomes = VGroup()
for nome, m in PICOS:
k = np.searchsorted(bordas, m) - 1
topo = y(n_final[max(k - 2, 0):k + 3].max())
base = np.array([x(m), topo + 0.1, 0])
ponta_ = base + UP * 0.55
rotulo = eq(f"{nome},\\ {m:g}".replace(".", "{,}") + "\\ \\text{GeV}", tamanho=38, cor=LARANJA)
if m > 50:
rotulo.next_to(ponta_, UP, buff=0.08).align_to(ponta_, RIGHT)
else:
rotulo.next_to(ponta_, UP, buff=0.08)
if rotulo.get_left()[0] < canto[0] + 0.15: # o nome do J/ψ não passa da borda esquerda
rotulo.align_to(np.array([canto[0] + 0.15, 0, 0]), LEFT)
nomes.add(Line(base, ponta_, color=LARANJA, stroke_width=3), rotulo)
self.play(Create(nomes[0::2]), Write(nomes[1::2]), run_time=1.4)
self.wait(3)
# ================================================================ 4. a compressão das entradas
N_COMPRESSAO = 300 # os 300 primeiros eventos do arquivo
TOPO_GEV = 350 # o fim da linha de cima, em GeV; a maior energia dos 300 é 322,8
TOPO_ASINH = 5.0 # o fim da linha de baixo
class Compressao(Scene):
"""A energia do múon 1 em 300 eventos, antes e depois de asinh(E / 5): os números pequenos se espalham."""
def construct(self):
self.camera.background_color = FUNDO
E = carrega()["E1"][:N_COMPRESSAO]
mediana = float(np.median(E))
x0, x1, ya, yb = -5.5, 5.5, 1.2, -1.6 # as duas linhas: a de cima em GeV, a de baixo depois do asinh
def xa(v):
return x0 + v / TOPO_GEV * (x1 - x0)
def xb(v):
return x0 + v / TOPO_ASINH * (x1 - x0)
def linha(yy, marcas, para_cima, f):
"""A linha, as marcas e os números; os números ficam do lado de fora, longe do corredor do meio."""
g, numeros = VGroup(Line(np.array([x0, yy, 0]), np.array([x1, yy, 0]), color=ESCURO, stroke_width=3)), VGroup()
sentido = UP if para_cima else DOWN
for v in marcas:
b = np.array([f(v), yy, 0])
g.add(Line(b, b + sentido * 0.12, color=ESCURO, stroke_width=2.5))
numeros.add(eq(str(v), tamanho=38, cor=TEXTO).move_to(b + sentido * 0.5))
return g, numeros
# um sobe-e-desce fixo, só para os pontos iguais não ficarem um em cima do outro
desvio = [((k * 37) % 11 - 5) * 0.026 for k in range(len(E))]
linha_a, numeros_a = linha(ya, range(0, TOPO_GEV + 1, 50), True, xa)
nome_a = frase("$E_1,$", "a energia do múon 1, em GeV").next_to(numeros_a, UP, buff=0.3).align_to(
np.array([x0, 0, 0]), LEFT)
pontos = VGroup(*[Dot(np.array([xa(e), ya + d, 0]), radius=0.06, color=AZUL, fill_opacity=0.45)
for e, d in zip(E, desvio)])
leg = legenda(f"A energia do múon 1 em {N_COMPRESSAO} eventos: quase todas pequenas")
self.play(Write(leg), Create(linha_a), Write(numeros_a), Write(nome_a), run_time=1.6)
self.play(FadeIn(pontos, lag_ratio=0.01), run_time=1.6)
chave_a = Brace(Line(np.array([xa(0), ya - 0.2, 0]), np.array([xa(mediana), ya - 0.2, 0])), DOWN,
color=LARANJA, buff=0.05)
nota_a = texto(f"metade dos eventos: abaixo de {mediana:.1f} GeV".replace(".", ","), 30, LARANJA)
nota_a.next_to(chave_a, DOWN, buff=0.12).align_to(np.array([x0, 0, 0]), LEFT)
self.play(Create(chave_a), Write(nota_a), run_time=1.0)
self.wait(2.0)
# o asinh: cada ponto desce para a sua posição na linha de baixo; a sombra fica em cima, para comparar
linha_b, numeros_b = linha(yb, range(0, int(TOPO_ASINH) + 1), False, xb)
nome_b = eq("\\operatorname{asinh}(E_1 / 5)").next_to(numeros_b, DOWN, buff=0.25).align_to(
np.array([x0, 0, 0]), LEFT)
leg = troca(self, leg, "O", "$\\operatorname{asinh}(E_1 / 5)$", "leva cada número para a linha de baixo")
self.play(FadeOut(chave_a, nota_a), run_time=0.5)
self.play(Create(linha_b), Write(numeros_b), Write(nome_b), run_time=1.4)
sombra = pontos.copy().set_fill(opacity=0.12)
self.add(sombra)
self.play(*[p.animate.move_to(np.array([xb(math.asinh(e / 5)), yb + d, 0]))
for p, e, d in zip(pontos, E, desvio)], run_time=3.0)
self.wait(0.8)
meio = math.asinh(mediana / 5)
antes, depois = round(100 * mediana / TOPO_GEV), round(100 * meio / TOPO_ASINH)
chave_b = Brace(Line(np.array([xb(0), yb + 0.2, 0]), np.array([xb(meio), yb + 0.2, 0])), UP,
color=LARANJA, buff=0.05)
nota_b = texto("a mesma metade dos eventos", 30, LARANJA).next_to(chave_b, UP, buff=0.12)
leg = troca(self, leg, f"A metade de baixo ocupava {antes}% da linha; agora ocupa {depois}%")
self.play(Create(chave_b), Write(nota_b), run_time=1.0)
self.wait(2.2)
# o leque: de 10 em 10 GeV até 50, e de 50 em 50 até 350, cada valor ligado à sua imagem
valores = [10, 20, 30, 40, 50, 100, 150, 200, 250, 300, 350]
leque = VGroup(*[Line(np.array([xa(v), ya - 0.25, 0]), np.array([xb(math.asinh(v / 5)), yb + 0.25, 0]),
color=CINZA, stroke_width=2.5) for v in valores])
parte = round(100 * math.asinh(50 / 5) / TOPO_ASINH)
leg = troca(self, leg, f"Os primeiros 50 GeV ganham {parte}% da linha; os outros 300, o resto")
self.play(FadeOut(chave_b, nota_b), run_time=0.5)
self.play(Create(leque, lag_ratio=0.15), run_time=2.0)
self.wait(3)
# ================================================================ 5. o treino, época a época
N_TREINO = 2000 # os 2 mil primeiros eventos do teste, os mesmos que mlp_massa.py retrata
class TreinoMassa(Scene):
"""A massa da rede contra a da fórmula, das épocas 0 a 80: a nuvem de pontos vai para a diagonal."""
def construct(self):
self.camera.background_color = FUNDO
r = json.loads((AULA / "resultados" / "massa.json").read_text(encoding="utf-8"))
formula = np.array(r["teste_formula"][:N_TREINO])
instantes = {int(e): np.array(m) for e, m in r["instantes"].items()}
canto, lado = np.array([-4.4, -2.6, 0.0]), 5.4
lim = (math.log10(1.5), math.log10(150))
def frac(m):
return (math.log10(min(max(m, 1.5), 150)) - lim[0]) / (lim[1] - lim[0])
def pos(mx, my):
return canto + np.array([frac(mx) * lado, frac(my) * lado, 0])
quadro = Rectangle(width=lado, height=lado, color=ESCURO, stroke_width=2.5).move_to(
canto + np.array([lado / 2, lado / 2, 0]))
marcas, numeros = VGroup(), VGroup()
for v in (2, 5, 10, 20, 50, 100):
bx, by = pos(v, 1.5), pos(1.5, v)
marcas.add(Line(bx, bx + UP * 0.12, color=ESCURO, stroke_width=2.5),
Line(by, by + RIGHT * 0.12, color=ESCURO, stroke_width=2.5))
numeros.add(eq(str(v), tamanho=38, cor=TEXTO).next_to(bx, DOWN, buff=0.14),
eq(str(v), tamanho=38, cor=TEXTO).next_to(by, LEFT, buff=0.14))
nome_x = texto("massa pela fórmula, em GeV").next_to(quadro, DOWN, buff=0.7)
nome_y = texto("massa da rede, em GeV").rotate(math.pi / 2).next_to(quadro, LEFT, buff=0.95)
diagonal = DashedLine(pos(1.5, 1.5), pos(150, 150), color=SUAVE, stroke_width=3)
nome_diag = texto("rede = fórmula", 30, SUAVE).next_to(pos(150, 150), RIGHT, buff=0.25)
def nuvem(e):
return VGroup(*[Dot(pos(mx, my), radius=0.028, color=AZUL, fill_opacity=0.5)
for mx, my in zip(formula, instantes[e])])
def erro(e):
return float(np.median(np.abs(instantes[e] - formula) / formula))
x_painel = 2.4
rot_epoca = texto("época", 30, SUAVE).move_to(np.array([0, 1.9, 0])).align_to(np.array([x_painel, 0, 0]), LEFT)
rot_erro = texto("erro mediano", 30, SUAVE).move_to(np.array([0, -0.3, 0])).align_to(
np.array([x_painel, 0, 0]), LEFT)
def num_epoca(e):
return eq(str(e), tamanho=72).next_to(rot_epoca, DOWN, buff=0.3).align_to(rot_epoca, LEFT)
def num_erro(e):
return eq(br(100 * erro(e), 1) + "\\%", tamanho=64).next_to(rot_erro, DOWN, buff=0.3).align_to(rot_erro, LEFT)
leg = legenda(f"{mil(N_TREINO)} eventos do teste: a massa da rede contra a da fórmula")
self.play(Write(leg), Create(quadro), Create(marcas), Write(numeros), Write(nome_x), Write(nome_y),
run_time=1.8)
self.play(Create(diagonal), Write(nome_diag), run_time=1.0)
pontos, ep, er = nuvem(0), num_epoca(0), num_erro(0)
self.play(FadeIn(pontos, lag_ratio=0.002), Write(rot_epoca), Write(ep), Write(rot_erro), Write(er),
run_time=1.6)
leg = troca(self, leg, "Antes do treino, a rede dá quase a mesma massa a todos")
self.wait(1.8)
frases = {1: ("Depois de uma época, os pontos já seguem a diagonal",),
10: ("A cada época, a nuvem aperta em volta da diagonal",),
80: ("Época 80, a melhor pela validação: os picos viram manchas",)}
for e in sorted(instantes)[1:]:
if e in frases:
leg = troca(self, leg, *frases[e])
self.play(Transform(pontos, nuvem(e)), Transform(ep, num_epoca(e)), Transform(er, num_erro(e)),
run_time=1.6)
self.wait(1.0 if e < 80 else 3.0)
CENAS = {"direcao": Direcao, "massa-invariante": MassaInvariante, "picos": Picos, "compressao": Compressao,
"treino-massa": TreinoMassa}
def grava(nome, largura=1920, altura=1080, quadros=30, crf=24, so_ultimo_quadro=False):
"""Desenha a cena e grava o mp4 e o png do último quadro, em videos/ e figuras/ da aula."""
inicio = time.perf_counter()
pasta_videos, pasta_quadros = AULA / "videos", AULA / "figuras"
pasta_videos.mkdir(parents=True, exist_ok=True)
pasta_quadros.mkdir(parents=True, exist_ok=True)
trabalho = pasta_videos / f"_manim-{nome}"
opcoes = {"pixel_width": largura, "pixel_height": altura, "frame_rate": quadros, "media_dir": str(trabalho),
"output_file": nome, "background_color": FUNDO, "disable_caching": True, "progress_bar": "none",
"verbosity": "WARNING", "tex_template": MODELO}
if so_ultimo_quadro:
opcoes.update(save_last_frame=True, write_to_movie=False)
with tempconfig(opcoes):
cena = CENAS[nome]()
cena.render()
if so_ultimo_quadro:
png_manim = Path(cena.renderer.file_writer.image_file_path)
else:
mp4 = Path(cena.renderer.file_writer.movie_file_path)
png = pasta_quadros / f"{nome}-quadro.png"
if so_ultimo_quadro:
shutil.copyfile(png_manim, png)
arquivos = [png]
else:
destino = pasta_videos / f"{nome}.mp4"
subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", str(mp4), "-c:v", "libx264", "-crf", str(crf),
"-preset", "slow", "-pix_fmt", "yuv420p", "-movflags", "+faststart", "-an", str(destino)],
check=True)
subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-sseof", "-0.5", "-i", str(destino),
"-frames:v", "1", "-update", "1", str(png)], check=True)
arquivos = [destino, png]
shutil.rmtree(trabalho, ignore_errors=True)
for arquivo in arquivos:
print(f"gravado: {arquivo} ({arquivo.stat().st_size / 1e6:.1f} MB)")
print(f"{nome}: {time.perf_counter() - inicio:.0f} s")
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
args = sys.argv[1:]
so_quadro = "--quadros" in args
for nome in [a for a in args if a != "--quadros"] or list(CENAS):
grava(nome, so_ultimo_quadro=so_quadro)
dimuon.py
Os dados do CMS: os 100 mil eventos com dois múons do registro 303, e a fórmula da massa invariante.
92 linesDownload
"""Os dados do CMS: os 100 mil eventos com dois múons do registro 303, e a fórmula da massa invariante.
Este arquivo vai para a turma: ele só lê os dados e calcula a fórmula. Antes,
rode uma vez, a partir da raiz do repositório:
python infra/baixar_dados.py
Rodado sozinho, confere a fórmula contra a coluna M do arquivo:
python aulas/08-dados-do-cern/codigo/dimuon.py
As colunas, como a página do registro 303 as descreve: Type, o tipo de cada
múon, T de tracker muon e G de global muon; Run, a corrida; Event, o evento; e,
para cada múon, 1 e 2, E, a energia em GeV; px, py e pz, as componentes do
momento em GeV; pt, o momento transversal; eta, a pseudorrapidez; phi, o ângulo
phi em radianos; Q, a carga. M é a massa invariante dos dois múons, em GeV.
"""
import csv
import sys
from pathlib import Path
import numpy as np
ARQUIVO = Path(__file__).resolve().parents[3] / "dados" / "dimuon.csv"
def carrega():
"""Lê o dimuon.csv e devolve um dicionário: o nome de cada coluna e um array com os 100 mil valores.
O cabeçalho do arquivo tem um espaço sobrando em "px1 ", que o strip() tira.
A coluna Type é texto, como "GT" ou "GG", e fica como lista; as outras viram
números.
"""
if not ARQUIVO.exists():
sys.exit("falta dados/dimuon.csv. Rode antes: python infra/baixar_dados.py")
with open(ARQUIVO, newline="", encoding="utf-8") as f:
linhas = list(csv.reader(f))
nomes = [n.strip() for n in linhas[0]]
colunas = {}
for k, nome in enumerate(nomes):
valores = [linha[k] for linha in linhas[1:]]
colunas[nome] = valores if nome == "Type" else np.array(valores, dtype=np.float64)
return colunas
def massa_invariante(E1, px1, py1, pz1, E2, px2, py2, pz2):
"""A massa invariante dos dois múons, em GeV, pela fórmula da relatividade especial.
Com a energia em GeV e o momento em GeV, nas unidades em que a velocidade da
luz vale 1, M² = (E₁ + E₂)² − |p⃗₁ + p⃗₂|²: a energia total ao quadrado menos o
momento total ao quadrado. É a massa da partícula que se desfez nos dois
múons, se foi isso que aconteceu. O np.maximum evita a raiz de um número
negativo, que o arredondamento pode produzir quando M é muito pequena.
"""
m2 = (E1 + E2) ** 2 - (px1 + px2) ** 2 - (py1 + py2) ** 2 - (pz1 + pz2) ** 2
return np.sqrt(np.maximum(m2, 0.0))
MASSA_DO_MUON = 0.1056583755 # GeV; o verbete "Muon" da Wikipedia dá 105,66 MeV
def massa_das_colunas(c):
"""A fórmula aplicada à energia e ao momento do arquivo, E, px, py e pz de cada múon."""
return massa_invariante(c["E1"], c["px1"], c["py1"], c["pz1"], c["E2"], c["px2"], c["py2"], c["pz2"])
def massa_pela_direcao(c):
"""A fórmula com o momento refeito de pt, eta e phi, e a energia de p e da massa do múon.
px = pt cos φ, py = pt sen φ, pz = pt senh η, e E = √(p² + m²). É assim que a
coluna M do arquivo bate em todos os eventos; com E, px, py e pz do arquivo,
bate em 99,6% deles, porque em 0,4% a energia de um múon não combina com o
momento dele. Isso é dedução, tirada dos próprios dados: a página do
registro não diz como M foi calculada.
"""
partes = []
for k in ("1", "2"):
pt, eta, phi = c["pt" + k], c["eta" + k], c["phi" + k]
px, py, pz = pt * np.cos(phi), pt * np.sin(phi), pt * np.sinh(eta)
partes += [np.sqrt(px ** 2 + py ** 2 + pz ** 2 + MASSA_DO_MUON ** 2), px, py, pz]
return massa_invariante(*partes)
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
c = carrega()
print(f"{len(c['M'])} eventos; colunas: {', '.join(c)}")
print(f"massa de {c['M'].min():.2f} a {c['M'].max():.2f} GeV")
for nome, M in (("com E e p do arquivo", massa_das_colunas(c)), ("com pt, eta e phi", massa_pela_direcao(c))):
rel = np.abs(M - c["M"]) / c["M"]
print(f"fórmula {nome}: diferença relativa mediana {np.median(rel):.1e}; "
f"eventos com mais de 0,1%: {(rel > 1e-3).sum()}")
tipos, contagem = np.unique(c["Type"], return_counts=True)
print("tipos:", dict(zip(tipos.tolist(), contagem.tolist())))
figuras_slides.py
Figuras dos slides da aula 8: os dados do CERN.
268 linesDownload
"""Figuras dos slides da aula 8: os dados do CERN.
Rode, a partir da raiz do repositório, depois de mlp_massa.py:
python aulas/08-dados-do-cern/codigo/figuras_slides.py
Gera, a 200 dpi, em aulas/08-dados-do-cern/figuras/:
histograma.png a massa invariante dos 100 mil eventos, com os três picos marcados
rede.png a massa da rede contra a da fórmula, nos 10 mil eventos do teste,
e os dois histogramas, um sobre o outro
perto.png os picos do J/ψ e do Υ de perto, em escala linear, com a largura
do pico do J/ψ à meia altura
curva.png o erro mediano no treino e na validação, época a época, com a
melhor época marcada
onde-erra.png o pico do J/ψ pela fórmula e pela rede, no teste, e o erro
mediano da rede em cada faixa de massa
Os números vêm de dimuon.py e de resultados/massa.json. As massas das partículas
marcadas são as da Wikipedia, que cita o Particle Data Group: J/ψ, 3,0969 GeV;
Υ, 9,46 GeV; Z, 91,1880 GeV.
"""
import json
import sys
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import FuncFormatter, NullFormatter
sys.path.insert(0, str(Path(__file__).resolve().parent))
from dimuon import carrega # noqa: E402
plt.rcParams["mathtext.fontset"] = "stix"
AULA = Path(__file__).resolve().parents[1]
AZUL, LARANJA, ESCURO, CINZA = "#1f5fad", "#a8500f", "#14213d", "#8a94a6"
SUAVE = "#5e6a7d"
VIRGULA = FuncFormatter(lambda v, _: f"{v:g}".replace(".", ",").replace("-", "−"))
MILHAR = FuncFormatter(lambda v, _: f"{v:,.0f}".replace(",", "."))
PICOS = [("J/ψ", 3.0969), ("Υ", 9.46), ("Z", 91.188)]
BORDAS = np.logspace(np.log10(2), np.log10(110), 301) # 300 faixas, iguais em escala logarítmica
def salva(fig, nome):
"""Salva a 200 dpi, recortada em volta do que foi desenhado."""
saida = AULA / "figuras" / nome
fig.savefig(saida, dpi=200, bbox_inches="tight", pad_inches=0.2)
plt.close(fig)
print(f"salva: {saida}")
def eixo_massa(ax):
"""O eixo da massa em escala logarítmica, com as marcas 2, 5, 10, 20, 50 e 100 GeV."""
ax.set_xscale("log")
ax.set_xlim(2, 110)
ax.set_xticks([2, 5, 10, 20, 50, 100])
ax.xaxis.set_major_formatter(VIRGULA)
ax.xaxis.set_minor_formatter(NullFormatter())
ax.set_xlabel("massa invariante dos dois múons, em GeV", fontsize=18)
def histograma(M):
"""Quantos eventos caem em cada faixa de massa, com os três picos marcados por cima.
As faixas são iguais em escala logarítmica, para os picos de 3 e de 91 GeV
terem a mesma largura relativa. O eixo vertical também é logarítmico: o fundo
cai de milhares para dezenas de eventos por faixa. Cada nome fica acima do
pico, longe dos dados, com uma linha fina até ele.
"""
fig, ax = plt.subplots(figsize=(11, 5.6))
n, _, _ = ax.hist(M, bins=BORDAS, histtype="stepfilled", color="#c9d7ea", edgecolor=AZUL, lw=1.4)
ax.set_yscale("log")
ax.set_ylim(3, 1.2e5) # espaço em cima para os nomes, fora dos dados
eixo_massa(ax)
ax.yaxis.set_major_formatter(MILHAR)
ax.yaxis.set_minor_formatter(NullFormatter())
ax.tick_params(labelsize=16)
ax.set_ylabel("eventos por faixa", fontsize=18)
for nome, m in PICOS:
k = np.searchsorted(BORDAS, m) - 1
topo = n[max(k - 2, 0):k + 3].max()
# o nome do Z, perto da borda direita, fica à esquerda da linha, para não sair do gráfico
ax.annotate(f"{nome}, {m:g} GeV".replace(".", ","), xy=(m, topo * 1.15), xytext=(m, topo * 4.5),
ha="right" if m > 50 else "center", fontsize=16, color=LARANJA,
arrowprops={"arrowstyle": "-", "color": LARANJA, "lw": 1.4})
salva(fig, "histograma.png")
def rede(resultado):
"""A rede contra a fórmula: à esquerda, um ponto por evento do teste; à direita, os dois histogramas.
Na esquerda, a diagonal cinza tracejada é onde a rede acerta em cheio; os
pontos azuis ficam em volta dela. Na direita, a fórmula em azul cheio e a
rede em laranja, só o contorno: os picos aparecem nos dois, mais largos na
rede.
"""
m_f, m_r = np.array(resultado["teste_formula"]), np.array(resultado["teste_rede"])
fig, (esq, dir_) = plt.subplots(1, 2, figsize=(12, 5.2), gridspec_kw={"wspace": 0.32})
esq.plot(m_f, m_r, "o", ms=2.2, color=AZUL, alpha=0.35, mew=0)
esq.plot([2, 110], [2, 110], color=CINZA, lw=1.6, ls=(0, (4, 3)))
eixo_massa(esq)
esq.set_xlabel("massa pela fórmula, GeV", fontsize=17)
esq.set_yscale("log")
esq.set_ylim(2, 110)
esq.set_yticks([2, 5, 10, 20, 50, 100])
esq.yaxis.set_major_formatter(VIRGULA)
esq.yaxis.set_minor_formatter(NullFormatter())
esq.set_ylabel("massa pela rede, GeV", fontsize=17)
esq.set_aspect("equal")
bordas = np.logspace(np.log10(2), np.log10(110), 121)
dir_.hist(m_f, bins=bordas, histtype="stepfilled", color="#c9d7ea", edgecolor=AZUL, lw=1.4, label="fórmula")
dir_.hist(m_r, bins=bordas, histtype="step", color=LARANJA, lw=2.2, ls=(0, (5, 2)), label="rede")
dir_.set_yscale("log")
eixo_massa(dir_)
dir_.set_xlabel("massa invariante, GeV", fontsize=17)
dir_.set_ylabel("eventos por faixa", fontsize=17)
dir_.yaxis.set_major_formatter(MILHAR)
dir_.yaxis.set_minor_formatter(NullFormatter())
for ax in (esq, dir_):
ax.tick_params(labelsize=15)
alcas = [plt.Line2D([], [], color=AZUL, lw=6, alpha=0.5, label="fórmula"),
plt.Line2D([], [], color=LARANJA, lw=2.2, ls=(0, (5, 2)), label="rede")]
# a legenda vai embaixo dos dois painéis, abaixo dos nomes dos eixos
fig.legend(handles=alcas, loc="upper center", ncol=2, frameon=False, fontsize=16, bbox_to_anchor=(0.5, -0.06))
salva(fig, "rede.png")
def meia_altura(centros, n, fundo):
"""A largura do pico à meia altura, em GeV, descontado o fundo.
O fundo é a média das faixas dos lados, longe do pico. A altura do pico é o
máximo menos o fundo; a largura é a distância entre os dois pontos em que a
contagem cruza o fundo mais a metade dessa altura, com interpolação linear
entre as faixas vizinhas. É uma medida simples, sem ajuste de curva.
Devolve as duas pontas, em GeV, e a contagem da meia altura.
"""
k = int(np.argmax(n))
meia = fundo + (n[k] - fundo) / 2
esq = k
while n[esq] > meia:
esq -= 1
dir_ = k
while n[dir_] > meia:
dir_ += 1
x_esq = centros[esq] + (meia - n[esq]) / (n[esq + 1] - n[esq]) * (centros[esq + 1] - centros[esq])
x_dir = centros[dir_ - 1] + (meia - n[dir_ - 1]) / (n[dir_] - n[dir_ - 1]) * (centros[dir_] - centros[dir_ - 1])
return x_esq, x_dir, meia
def perto(M):
"""Os picos do J/ψ e do Υ em escala linear, nos 100 mil eventos, e a largura do J/ψ à meia altura.
Faixas de 10 MeV no J/ψ e de 25 MeV no Υ. A seta laranja marca a largura do
pico do J/ψ à meia altura, medida por meia_altura(); o número fica à direita
da seta, sobre o fundo, longe do pico. Devolve a largura, em MeV.
"""
fig, (esq, dir_) = plt.subplots(1, 2, figsize=(12, 4.8), gridspec_kw={"wspace": 0.28})
bordas = np.arange(2.80, 3.40001, 0.01)
n, _, _ = esq.hist(M, bins=bordas, histtype="stepfilled", color="#c9d7ea", edgecolor=AZUL, lw=1.4)
centros = (bordas[1:] + bordas[:-1]) / 2
lados = (centros < 2.95) | (centros > 3.25)
x_esq, x_dir, meia = meia_altura(centros, n, n[lados].mean())
esq.annotate("", xy=(x_esq, meia), xytext=(x_dir, meia),
arrowprops={"arrowstyle": "<->", "color": LARANJA, "lw": 2})
esq.text(x_dir + 0.03, meia, f"{1000 * (x_dir - x_esq):.0f} MeV", color=LARANJA, fontsize=16, va="center")
esq.set_xlim(2.8, 3.4)
esq.set_ylim(0, n.max() * 1.12)
esq.set_title("J/ψ, faixas de 10 MeV", fontsize=17, color=ESCURO)
bordas = np.arange(8.80, 10.60001, 0.025)
n2, _, _ = dir_.hist(M, bins=bordas, histtype="stepfilled", color="#c9d7ea", edgecolor=AZUL, lw=1.4)
dir_.set_xlim(8.8, 10.6)
dir_.set_ylim(0, n2.max() * 1.12)
dir_.set_title("Υ, faixas de 25 MeV", fontsize=17, color=ESCURO)
for ax in (esq, dir_):
ax.xaxis.set_major_formatter(VIRGULA)
ax.yaxis.set_major_formatter(MILHAR)
ax.tick_params(labelsize=15)
ax.set_xlabel("massa invariante, GeV", fontsize=17)
esq.set_ylabel("eventos por faixa", fontsize=17)
salva(fig, "perto.png")
return 1000 * (x_dir - x_esq)
def curva(resultado):
"""O erro mediano no treino e na validação, época a época, em escala logarítmica, com a melhor época marcada.
Marcadores diferentes para as duas curvas, círculo e quadrado, para a figura
se ler em escala de cinza; a legenda vai embaixo, fora dos dados.
"""
h = resultado["historico"]
epocas = np.arange(1, len(h["erro_treino"]) + 1)
melhor = resultado["melhor_epoca"]
fig, ax = plt.subplots(figsize=(11, 4.8))
ax.plot(epocas, 100 * np.array(h["erro_treino"]), "-o", color=AZUL, ms=4, lw=1.6, label="treino, 80 mil eventos")
ax.plot(epocas, 100 * np.array(h["erro_validacao"]), "-s", color=LARANJA, ms=4, lw=1.6,
label="validação, 10 mil eventos")
ax.axvline(melhor, color=CINZA, lw=1.4, ls=(0, (4, 3)))
ax.set_yscale("log")
ax.set_ylim(1.5, 16)
ax.set_yticks([2, 3, 5, 10, 15])
ax.yaxis.set_major_formatter(VIRGULA)
ax.yaxis.set_minor_formatter(NullFormatter())
ax.set_xlim(0, len(epocas) + 1)
ax.text(melhor - 1.5, 10, f"época {melhor}, a melhor\nna validação", ha="right", va="center", fontsize=15,
color=SUAVE)
ax.set_xlabel("época", fontsize=17)
ax.set_ylabel("erro relativo mediano, %", fontsize=17)
ax.tick_params(labelsize=15)
fig.legend(loc="upper center", ncol=2, frameon=False, fontsize=15, bbox_to_anchor=(0.5, -0.02))
salva(fig, "curva.png")
REGIOES = [(2, 2.9, "2 a 2,9"), (2.9, 3.3, "J/ψ\n2,9 a 3,3"), (3.3, 8.5, "3,3 a 8,5"), (8.5, 11, "Υ\n8,5 a 11"),
(11, 80, "11 a 80"), (80, 100, "Z\n80 a 100")]
def onde_erra(resultado):
"""À esquerda, o pico do J/ψ no teste, pela fórmula e pela rede; à direita, o erro mediano da rede por faixa.
Em cima de cada barra, quantos eventos do teste caem na faixa: o erro do Z,
com poucos eventos, é o menos firme. Devolve, por faixa, o erro mediano em %
e a contagem.
"""
m_f, m_r = np.array(resultado["teste_formula"]), np.array(resultado["teste_rede"])
rel = np.abs(m_r - m_f) / m_f
fig, (esq, dir_) = plt.subplots(1, 2, figsize=(12, 4.8), gridspec_kw={"wspace": 0.3})
bordas = np.arange(2.80, 3.40001, 0.02)
esq.hist(m_f, bins=bordas, histtype="stepfilled", color="#c9d7ea", edgecolor=AZUL, lw=1.4)
esq.hist(m_r, bins=bordas, histtype="step", color=LARANJA, lw=2.2, ls=(0, (5, 2)))
esq.set_xlim(2.8, 3.4)
esq.xaxis.set_major_formatter(VIRGULA)
esq.set_xlabel("massa invariante, GeV", fontsize=17)
esq.set_ylabel("eventos do teste por faixa", fontsize=17)
esq.set_title("o J/ψ no teste, faixas de 20 MeV", fontsize=17, color=ESCURO)
medianas, contagens = [], []
for lo, hi, _ in REGIOES:
s = (m_f >= lo) & (m_f < hi)
medianas.append(100 * np.median(rel[s]))
contagens.append(int(s.sum()))
posicoes = np.arange(len(REGIOES))
dir_.bar(posicoes, medianas, color="#c9d7ea", edgecolor=AZUL, lw=1.4, width=0.7)
for x, v, n in zip(posicoes, medianas, contagens):
dir_.text(x, v + 0.08, f"{n:,}".replace(",", "."), ha="center", va="bottom", fontsize=13, color=SUAVE)
dir_.set_xticks(posicoes, [r[2] for r in REGIOES], fontsize=13)
dir_.set_ylim(0, max(medianas) * 1.25)
dir_.yaxis.set_major_formatter(VIRGULA)
dir_.set_ylabel("erro relativo mediano, %", fontsize=17)
dir_.set_xlabel("massa pela fórmula, GeV", fontsize=17)
dir_.set_title("o erro da rede; em cima, quantos eventos", fontsize=17, color=ESCURO)
for ax in (esq, dir_):
ax.tick_params(axis="y", labelsize=15)
esq.tick_params(axis="x", labelsize=15)
alcas = [plt.Line2D([], [], color=AZUL, lw=6, alpha=0.5, label="fórmula"),
plt.Line2D([], [], color=LARANJA, lw=2.2, ls=(0, (5, 2)), label="rede")]
fig.legend(handles=alcas, loc="upper left", ncol=2, frameon=False, fontsize=15, bbox_to_anchor=(0.1, -0.06))
salva(fig, "onde-erra.png")
return dict(zip([r[2] for r in REGIOES], zip(medianas, contagens)))
if __name__ == "__main__":
c = carrega()
resultado = json.loads((AULA / "resultados" / "massa.json").read_text(encoding="utf-8"))
histograma(c["M"])
rede(resultado)
print(f"largura do J/ψ à meia altura: {perto(c['M']):.0f} MeV")
curva(resultado)
print(onde_erra(resultado))
Not published here: the reference solution of an assignment that is still open.
