Metadata-Version: 2.5
Name: suns
Version: 0.1.1
Summary: Interpolate digitized optical-component loss curves and use them to correct a SUNS spectrometer/telescope calibration spectrum, per component and combined.
Project-URL: Homepage, https://github.com/guicavazzana/suns
Project-URL: Repository, https://github.com/guicavazzana/suns
Author: Guilherme Cavazzana
License: MIT
License-File: LICENSE
Requires-Python: >=3.9
Requires-Dist: matplotlib>=3.5
Requires-Dist: numpy>=1.21
Requires-Dist: pandas>=1.3
Provides-Extra: test
Requires-Dist: pytest; extra == 'test'
Description-Content-Type: text/markdown

# suns

Biblioteca para corrigir um espectro de calibração do SUNS (espectrômetro/
telescópio) das perdas ópticas de cada elemento no caminho do feixe
(divisores de feixe, filtros, lentes, dicroicas, fibra, ...), cujas curvas
de transmitância/refletância foram digitalizadas de gráficos de datasheet
(ex.: com PlotDigitizer).

Publicada no PyPI como `suns` — mesmo nome pra instalar e pra importar:

```bash
pip install suns
```

```python
from suns import OpticalElement, generate_report, read_suns_spectrum
```

---

## ✅ Status: publicada e acessível de qualquer computador

**Sim, dos dois jeitos.** De **qualquer computador** com Python, pip e
acesso à internet (não precisa da pasta local, não precisa do OneDrive, não
precisa desta máquina) — no VS Code de qualquer lugar, terminal, o que for:

**PyPI** (recomendado — mais simples):

```bash
pip install suns
```

**GitHub** (código-fonte, issues, histórico): **https://github.com/guicavazzana/suns**

```bash
pip install "git+https://github.com/guicavazzana/suns.git"
```

```python
from suns import OpticalElement, generate_report, read_suns_spectrum
```

Ambos os caminhos foram testados (desinstalado localmente e reinstalado a
partir de cada um) e funcionam.

### Se quiser instalar em modo "dev" (editando o código)

```bash
git clone https://github.com/guicavazzana/suns.git
cd suns
py -3 -m pip install -e ".[test]"
```

(Nesta máquina especificamente, `-e` não funciona por causa do caminho
acentuado — ver aviso mais abaixo. Num clone limpo em outro caminho, sem
acento, `-e` funciona normalmente.)

---

## Instalar (nesta máquina, ou em qualquer outra que tenha a pasta)

Da pasta `suns_loss_calib/`:

```bash
py -3 -m pip install .
```

> **Não use `pip install -e .` nesta máquina.** A instalação editável fica
> silenciosamente quebrada aqui: o caminho da pasta contém "Dissertação"
> (com `ç`/`ã`), e o `site.py` do Windows lê o arquivo `.pth` do install
> editável usando o codepage ANSI (cp1252) em vez de UTF-8 — o caminho vem
> corrompido, `import suns` falha com `ModuleNotFoundError` e **nenhum
> erro é mostrado**. Uma instalação normal (`pip install .`, sem `-e`)
> copia os arquivos de verdade e funciona sem problema; só é preciso rodar
> de novo depois de editar o código da biblioteca. Ver `tests/conftest.py`
> para o diagnóstico completo.

Para desenvolver/rodar os testes sem precisar reinstalar a cada mudança,
os testes já resolvem isso sozinhos (veja "Testes" abaixo).

Dependências: `numpy`, `pandas`, `matplotlib` (instaladas automaticamente).

---

## Conceitos

### `OpticalElement` — um elemento óptico no caminho do feixe

```python
from suns import OpticalElement

elemento = OpticalElement(
    name="ND filter OD 1.0",       # nome (aparece em títulos, legendas e nomes de arquivo)
    path="dados/filtro_od10.csv",  # CSV digitalizado (qualquer um dos dois formatos, ver abaixo)
    kind="direct",                 # "direct" ou "reflection_loss" (ver abaixo)
    percent=True,                  # valores digitalizados estão em escala 0-100 (padrão) ou já 0-1
    enabled=True,                  # False = mantém o elemento na lista mas exclui ele da conta
)
```

