nini-datacomp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nini_datacomp-0.1.0/.gitattributes +4 -0
- nini_datacomp-0.1.0/.gitignore +23 -0
- nini_datacomp-0.1.0/.python-version +1 -0
- nini_datacomp-0.1.0/CHANGELOG.md +281 -0
- nini_datacomp-0.1.0/CMakeLists.txt +68 -0
- nini_datacomp-0.1.0/LICENSE +201 -0
- nini_datacomp-0.1.0/NOTICE +4 -0
- nini_datacomp-0.1.0/PKG-INFO +248 -0
- nini_datacomp-0.1.0/README.md +215 -0
- nini_datacomp-0.1.0/cpp/abi.cpp +25 -0
- nini_datacomp-0.1.0/cpp/batch.cpp +104 -0
- nini_datacomp-0.1.0/cpp/columns.hpp +155 -0
- nini_datacomp-0.1.0/cpp/common.hpp +28 -0
- nini_datacomp-0.1.0/cpp/compare.cpp +218 -0
- nini_datacomp-0.1.0/cpp/datacomp.h +56 -0
- nini_datacomp-0.1.0/cpp/keys.cpp +311 -0
- nini_datacomp-0.1.0/cpp/memory.cpp +143 -0
- nini_datacomp-0.1.0/cpp/parquet_meta.cpp +155 -0
- nini_datacomp-0.1.0/cpp/rowhash.cpp +174 -0
- nini_datacomp-0.1.0/pyproject.toml +155 -0
- nini_datacomp-0.1.0/src/nini_datacomp/__init__.py +55 -0
- nini_datacomp-0.1.0/src/nini_datacomp/_native.py +121 -0
- nini_datacomp-0.1.0/src/nini_datacomp/_signatures.py +27 -0
- nini_datacomp-0.1.0/src/nini_datacomp/api.py +250 -0
- nini_datacomp-0.1.0/src/nini_datacomp/core/__init__.py +1 -0
- nini_datacomp-0.1.0/src/nini_datacomp/core/allocator.py +60 -0
- nini_datacomp-0.1.0/src/nini_datacomp/core/arrow.py +207 -0
- nini_datacomp-0.1.0/src/nini_datacomp/core/arrow_c.py +374 -0
- nini_datacomp-0.1.0/src/nini_datacomp/core/blocks.py +112 -0
- nini_datacomp-0.1.0/src/nini_datacomp/core/compare.py +203 -0
- nini_datacomp-0.1.0/src/nini_datacomp/core/keys.py +138 -0
- nini_datacomp-0.1.0/src/nini_datacomp/core/memory.py +73 -0
- nini_datacomp-0.1.0/src/nini_datacomp/core/rowhash.py +194 -0
- nini_datacomp-0.1.0/src/nini_datacomp/errors.py +17 -0
- nini_datacomp-0.1.0/src/nini_datacomp/io.py +130 -0
- nini_datacomp-0.1.0/src/nini_datacomp/options.py +101 -0
- nini_datacomp-0.1.0/src/nini_datacomp/pipeline.py +824 -0
- nini_datacomp-0.1.0/src/nini_datacomp/py.typed +0 -0
- nini_datacomp-0.1.0/src/nini_datacomp/report.py +595 -0
- nini_datacomp-0.1.0/src/nini_datacomp/results.py +256 -0
- nini_datacomp-0.1.0/src/nini_datacomp/windows.py +530 -0
- nini_datacomp-0.1.0/tests/conftest.py +4 -0
- nini_datacomp-0.1.0/tests/golden/basic.json +82 -0
- nini_datacomp-0.1.0/tests/golden/categorical_cast_right.json +42 -0
- nini_datacomp-0.1.0/tests/golden/categorical_vs_string.json +35 -0
- nini_datacomp-0.1.0/tests/golden/composite_key.json +43 -0
- nini_datacomp-0.1.0/tests/golden/duplicate_keys.json +31 -0
- nini_datacomp-0.1.0/tests/golden/empty_left.json +31 -0
- nini_datacomp-0.1.0/tests/golden/floats.json +70 -0
- nini_datacomp-0.1.0/tests/golden/int_widths_left.json +42 -0
- nini_datacomp-0.1.0/tests/golden/int_widths_none.json +35 -0
- nini_datacomp-0.1.0/tests/golden/no_common_keys.json +31 -0
- nini_datacomp-0.1.0/tests/golden/null_keys.json +41 -0
- nini_datacomp-0.1.0/tests/golden/shuffled.json +82 -0
- nini_datacomp-0.1.0/tests/golden/sparse_keys.json +41 -0
- nini_datacomp-0.1.0/tests/golden/temporal.json +68 -0
- nini_datacomp-0.1.0/tests/golden/text_exact.json +61 -0
- nini_datacomp-0.1.0/tests/golden/text_ignore.json +48 -0
- nini_datacomp-0.1.0/tests/golden/text_key.json +41 -0
- nini_datacomp-0.1.0/tests/golden/tolerance_abs.json +61 -0
- nini_datacomp-0.1.0/tests/golden/tolerance_per_column.json +61 -0
- nini_datacomp-0.1.0/tests/golden/tolerance_rel.json +66 -0
- nini_datacomp-0.1.0/tests/golden/wide_300.json +364 -0
- nini_datacomp-0.1.0/tests/test_allocator.py +54 -0
- nini_datacomp-0.1.0/tests/test_api.py +232 -0
- nini_datacomp-0.1.0/tests/test_arrow.py +220 -0
- nini_datacomp-0.1.0/tests/test_benchstats.py +38 -0
- nini_datacomp-0.1.0/tests/test_blocks.py +59 -0
- nini_datacomp-0.1.0/tests/test_compare.py +194 -0
- nini_datacomp-0.1.0/tests/test_data.py +95 -0
- nini_datacomp-0.1.0/tests/test_golden.py +93 -0
- nini_datacomp-0.1.0/tests/test_io.py +75 -0
- nini_datacomp-0.1.0/tests/test_keys.py +223 -0
- nini_datacomp-0.1.0/tests/test_memory.py +95 -0
- nini_datacomp-0.1.0/tests/test_native.py +67 -0
- nini_datacomp-0.1.0/tests/test_pipeline.py +294 -0
- nini_datacomp-0.1.0/tests/test_read_experiment.py +23 -0
- nini_datacomp-0.1.0/tests/test_report.py +122 -0
- nini_datacomp-0.1.0/tests/test_rowhash.py +216 -0
- nini_datacomp-0.1.0/tests/test_windows.py +197 -0
- nini_datacomp-0.1.0/tools/bench_baseline.py +266 -0
- nini_datacomp-0.1.0/tools/bench_hash.py +213 -0
- nini_datacomp-0.1.0/tools/bench_rows.py +198 -0
- nini_datacomp-0.1.0/tools/benchstats.py +87 -0
- nini_datacomp-0.1.0/tools/cases.py +223 -0
- nini_datacomp-0.1.0/tools/check.py +71 -0
- nini_datacomp-0.1.0/tools/diag_memory.py +313 -0
- nini_datacomp-0.1.0/tools/diag_profile.py +99 -0
- nini_datacomp-0.1.0/tools/diag_read.py +118 -0
- nini_datacomp-0.1.0/tools/gen_bindings.py +143 -0
- nini_datacomp-0.1.0/tools/make_data.py +235 -0
- nini_datacomp-0.1.0/tools/make_golden.py +116 -0
- nini_datacomp-0.1.0/tools/read_experiment.py +222 -0
- nini_datacomp-0.1.0/uv.lock +1226 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
|
|
10
|
+
# Virtual environments
|
|
11
|
+
.venv
|
|
12
|
+
|
|
13
|
+
# Data folder
|
|
14
|
+
data/
|
|
15
|
+
|
|
16
|
+
# Benchmarks
|
|
17
|
+
benchmarks/results/
|
|
18
|
+
|
|
19
|
+
# Demo
|
|
20
|
+
reports/
|
|
21
|
+
# Dados gerados (tools/make_data.py) e resultados de benchmark
|
|
22
|
+
benchmarks/
|
|
23
|
+
amostras.md
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Formato: [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/); versões: [SemVer](https://semver.org/lang/pt-BR/).
|
|
4
|
+
|
|
5
|
+
## [Não lançado]
|
|
6
|
+
|
|
7
|
+
## [0.1.0] — 2026-11-03
|
|
8
|
+
|
|
9
|
+
Primeira versão: a reescrita do datacompolars 0.4.4 com a comparação em kernels C++.
|
|
10
|
+
|
|
11
|
+
- `dc.compare(left, right, on, ...)` com `CompareOptions` e `ComparisonResult`: o mesmo resultado do datacompolars
|
|
12
|
+
(contagens, divergências por coluna e amostras, conferidos contra ele em 21 casos de borda), com os nomes da
|
|
13
|
+
tabela de migração do README.
|
|
14
|
+
- Comparação por janelas pela chave, dentro de um `memory_limit`: todos os casos da grade (1M e 10M linhas x 10, 50
|
|
15
|
+
e 300 colunas) ficam abaixo de 1 GB, onde o datacompolars chegava a quase 2 GB.
|
|
16
|
+
- Igualdade exata pelo hash de linha (`verify_hash=True` confere célula a célula); tolerâncias, `ignore_case` e
|
|
17
|
+
`ignore_whitespace` coluna a coluna, com a aritmética do Polars.
|
|
18
|
+
- Relatório em texto, Markdown, HTML e JSON (`result.report`).
|
|
19
|
+
- Desempenho (caminho exato, `memory_limit="1GB"`): de 0,66x a 1,05x o tempo do datacompolars, e 10M x 50 em 1,20x
|
|
20
|
+
(inconclusivo: o IC de 1,05 a 1,35 passa de 1,10, mas não fica inteiro acima). Nenhum caso mais lento.
|
|
21
|
+
- Wheels para Linux, Windows e macOS (x86-64 e ARM64), Python 3.10 a 3.14, sem pyarrow.
|
|
22
|
+
|
|
23
|
+
O histórico abaixo, fase a fase, é o do desenvolvimento.
|
|
24
|
+
|
|
25
|
+
### Fase 0a — fundação
|
|
26
|
+
- Biblioteca C++ (`datacomp.dll` / `libdatacomp.so` / `.dylib`) com ABI em C de prefixo `dc_`, versão de ABI conferida
|
|
27
|
+
no carregamento (`DC_ABI_VERSION`) e assinaturas do ctypes geradas dos `.cpp` (`tools/gen_bindings.py`).
|
|
28
|
+
- Do nini-datagen 0.3.0: build com scikit-build-core e CMake (clang-cl nos wheels do Windows, x86-64-v2, sem FMA,
|
|
29
|
+
`/MT`), pool de threads (`core/blocks.py`), pool de memória (`cpp/memory.cpp`, `core/memory.py`), interface C do
|
|
30
|
+
Arrow sem pyarrow (`core/arrow_c.py`) e os workflows de CI e de wheels.
|
|
31
|
+
- `core/arrow.py`: `import_column` lê qualquer coluna dos tipos da 0.1 (inteiros, reais, booleano, texto, Date,
|
|
32
|
+
Datetime, Duration, Time, Decimal) como buffers do Arrow, sem cópia; outro tipo levanta `UnsupportedTypeError`.
|
|
33
|
+
- API pública por enquanto: `release_memory()`, `pool_limit()`, `pool_stats()`, `__version__`.
|
|
34
|
+
|
|
35
|
+
### Fase 0b — dados, gabaritos e linha de base
|
|
36
|
+
- `tools/make_data.py`: pares de tabelas Parquet gerados com o nini-datagen (semente fixa; inteiros, reais, texto,
|
|
37
|
+
booleano, data e data e hora em ciclo, ~2% de nulos), com o lado direito mudado de forma controlada e o resultado
|
|
38
|
+
esperado gravado pela própria construção (`expected.json`).
|
|
39
|
+
- `tools/cases.py` + `tools/make_golden.py`: 21 casos de borda (chave composta, de texto, nula, esparsa, fora de
|
|
40
|
+
ordem; duplicatas; lado vazio; -0.0, NaN, infinitos; texto com acentos, NBSP e emoji; Categorical; larguras de
|
|
41
|
+
inteiro; tolerâncias na borda; 300 colunas; tipos temporais e Decimal) com gabaritos do datacompolars 0.4.4 pelo
|
|
42
|
+
caminho columnwise em `tests/golden/`. Alguns conferidos à mão nos testes.
|
|
43
|
+
- `tools/bench_baseline.py` + `tools/benchstats.py`: linha de base do datacompolars 0.4.4 na grade do plano (100 a 10M
|
|
44
|
+
linhas x 10 a 300 colunas, caminhos hash e columnwise), cada execução num processo novo, média e IC de 95%,
|
|
45
|
+
retomável; resultado em `docs/benchmarks/baseline.{md,json}`.
|
|
46
|
+
|
|
47
|
+
### Fase 0c — experimento de leitura
|
|
48
|
+
- `tools/read_experiment.py`: lê as mesmas janelas da comparação (faixas de ids, os dois lados) pelo Polars, pelo
|
|
49
|
+
leitor C++ do Apache Arrow, pelo Polars sem compressão e só os bytes do disco, para medir o teto do ganho de um
|
|
50
|
+
leitor de Parquet próprio antes de escrevê-lo. Grupo `bench` (pyarrow) só para isso; o pacote não usa o pyarrow.
|
|
51
|
+
- Gabaritos: o tipo divergente guarda só o nome do tipo (o texto do Categorical muda entre versões do Polars).
|
|
52
|
+
- Decimal: o Polars 1.30 não o exporta pela interface C do Arrow (nem pela representação física); exige um Polars
|
|
53
|
+
mais novo.
|
|
54
|
+
|
|
55
|
+
### Fase 1a — alinhamento das chaves (ABI 2)
|
|
56
|
+
- Kernel `dc_align_keys` (`cpp/keys.cpp`) e `core/keys.py` (`align_keys`): pares de linhas com a mesma chave e as
|
|
57
|
+
contagens do datacompolars (em comum, só esquerda, só direita, excedentes por chave repetida). Chave de qualquer tipo
|
|
58
|
+
da 0.1 e composta; nulo casa com nulo, `-0.0 == 0.0`, NaN casa com NaN. Merge linear quando os dois lados vêm
|
|
59
|
+
ordenados (caminho direto para chave inteira sem nulos), hash join quando não; com chave repetida, só as contagens.
|
|
60
|
+
- Medido (Linux, 1 thread, chave int64): merge ~7 ns/linha; hash ~27 ns/linha numa janela de 100 mil linhas e ~59 em
|
|
61
|
+
1 milhão. Particionar o hash e paralelizar ficam para a fase 5.
|
|
62
|
+
- `import_column` recusa colunas codificadas por dicionário (Categorical, Enum): os buffers seriam códigos, diferentes
|
|
63
|
+
em cada tabela. A comparação converte para String antes.
|
|
64
|
+
|
|
65
|
+
### Fase 1b — janelas por faixa de qualquer chave (ABI 3)
|
|
66
|
+
- `windows.py` (`plan_windows`): janelas = faixas da tupla inteira da chave (ordem lexicográfica), de qualquer tipo
|
|
67
|
+
ordenável — inteiro, texto, data, chave composta; acaba a exigência do datacompolars de 1ª coluna inteira. Chaves com
|
|
68
|
+
nulo vão para a primeira janela. Fronteiras = a chave a cada N linhas do lado maior, por uma leitura só das colunas
|
|
69
|
+
da chave.
|
|
70
|
+
- Tamanho da janela pelo `memory_limit` (`"auto"` = 1/4 da memória livre, até 2 GiB; ou `"2GB"`, `"1.5GiB"`, bytes):
|
|
71
|
+
N = orçamento x 0,8 / (2 janelas x (bytes por linha dos dois lados + trabalho dos kernels)), com os bytes por linha
|
|
72
|
+
medidos pelo Polars numa amostra (memória depois de lida, não o tamanho no disco). `window_rows` fixa N à mão.
|
|
73
|
+
- Com o lado maior gravado em ordem, as fronteiras vão para o começo do row group mais próximo: nenhum row group é
|
|
74
|
+
decodificado duas vezes. As linhas de cada row group vêm do rodapé do Parquet, lido por `cpp/parquet_meta.cpp`
|
|
75
|
+
(`dc_parquet_row_groups`, Thrift compacto, sem pyarrow).
|
|
76
|
+
- `io.py`: fontes Parquet (arquivo, pasta com subpastas, glob), `pl.DataFrame` e `pl.LazyFrame`; CSV e SAS na fase 6.
|
|
77
|
+
|
|
78
|
+
### Fase 1c — caminho completo: contagem de linhas por janelas
|
|
79
|
+
- `pipeline.py`: `count_rows(left, right, on, memory_limit=..., window_rows=..., columns=...)` abre as fontes, confere
|
|
80
|
+
a chave (Categorical/Enum viram texto; tipos diferentes nos dois lados = erro claro), planeja as janelas, lê cada uma
|
|
81
|
+
pelo Polars e alinha as chaves no C++, somando as contagens. `WindowReader` lê a janela k+1 numa thread enquanto a k
|
|
82
|
+
é processada (no máximo duas em memória), e é a base das fases 2 e 3.
|
|
83
|
+
- A primeira janela só leva a cláusula "ou chave nula" quando algum lado tem chave nula: o "ou" impedia o Polars de
|
|
84
|
+
pular row groups pelas estatísticas, e a primeira janela leria a tabela inteira.
|
|
85
|
+
- `tools/bench_rows.py`: memória e tempo de 1M a 10M linhas (chave inteira e chave de texto), cada execução num
|
|
86
|
+
processo novo; resultado em `docs/benchmarks/fase1.{md,json}`.
|
|
87
|
+
- Medido no primeiro benchmark (`memory_limit = 1GB`): contagens certas em tudo, mas pico de 1,6–2,1 GB em 10M linhas,
|
|
88
|
+
crescendo com as linhas. Corrigido no planejamento: ele não carrega mais todas as chaves (nem cópias ordenadas) —
|
|
89
|
+
só conta as linhas numa passada, lê a chave do começo de cada row group (Parquet em ordem) ou uma amostra de até 200
|
|
90
|
+
mil chaves; a janela passa a contar 4 cópias vivas no pico (medido), não 2; e a base é o lado em ordem no disco
|
|
91
|
+
(antes era sempre o maior, que no make_data tem os arquivos extra-* fora de ordem).
|
|
92
|
+
- Medido no segundo (`memory_limit = 1GB`): pico de 1,3–1,6 GB em 10M linhas com a maior janela em ~200 MB. Para achar
|
|
93
|
+
de onde vem o resto: `engine` no `count_rows`/`WindowReader` (o engine do `collect` do Polars em cada janela) e
|
|
94
|
+
`tools/diag_memory.py`, que anota a memória etapa a etapa e janela a janela e roda variações que isolam uma causa
|
|
95
|
+
por vez (sem leitura em paralelo, menos threads, engine streaming, só a chave, janela menor, alocador devolvendo a
|
|
96
|
+
memória na hora, sem os kernels, sem o pool).
|
|
97
|
+
- Diagnóstico: o resto do pico era memória que o alocador do Polars (mimalloc, no Windows) guarda depois de liberar e
|
|
98
|
+
vai acumulando janela após janela; janelas menores, sem leitura em paralelo, sem os kernels ou sem o pool quase não
|
|
99
|
+
mudavam o pico. Com `MIMALLOC_PURGE_DELAY=0`: 10M x 10 de 1159 para 605 MB, 10M x 50 de 1094 para 693 MB (e 5,6 →
|
|
100
|
+
4,2 s nesse caso).
|
|
101
|
+
- `core/allocator.py`: o import do `nini_datacomp` define `MIMALLOC_PURGE_DELAY=0` quando o Polars ainda não foi
|
|
102
|
+
importado (sem sobrescrever um valor existente); quando já foi, avisa com `AllocatorWarning` (no Windows).
|
|
103
|
+
`NINI_DATACOMP_ALLOCATOR=0` desliga; `nini_datacomp.ALLOCATOR_STATUS` diz o que aconteceu. Os benchmarks medem com o
|
|
104
|
+
alocador configurado.
|
|
105
|
+
- `count_rows` solta as tabelas de cada janela antes de pedir a próxima: a variável do laço segurava a janela k
|
|
106
|
+
enquanto o leitor entregava a k+1 e já lia a k+2 (três janelas vivas; ~200 MB a mais no pico em 10M linhas, a
|
|
107
|
+
diferença entre o bench_rows e o diag_memory).
|
|
108
|
+
|
|
109
|
+
### Fase 2 — hash de linha (ABI 4)
|
|
110
|
+
- Kernels `dc_row_hash` e `dc_diff_pairs` (`cpp/rowhash.cpp`) e `core/rowhash.py` (`row_hash`, `diff_pairs`): um hash de
|
|
111
|
+
64 bits por linha das colunas comparadas, por blocos de 65.536 linhas em paralelo, e os pares (da `align_keys`) com
|
|
112
|
+
hash diferente. Hash próprio, igual em toda plataforma e versão do Polars (valores fixados nos testes), com a
|
|
113
|
+
igualdade do `eq_missing`: nulo = nulo, `-0.0 == 0.0`, NaN = NaN, texto inline ou em buffer, com ou sem bitmap de
|
|
114
|
+
validade. Construção: `h = fmix64((h ^ valor) + sal da coluna)` — o fmix64 (do murmur3) é uma bijeção, então duas
|
|
115
|
+
linhas que diferem numa coluna só nunca colidem; o plano falava em wyhash, que precisaria de multiplicação de 128
|
|
116
|
+
bits (não portátil no clang-cl com `-Wpedantic`), sem ganho de qualidade aqui.
|
|
117
|
+
- `verify=True` (`verify_hash` no `compare_rows`) reconfere célula a célula os pares de hash igual e conta as colisões.
|
|
118
|
+
- Medido (Linux, 1 thread): ~3 ns por célula, inteiro ou real.
|
|
119
|
+
- `cpp/columns.hpp`: a leitura das colunas (descritores, valores, igualdade) sai do `keys.cpp` para os dois kernels.
|
|
120
|
+
- `pipeline.compare_rows(left, right, on, casting=..., columns=..., verify_hash=...)`: as contagens do `count_rows` e
|
|
121
|
+
as linhas divergentes (`mismatched`, `equal`). Colunas comparadas como no datacompolars: nos dois lados, fora da
|
|
122
|
+
chave, com o mesmo tipo depois do `casting` (`"left"`/`"right"` convertem um lado para os tipos do outro, também na
|
|
123
|
+
chave); as de tipo diferente vão para `type_mismatches`; Categorical/Enum comparados como texto; colunas aninhadas
|
|
124
|
+
(List, Struct) levantam `UnsupportedTypeError` (fase 6). Só as colunas comparadas são lidas.
|
|
125
|
+
- Testes: os gabaritos de igualdade exata (linhas divergentes, colunas comparadas, tipos divergentes), com uma janela
|
|
126
|
+
e com janelas de 2 linhas, e os dados do make_data.
|
|
127
|
+
- `tools/bench_hash.py`: tempo e memória de 1M a 10M linhas x 10 a 300 colunas contra o caminho hash do
|
|
128
|
+
datacompolars, com a razão e o IC de 95%; resultado em `docs/benchmarks/fase2.{md,json}`.
|
|
129
|
+
- Primeiro benchmark (`memory_limit = 1GB`): contagens certas em tudo, mas mais lento que o datacompolars (10M x 300:
|
|
130
|
+
80 s contra 50 s) e acima do limite (1,5 GB). Duas causas corrigidas:
|
|
131
|
+
- o hash juntava cada coluna da janela num pedaço só de memória (`rechunk`, uma cópia da janela inteira por lado)
|
|
132
|
+
quando o Polars a entregava em vários (um por row group): agora `row_hash_parts` lê pedaço a pedaço, sem cópia
|
|
133
|
+
(o `verify_hash` ainda junta, porque lê fora de ordem). O `import_column` nem tenta exportar uma coluna em vários
|
|
134
|
+
pedaços (a exportação falharia e custava tempo).
|
|
135
|
+
- com 300 colunas o orçamento dava janelas de ~52 mil linhas, menores que os row groups de 100 mil: cada row group
|
|
136
|
+
era decodificado duas vezes. A janela agora nunca é menor que o maior row group (com uma nota no plano).
|
|
137
|
+
- Segundo benchmark: contagens certas; 10M x 300 em 43 s (0,86x o datacompolars, `ok`), 10M x 10 e 1M x 300 `ok`;
|
|
138
|
+
10M x 50 (1,13x) e 1M x 50 (1,58x) ainda mais lentos. O tempo estava no hash (30 s dos 43 em 10M x 300), e não no
|
|
139
|
+
kernel: ler as colunas uma a uma pelo Python (`import_column`) custa ~0,3 ms por coluna, e uma janela de 300 colunas
|
|
140
|
+
e 100 mil linhas tinha só 2 blocos de 65.536 linhas para 16 threads. Corrigido:
|
|
141
|
+
- `import_frame` (`core/arrow.py`) exporta o pedaço do DataFrame inteiro de uma vez (`__arrow_c_stream__` do
|
|
142
|
+
DataFrame, um struct do Arrow) e `dc_describe_batch` (`cpp/batch.cpp`) preenche os descritores de todas as colunas
|
|
143
|
+
numa chamada só. Sem lote único (Categorical, Polars antigo, vários pedaços), volta à leitura coluna a coluna.
|
|
144
|
+
- o hash divide as linhas em tarefas de 4.096 a 65.536 linhas, ao menos ~4 por thread (o resultado não muda).
|
|
145
|
+
- Terceiro benchmark: o hash caiu para 6 s em 10M x 300 (de 30) e o pico para ~1,2 GB (de 1,6), mas o tempo total
|
|
146
|
+
ficou em 44,5 s: agora é a leitura que não fica escondida (`read_wait` 30 s). Ela crescia mais que as linhas
|
|
147
|
+
(10M x 300 leva 10x as linhas e 10x as janelas de 1M x 300, mas ~17x a leitura) porque cada janela lia a fonte
|
|
148
|
+
inteira: o Polars abria o rodapé dos 100 arquivos de cada lado a cada uma das 100 janelas (com 300 colunas, um
|
|
149
|
+
rodapé grande). Agora o planejamento anota a faixa da 1ª coluna da chave de cada arquivo (uma passada pela chave) e
|
|
150
|
+
cada janela lê só os arquivos que encostam nela (`Source.scan`, com as mesmas conversões de tipo da fonte).
|
|
151
|
+
- Diagnóstico da leitura (`tools/diag_read.py`): o Polars não tem custo fixo alto por leitura; o tempo é proporcional
|
|
152
|
+
às células (~3,5 ns cada, também com o disco frio), e o corte de arquivos por janela ajuda pouco (fica). O que pesa
|
|
153
|
+
é o tamanho: os row groups de 100 mil linhas dos dados de benchmark eram arbitrários.
|
|
154
|
+
- Dados de benchmark com o row group padrão do Polars (`Spec.row_group_size = None`; a pasta ganha `_rg<n>` quando o
|
|
155
|
+
row group é fixo) e arquivos de ~100 milhões de células (antes 30 milhões: um row group por arquivo nas tabelas
|
|
156
|
+
largas). O `read_experiment` (fase 0c) continua com os row groups de 100 mil.
|
|
157
|
+
- Grupos de colunas: quando um row group dos dois lados não cabe no `memory_limit`, cada janela (um row group) é lida
|
|
158
|
+
em grupos de colunas seguidas, cada um com a chave, e o hash de linha continua de um grupo para o outro
|
|
159
|
+
(`dc_row_hash(..., first, ...)`, ABI 5: mesmos bits que todas as colunas juntas). A memória passa a depender do
|
|
160
|
+
grupo, não do row group. `Plan.column_groups`/`split_columns`, `WindowReader(groups=...)`/`pieces()`,
|
|
161
|
+
`RowCounts.column_groups`. O `verify_hash` confere célula a célula grupo a grupo e junta os pares diferentes.
|
|
162
|
+
- `tools/check.py`: formatação, lint, tipos e testes em sequência, parando no primeiro que falhar; `--fix` e
|
|
163
|
+
`--then <ferramenta>` (benchmarks no fim).
|
|
164
|
+
- Benchmark global (dados novos, linha de base refeita): divergentes certos em tudo; todos os picos abaixo de 1 GB
|
|
165
|
+
(10M x 300: 588 MB em 3 grupos de colunas, 47,1 s, 0,94x o datacompolars); mas 1M x 50 (1,41x) e 10M x 50
|
|
166
|
+
(1,56x) mais lentos, com a leitura dominando e só ~2/3 do memory_limit em uso. Ajustes:
|
|
167
|
+
- `IN_FLIGHT` de 4 para 3 (o pico medido, já sem o bug das três janelas e com o alocador configurado, é de 1,9 a
|
|
168
|
+
2,9x a maior janela): janelas ~1/3 maiores, menos leituras.
|
|
169
|
+
- os dois lados de cada janela lidos ao mesmo tempo (`WindowReader(parallel_sides=True)`): uma leitura do Polars
|
|
170
|
+
de poucos row groups não ocupa todas as threads.
|
|
171
|
+
- Resultado: 10M x 300 em 36,9 s (0,74x o datacompolars) e 917 MB; 10M x 50 de 6,7 para 5,4 s (1,27x); 1M x 50 quase
|
|
172
|
+
igual (1,37x). Mas a contagem com chave de texto em 10M x 10 passou do limite (1.086 MB): `IN_FLIGHT` = 3,5. Novas
|
|
173
|
+
etapas nos tempos: `setup` (abrir as fontes e alinhar os schemas) e `other` (o que nenhuma etapa mediu; em 10M x 50
|
|
174
|
+
eram ~1,6 s dos 5,4).
|
|
175
|
+
- Com `IN_FLIGHT` = 3,5: 10M x 300 em 36,5 s (0,73x), 1M x 300 0,88x, 10M x 10 1,01x, 10M x 50 1,01x (inconclusivo),
|
|
176
|
+
1M x 50 1,31x (mais lento). O `other` era grande (10M x 300: 8,7 s; 10M x 50: 2,1 s): soltar a memória de cada
|
|
177
|
+
janela na thread principal, com o alocador devolvendo as páginas na hora. Agora as janelas são soltas numa thread
|
|
178
|
+
à parte (`_dispose`).
|
|
179
|
+
- Os relatórios dão o pico em MiB (`MB` = 1.048.576 bytes) e o `memory_limit` "1GB" é 10^9 bytes = 954 MiB: alguns
|
|
180
|
+
picos passavam do limite por até ~3% (10M x 50: 959 MiB; contagem com chave de texto em 10M x 10: até 985 MiB).
|
|
181
|
+
O `bench_hash` ganhou a coluna `cabe` (o maior pico contra o limite).
|
|
182
|
+
- Soltar as janelas noutra thread tirou o `other` (10M x 300: 8,7 → 2,6 s), mas o tempo total não mudou: o custo foi
|
|
183
|
+
para a leitura em paralelo (o gargalo agora é o processador, lendo). Fica, porque não atrapalha. Dois picos ainda
|
|
184
|
+
passavam do limite (10M x 50: 983 MiB; chave de texto em 10M x 10: 992 MiB; o limite de 1GB é 954 MiB):
|
|
185
|
+
`SAFETY` de 0,8 para 0,7 (janelas ~12% menores).
|
|
186
|
+
- Com `SAFETY` = 0,7: todos os picos dentro do limite (o maior: 10M x 50, 904 MiB) e os tempos iguais. Fase 2
|
|
187
|
+
fechada: 10M x 300 em 36,5 s (0,73x o datacompolars, 686 MiB); ficam para a fase 5 o 1M x 50 (1,32x) e o
|
|
188
|
+
10M x 50 (1,03x, inconclusivo).
|
|
189
|
+
|
|
190
|
+
### Fase 3 — coluna a coluna (ABI 6)
|
|
191
|
+
- Kernel `dc_compare_column` (`cpp/compare.cpp`) e `core/compare.py` (`compare_column`, `tolerance_for`): uma coluna,
|
|
192
|
+
par a par, exata (o `eq_missing` do Polars, NaN = NaN) ou com tolerância nas numéricas (`|l - r| <= abs_tol +
|
|
193
|
+
rel_tol * |r|`, com a aritmética do Polars: inteiros no próprio tipo, com volta; Float32 em float; Decimal pelo
|
|
194
|
+
inteiro de 128 bits). Nulo dos dois lados = igual. Conferido nos testes contra a própria expressão do datacompolars
|
|
195
|
+
rodando no Polars instalado, em todos os tipos inteiros, nos reais e no Decimal.
|
|
196
|
+
- `compare_rows(..., abs_tol=..., rel_tol=..., ignore_case=..., ignore_spaces=...)` e `RowCounts.column_mismatches`
|
|
197
|
+
(divergências por coluna). Em cada grupo de colunas de uma janela: as exatas pelo hash de linha e, só nos pares
|
|
198
|
+
divergentes (poucos: um `gather` do Polars), a contagem coluna a coluna; as com tolerância, uma a uma, em todos os
|
|
199
|
+
pares. Cada grupo tem o seu hash (um par diverge se diverge em algum grupo), então a contagem por coluna sai
|
|
200
|
+
enquanto o grupo está em memória.
|
|
201
|
+
- Texto com `ignore_case`/`ignore_spaces`: normalizado na leitura com as mesmas expressões do datacompolars
|
|
202
|
+
(`str.to_lowercase`, `str.replace_all(r"\s+", "")`), e depois comparado exato (pelo hash). O plano previa caixa e
|
|
203
|
+
espaços no C++ (ASCII e Latin-1 rápidos, UTF-8 completo); com as expressões do Polars o resultado bate com o do
|
|
204
|
+
datacompolars em qualquer texto Unicode, e o texto normalizado ainda vai pelo hash. O C++ fica para a fase 5, se o
|
|
205
|
+
perfil mandar.
|
|
206
|
+
- Testes: todos os 21 gabaritos agora (tolerâncias e opções de texto inclusive), também por coluna e com
|
|
207
|
+
`verify_hash`; grupos de colunas com tolerância.
|
|
208
|
+
- `tools/bench_hash.py --mode columnwise`: `abs_tol=1e-12`, contra o columnwise da linha de base, conferindo também
|
|
209
|
+
as contagens por coluna; resultado em `docs/benchmarks/fase3.{md,json}`.
|
|
210
|
+
- Os testes contra o Polars confirmaram a aritmética dos inteiros (com volta, também os sem sinal) e acharam uma
|
|
211
|
+
diferença nos reais: o Polars compara reais em ordem total (NaN maior que tudo, NaN = NaN), não pelo IEEE. Na
|
|
212
|
+
tolerância isso importa: com r infinito, `rel_tol * |r|` pode dar `0 * inf = NaN`, e para o Polars `x <= NaN` é
|
|
213
|
+
verdade (o datacompolars dá esses pares como iguais). O kernel agora compara como o Polars (`polars_le`).
|
|
214
|
+
- `column_mismatches` lista todas as colunas comparadas (as sem divergência com 0); o datacompolars lista só as
|
|
215
|
+
com divergência.
|
|
216
|
+
- CI (min-deps, Polars 1.30): o Decimal exportado como Series sai com o formato `_pli128` (o mesmo inteiro de 128
|
|
217
|
+
bits); o hash passava (o DataFrame inteiro sai como `d:`), mas o `verify_hash` e a coluna a coluna recusavam a
|
|
218
|
+
coluna. `_pli128` aceito como Decimal no `import_column` e no `dc_describe_batch`.
|
|
219
|
+
- Benchmark coluna a coluna (`abs_tol = 1e-12`): contagens certas (também por coluna) em tudo; 10M x 300 em 37,2 s
|
|
220
|
+
(0,57x o columnwise do datacompolars, 805 MiB); 1M x 50, 1M x 300, 10M x 10 e 10M x 50 `ok`; 1M x 10
|
|
221
|
+
inconclusivo (1,17x). Um pico de 10M x 50 passou o limite em 1% (964 MiB): `SAFETY` = 0,65.
|
|
222
|
+
|
|
223
|
+
### Fase 4a — API pública
|
|
224
|
+
- `dc.compare(left, right, on, options=None, **kwargs)`: a função pública. As opções em `CompareOptions` (pydantic,
|
|
225
|
+
`extra="forbid"`) ou como argumentos nomeados; com os dois, os nomeados ganham.
|
|
226
|
+
- `CompareOptions`: `names`, `case_sensitive_columns`, `cast`, `abs_tol`/`rel_tol` (número ou por coluna),
|
|
227
|
+
`ignore_whitespace`, `ignore_case`, `details`, `samples`, `memory_limit`, `window_rows`, `threads`, `verify_hash`.
|
|
228
|
+
- `ComparisonResult` (dataclasses imutáveis): `rows`, `columns` como mapeamento por nome (`result.columns["v"]`, com
|
|
229
|
+
`mismatches`, `rate`, `dtype`, `tolerance` e as amostras em `pl.DataFrame`), `columns.mismatched()` na ordem do
|
|
230
|
+
datacompolars, `errors`, `warnings`, `execution`, `is_match`, `aborted`, `to_dict()`/`to_json()`.
|
|
231
|
+
- Amostras: as primeiras `samples` linhas divergentes de cada coluna, na ordem da chave, com cabeçalhos
|
|
232
|
+
`[*chave, "<coluna> (<esquerda>)", "<coluna> (<direita>)"]`, como no datacompolars. Com `ignore_case`/
|
|
233
|
+
`ignore_whitespace`, o texto normalizado é lido ao lado do original e as amostras mostram o valor de verdade.
|
|
234
|
+
- Erros fatais no resultado, não em exceção (como no datacompolars): chave que falta, chave de tipos diferentes sem
|
|
235
|
+
`cast`, chave repetida, nomes que colidem em minúsculas. Avisos: nenhuma coluna além da chave; tolerância por coluna
|
|
236
|
+
com nome que não é comparado.
|
|
237
|
+
- Chave repetida sempre aborta: não há `check_unique_keys=False` (o plano previa). A checagem sai de graça do
|
|
238
|
+
alinhamento das chaves, e desligá-la só serviria para contagens infladas.
|
|
239
|
+
- Testes: os 21 gabaritos pela API, inclusive as amostras valor a valor, com uma janela e com janelas de 2 linhas.
|
|
240
|
+
|
|
241
|
+
### Fase 4b — relatório e publicação
|
|
242
|
+
- `result.report(format=None, path=None, *, width=100, style="auto", max_columns=20, max_sample_columns=10,
|
|
243
|
+
print_output=False)`: o relatório do datacompolars nos mesmos quatro formatos (texto, Markdown, HTML, JSON),
|
|
244
|
+
portado para o resultado novo. Sem `format`, sai da extensão de `path`; com `path`, também salva. Diferente do
|
|
245
|
+
datacompolars, não imprime sozinho (`print_output=True` imprime).
|
|
246
|
+
- A seção "Execution" diz como cada coluna foi comparada (hash de linha ou coluna a coluna com tolerância), as
|
|
247
|
+
janelas, os grupos de colunas, o `memory_limit` e a maior janela, os tempos por etapa e as versões.
|
|
248
|
+
- README: uso e a tabela de migração do datacompolars.
|
|
249
|
+
- Versão `0.1.0rc1` para o TestPyPI; o teste do TestPyPI nas máquinas limpas agora roda uma comparação de verdade e
|
|
250
|
+
o relatório nos quatro formatos.
|
|
251
|
+
|
|
252
|
+
### Fase 5a — desempenho (antes da 0.1.0)
|
|
253
|
+
- Planejamento das janelas: as linhas de um Parquet saem do rodapé (sem ler dados) e as consultas que restam (a
|
|
254
|
+
amostra de cada lado para os bytes por linha, as chaves nulas, a chave no começo de cada row group) rodam juntas,
|
|
255
|
+
em paralelo, em vez de uma depois da outra (em 1M x 50, o planejamento levava ~0,14 s de 0,59 s).
|
|
256
|
+
- `tools/diag_profile.py`: o cProfile de uma comparação pela API, com as etapas de cada execução.
|
|
257
|
+
- O planejamento faz uma rodada só de consultas ao Polars: a chave no começo de cada row group e a faixa de chaves
|
|
258
|
+
de cada arquivo não dependem do tamanho das janelas e vão junto com a amostra, as chaves nulas e as linhas.
|
|
259
|
+
- As colunas da chave de cada janela numa exportação só do Polars por lado (`align_frames`), em vez de uma por
|
|
260
|
+
coluna.
|
|
261
|
+
- Amostras montadas a partir de listas do Python (uma conversão por lado), em vez de indexar uma Series do Polars por
|
|
262
|
+
valor (cada indexação passa por uma consulta).
|
|
263
|
+
- `tools/check.py --then "bench_hash --cases 1000000x50" diag_profile`: cada ferramenta entre aspas leva os próprios
|
|
264
|
+
argumentos.
|
|
265
|
+
- Resultado (fase 2, caminho exato, `memory_limit="1GB"`): 1M x 50 de 1,39x para 1,05x (ok) e o pico de 801 para
|
|
266
|
+
724 MiB; 1M x 10 0,81x, 1M x 300 0,81x, 10M x 10 0,96x, 10M x 300 0,66x (32,9 s), todos ok; 10M x 50 1,20x,
|
|
267
|
+
inconclusivo. Contagens certas em todos, também no caminho coluna a coluna (fase 3).
|
|
268
|
+
- Bytes por linha: os tipos de largura fixa (inteiros, reais, datas, Decimal, booleano) pelo tipo; só as colunas de
|
|
269
|
+
largura variável (texto, Categorical) vão para a amostra. A amostra lia o primeiro row group inteiro de todas as
|
|
270
|
+
colunas: em 1M x 50, ~50 ms dos ~80 ms do planejamento.
|
|
271
|
+
- `execution.window_bytes` pelos bytes por linha do plano, sem o `estimated_size` do Polars (alguns ms por janela,
|
|
272
|
+
segurando o GIL enquanto a comparação roda).
|
|
273
|
+
- Contagem por coluna nos pares divergentes: `dc_compare_pairs` (ABI 8) compara todas as colunas numa chamada,
|
|
274
|
+
lendo direto dos pedaços de memória da janela que o hash de linha já leu (sem reunir as linhas no Polars, cujas
|
|
275
|
+
threads estão ocupadas lendo a janela seguinte); antes, eram uma `dc_compare_column` e duas exportações do Polars
|
|
276
|
+
por coluna (em 1M x 50, ~0,12 s por comparação). `dc_compare_rows` (`compare_frames`) faz o mesmo com as linhas
|
|
277
|
+
já reunidas.
|
|
278
|
+
- Amostras: os pares divergentes vão para a ordem da chave uma vez por janela, e as amostras de cada coluna saem das
|
|
279
|
+
linhas já reunidas para a contagem (as primeiras em que ela diverge); antes, um `select`, um `sort` e um `head` do
|
|
280
|
+
Polars por coluna (em 1M x 50 com `samples=5`, ~0,13 s).
|
|
281
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.20)
|
|
2
|
+
project(nini_datacomp LANGUAGES CXX)
|
|
3
|
+
|
|
4
|
+
# Biblioteca compartilhada com ABI em C (cpp/datacomp.h), carregada pelo Python via ctypes.
|
|
5
|
+
# Gera libdatacomp.so (Linux), libdatacomp.dylib (macOS) ou datacomp.dll (Windows) dentro do pacote nini_datacomp.
|
|
6
|
+
|
|
7
|
+
set(CMAKE_CXX_STANDARD 17)
|
|
8
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
9
|
+
set(CMAKE_CXX_EXTENSIONS OFF)
|
|
10
|
+
set(CMAKE_CXX_VISIBILITY_PRESET hidden) # só as funções DC_EXPORT ficam visíveis
|
|
11
|
+
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)
|
|
12
|
+
if(NOT CMAKE_BUILD_TYPE)
|
|
13
|
+
set(CMAKE_BUILD_TYPE Release)
|
|
14
|
+
endif()
|
|
15
|
+
|
|
16
|
+
# MSVC: runtime do C++ embutido na DLL (/MT). Sem isto, datacomp.dll depende do msvcp140.dll, que o Python não traz e
|
|
17
|
+
# que só existe em máquinas com o Visual C++ Redistributable instalado.
|
|
18
|
+
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
|
19
|
+
|
|
20
|
+
option(DC_NATIVE "Compilar para a CPU desta máquina (-march=native); nunca para wheels publicados" OFF)
|
|
21
|
+
|
|
22
|
+
file(GLOB DC_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/cpp/*.cpp")
|
|
23
|
+
add_library(datacomp SHARED ${DC_SOURCES})
|
|
24
|
+
target_include_directories(datacomp PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/cpp")
|
|
25
|
+
|
|
26
|
+
set(DC_X86 OFF)
|
|
27
|
+
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|amd64|x64")
|
|
28
|
+
set(DC_X86 ON)
|
|
29
|
+
endif()
|
|
30
|
+
|
|
31
|
+
# Mesmo resultado em qualquer sistema: a tolerância |l - r| <= abs_tol + rel_tol * |r| não pode depender da CPU nem do
|
|
32
|
+
# compilador, inclusive exatamente na borda. Sem fusão de multiplicação e soma (-ffp-contract=off: FMA arredonda
|
|
33
|
+
# diferente, e o ARM e as CPUs com FMA fundiriam a*b + c por conta própria) e sem matemática "rápida".
|
|
34
|
+
# -fno-math-errno e -fno-trapping-math não mudam nenhum resultado (só dizem que ninguém lê errno nem captura exceções
|
|
35
|
+
# de ponto flutuante) e deixam os laços vetorizar.
|
|
36
|
+
if(MSVC AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
|
37
|
+
# clang-cl (DC_CLANGCL=1 no Windows): opções do clang passadas com /clang:
|
|
38
|
+
target_compile_options(datacomp PRIVATE /utf-8 /W4 /clang:-O3 /clang:-ffp-contract=off /clang:-fno-math-errno /clang:-fno-trapping-math)
|
|
39
|
+
if(DC_NATIVE)
|
|
40
|
+
target_compile_options(datacomp PRIVATE /clang:-march=native)
|
|
41
|
+
elseif(DC_X86)
|
|
42
|
+
target_compile_options(datacomp PRIVATE /clang:-march=x86-64-v2)
|
|
43
|
+
endif()
|
|
44
|
+
target_compile_definitions(datacomp PRIVATE _CRT_SECURE_NO_WARNINGS)
|
|
45
|
+
elseif(MSVC)
|
|
46
|
+
# /utf-8: os comentários têm acentos; /O2 já vem do Release.
|
|
47
|
+
# /fp:precise (padrão desde o VS 2022) não funde multiplicação e soma.
|
|
48
|
+
target_compile_options(datacomp PRIVATE /utf-8 /W4 /permissive- /Zc:__cplusplus /fp:precise)
|
|
49
|
+
target_compile_definitions(datacomp PRIVATE _CRT_SECURE_NO_WARNINGS)
|
|
50
|
+
else()
|
|
51
|
+
target_compile_options(datacomp PRIVATE -O3 -Wall -Wextra -Wpedantic -ffp-contract=off -fno-math-errno -fno-trapping-math)
|
|
52
|
+
if(DC_NATIVE)
|
|
53
|
+
target_compile_options(datacomp PRIVATE -march=native)
|
|
54
|
+
elseif(DC_X86)
|
|
55
|
+
# base dos wheels: x86-64-v2 (SSE4.2/POPCNT). AVX2 entra depois (fase 5) como segunda cópia escolhida na hora,
|
|
56
|
+
# só nos kernels em que o perfil mandar; FMA fica de fora de propósito (veja acima).
|
|
57
|
+
target_compile_options(datacomp PRIVATE -march=x86-64-v2)
|
|
58
|
+
endif()
|
|
59
|
+
endif()
|
|
60
|
+
|
|
61
|
+
if(WIN32)
|
|
62
|
+
set_target_properties(datacomp PROPERTIES PREFIX "") # datacomp.dll
|
|
63
|
+
endif()
|
|
64
|
+
|
|
65
|
+
# scikit-build-core coloca o conteúdo de CMAKE_INSTALL_PREFIX na raiz do wheel
|
|
66
|
+
install(TARGETS datacomp
|
|
67
|
+
LIBRARY DESTINATION nini_datacomp
|
|
68
|
+
RUNTIME DESTINATION nini_datacomp)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright 2026 Nini (B. L. Z. Rosa)
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|