O parâmetro **`kind`** diz o que o valor digitalizado *significa* para a luz
que chega no SUNS:

- **`"direct"`**: o valor digitalizado já É a fração de luz que chega ao
  SUNS através desse elemento (transmissão de um filtro, transmissão de
  uma fibra, ou a refletância do lado *de trabalho* de um divisor de feixe
  — quando é o lado refletido que segue para o SUNS).
- **`"reflection_loss"`**: o valor digitalizado é uma refletância que
  *tira* luz do caminho de transmissão desejado (ex.: refletância residual
  do revestimento AR de uma lente); a transmitância é `1 - valor`.

O parâmetro **`enabled`** (padrão `True`) deixa você desligar um elemento
sem apagar a linha — ver "Guia do usuário final" logo abaixo, é a resposta
mais direta pra "como eu tiro uma perda óptica da conta".

```python
T = elemento.transmittance(wl_grid)  # np.ndarray, mesma forma de wl_grid
```

Chama `1 - warnings.warn(...)` (aviso, não erro) se o `wl_grid` pedido for
mais largo que o range digitalizado (extrapola constante nas pontas), e
outro aviso se der T fora de `[0, 1]` antes de recortar (`clip=True` por
padrão).

### `read_digitized_csv` — leitor único para os dois formatos de CSV

```python
from suns import read_digitized_csv
df = read_digitized_csv("dados/qualquer_arquivo.csv")  # -> DataFrame(wavelength, value)
```

Não precisa saber de antemão qual dos dois formatos o arquivo usa:

- **"limpo"**: primeira linha já é o cabeçalho real (`WAVELENGTH (nm),PERCENT`).
- **"bruto"** (exportação nativa do PlotDigitizer): linha de metadado entre
  aspas, linha "Date: ...", linhas em branco, uma linha solta só com a
  contagem de pontos, o cabeçalho, e então as linhas de dado — que podem
  terminar com vírgula sobrando.

O leitor trata qualquer linha cujos dois primeiros tokens (separados por
vírgula) sejam ambos conversíveis pra float como uma linha de dado
`(wavelength, value)`; todo o resto (metadado, data, contagem, cabeçalho,
linha em branco) é ignorado silenciosamente.

### `read_suns_spectrum` — carrega o espectro médio do SUNS

```python
from suns import read_suns_spectrum
suns = read_suns_spectrum("espectro_medio.csv")  # -> DataFrame(wavelength, dn)
```

### `compute_losses` — combina vários elementos

```python
from suns import compute_losses
import numpy as np

wl_grid = suns["wavelength"].values
result = compute_losses([elemento1, elemento2, ...], wl_grid)

result.per_item   # dict: nome do elemento -> np.ndarray de transmitância
result.total       # np.ndarray: produto de todas as transmitâncias (perda total do sistema)
```

### `correct_spectrum` — aplica a correção

```python
from suns import correct_spectrum
espectro_corrigido = correct_spectrum(suns["dn"].values, result.total)
```

### `generate_report` — gera tudo de uma vez (por item + combinado)

```python
from suns import generate_report

result, corrigido_total = generate_report(
    suns_wl=suns["wavelength"].values,
    suns_dn=suns["dn"].values,
    components=[elemento1, elemento2, ...],
    out_dir="resultados",
    lang="en",              # "en" ou "pt"
    xlim=(340, 820),        # opcional, range do eixo X nos gráficos
)
```

Gera em `out_dir/`:

```
resultados/
  per_item/
    <slug>_transmittance.png      # curva digitalizada, interpolada no grid do SUNS
    <slug>_suns_corrected.png     # SUNS original vs. corrigido só por ESSE elemento
  combined/
    total_loss.png                          # produto de todos os elementos
    spectrum_original_vs_corrected.png       # original vs. corrigido (tudo aplicado)
    spectrum_original_vs_corrected_log.png   # idem, escala log
    spectrum_normalized.png                  # comparação normalizada
```

(`<slug>` = nome do elemento normalizado — ex.: `"ND filter OD 1.0"` vira
`nd_filter_od_1_0`.)

---

## Guia do usuário final: adicionar, remover, trocar e ajustar perdas ópticas

Tudo isso é feito editando uma **lista Python comum** de `OpticalElement`
(no seu caso, a lista `ELEMENTS` em `gerar_correcoes_suns.py`) — não existe
configuração em outro lugar, arquivo `.yaml`, nada escondido. A lista *é*
a configuração. Isso é intencional: qualquer mudança na montagem óptica diz
respeito a essa lista, e só a ela.

### 1. Adicionar um elemento óptico novo

Duas coisas: o CSV digitalizado da curva, e uma entrada na lista.

```python
ELEMENTS.append(
    OpticalElement(
        name="Espelho X",
        path=DADOS / "espelho_x.csv",
        kind="direct",  # ou "reflection_loss", dependendo do que a curva representa
    )
)
```

Não precisa se preocupar com o formato do CSV — `read_digitized_csv` aceita
tanto uma exportação "limpa" quanto a "bruta" nativa do PlotDigitizer, ele
descobre sozinho. O único cuidado real é o **`kind`** (seção "Conceitos"
acima): pergunte "o valor digitalizado já é a fração de luz que segue pro
SUNS, ou é uma reflexão que eu preciso descontar de 1?".

### 2. Remover ou desligar um elemento (sem apagar a linha)

Três formas, do mais permanente ao mais reversível:

```python
# a) Apagar a linha de vez — não sobra rastro nenhum
# (é só remover o OpticalElement(...) inteiro da lista ELEMENTS)

# b) Comentar a linha — fica documentado no código que existe, mas não roda
ELEMENTS = [
    OpticalElement(name="Divisor PDOT-2", path=..., kind="direct"),
    # OpticalElement(name="Filtro OD 1.3", path=..., kind="direct"),  # removido para teste X
    ...
]

# c) enabled=False — a forma mais "de biblioteca": fica na lista, com nome e
#    CSV documentados, mas compute_losses()/generate_report() ignoram ele
#    por completo (nem entra no total, nem gera figura por item).
OpticalElement(name="Filtro OD 1.3", path=DADOS / "filtro_1_3.csv", kind="direct", enabled=False),
```

Use `enabled=False` quando você quer comparar "com" vs. "sem" um elemento
sem duplicar código nem perder a definição de onde o CSV está.

### 3. Trocar qual curva/arquivo um elemento usa

É só mudar o `path` — por exemplo, testar a placa dicroica com a curva
polarizada em vez da "Unpoll":

```python
OpticalElement(
    name="BSW26R dichroic plate",
    path=DADOS / "Figura 42 S-Pol - ....csv",  # em vez de "Unpoll"
    kind="direct",
),
```

### 4. Rodar uma comparação "e se eu tirasse esse elemento?" sem mexer no script principal

Como `ELEMENTS` é só uma lista Python, dá pra filtrar ela em outro script
(ou num notebook) sem tocar no `gerar_correcoes_suns.py`:

```python
from gerar_correcoes_suns import ELEMENTS, DADOS  # reaproveita a config que já existe
from suns import compute_losses, correct_spectrum, read_suns_spectrum

suns = read_suns_spectrum("espectro_medio.csv")
wl = suns["wavelength"].values

sem_fibra = [e for e in ELEMENTS if e.name != "1000um fiber"]
resultado = compute_losses(sem_fibra, wl)
corrigido_sem_fibra = correct_spectrum(suns["dn"].values, resultado.total)
# compare corrigido_sem_fibra com o resultado "oficial" (todos os elementos)
```

Ou, usando `enabled=False` num experimento isolado (sem alterar a lista
original — cria cópias com `dataclasses.replace`):

```python
from dataclasses import replace

so_filtros = [
    replace(e, enabled=e.name.startswith("ND filter"))
    for e in ELEMENTS
]
resultado_so_filtros = compute_losses(so_filtros, wl)
```

### 5. Ajustar como as figuras saem (idioma, faixa de comprimento de onda)

```python
generate_report(
    suns_wl, suns_dn, ELEMENTS,
    out_dir="resultados_pt",
    lang="pt",              # "en" (padrão) ou "pt" — troca todo texto dos gráficos
    xlim=(350, 900),         # None = sem limite fixo no eixo X
)
```

### 6. Depois de qualquer mudança: como saber se deu certo

```bash
cd suns_loss_calib
py -3 -m pytest -q          # os 14 testes continuam passando?
```

E rode o script de novo (`py -3 gerar_correcoes_suns.py`) e olhe:
- os **avisos no console** (`UserWarning`) — eles apontam quando um elemento
  não cobre toda a faixa de comprimento de onda do SUNS (extrapolação nas
  pontas) ou quando a transmitância calculada saiu de `[0, 1]`, sinal de que
  o `kind` ou o CSV podem estar errados;
- o gráfico `combined/total_loss.png` — se a perda total mudar de ordem de
  grandeza de forma inesperada depois de uma edição, é o primeiro lugar
  pra desconfiar.

---

## Exemplo completo

Veja `graficos_digitalizados/gerar_correcoes_suns.py` no repositório da
dissertação para o exemplo real e completo (6 elementos ópticos do SUNS,
com as referências ao relatório técnico que justificam cada escolha de
`kind`). Versão resumida:

```python
from pathlib import Path
from suns import OpticalElement, generate_report, read_suns_spectrum

DADOS = Path("dados")

elementos = [
    OpticalElement(name="Divisor PDOT-2", path=DADOS / "divisor.csv", kind="direct"),
    OpticalElement(name="Filtro OD 1.0", path=DADOS / "filtro_1_0.csv", kind="direct"),
    OpticalElement(name="Dubleto acromático", path=DADOS / "dubleto.csv", kind="reflection_loss"),
]

suns = read_suns_spectrum("espectro_medio.csv")

result, corrigido = generate_report(
    suns["wavelength"].values, suns["dn"].values, elementos,
    out_dir="resultados", lang="en", xlim=(340, 820),
)
```

---

## Testes

```bash
cd suns_loss_calib
py -3 -m pytest -q
```

Os testes **não dependem** de `pip install`/`pip install -e` — um
`tests/conftest.py` insere `src/` no `sys.path` diretamente, então rodam
mesmo sem instalar nada (contorna o mesmo problema de caminho acentuado
descrito acima).

---

## Referência rápida da API

| Nome | O quê |
|---|---|
| `OpticalElement(name, path, kind="direct", percent=True)` | Um elemento óptico digitalizado |
| `.transmittance(wl_grid, clip=True) -> np.ndarray` | Transmitância interpolada no grid pedido |
| `.slug() -> str` | Nome normalizado (usado em nomes de arquivo) |
| `read_digitized_csv(path) -> pd.DataFrame` | Lê um CSV digitalizado (qualquer um dos dois formatos) |
| `read_suns_spectrum(path) -> pd.DataFrame` | Lê o espectro médio do SUNS |
| `compute_losses(elements, wl_grid) -> LossResult` | Combina vários elementos (por item + total) |
| `correct_spectrum(spectrum, transmittance) -> np.ndarray` | `spectrum / transmittance` |
| `generate_report(suns_wl, suns_dn, elements, out_dir, lang="en", xlim=None)` | Gera todas as figuras (por item + combinado) |

Ver `src/suns/pipeline.py` para a orquestração completa e `tests/` para
exemplos executáveis de cada peça.
