docs: add teaching examples and video project

This commit is contained in:
Yaossg 2026-10-11 12:29:28 +08:00
parent e93bc20b0a
commit 7a06f94577
42 changed files with 8022 additions and 0 deletions

2
show/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/bin/
__pycache__/

52
show/01_cpu.cpp Normal file
View file

@ -0,0 +1,52 @@
#include "common.hpp"
#include <atomic>
#include <thread>
// Original seed -> x -> z -> cells nesting, counting every matching anchor.
std::vector<show::Hit> scan(const show::Args& a) {
std::atomic<uint64_t> next{0}, count{0};
std::atomic<bool> overflow{false};
std::vector<std::vector<show::Hit>> local(a.cpuThreads);
std::vector<std::thread> workers;
for(int t=0;t<a.cpuThreads;++t) workers.emplace_back([&,t] {
for (;;) {
const uint64_t s=next.fetch_add(1,std::memory_order_relaxed);
if(s>=a.seeds) break;
for(uint64_t xi=0;xi<a.width;++xi) for(uint64_t zi=0;zi<a.height;++zi) {
const int32_t x=int32_t(int64_t(a.x0)+xi), z=int32_t(int64_t(a.z0)+zi);
if(show::solid<1>(a.seed+s,x,z,a.n)) {
if(count.fetch_add(1,std::memory_order_relaxed)<a.capacity)
local[t].push_back({a.seed+s,x,z});
else overflow.store(true,std::memory_order_relaxed);
}
}
}
});
for(auto& w:workers) w.join();
if(overflow.load()) throw std::runtime_error("hit buffer overflow; raise --capacity");
std::vector<show::Hit> result;
for(const auto& v:local) result.insert(result.end(),v.begin(),v.end());
return result;
}
int main(int argc,char** argv) {
try {
const auto a=show::parse(argc,argv);
std::cout.precision(10);
auto expected=scan(a); // full-workload warmup
std::sort(expected.begin(),expected.end());
std::vector<double> times;
for(int r=0;r<a.repeats;++r) {
const auto begin=std::chrono::steady_clock::now();
auto hits=scan(a);
times.push_back(show::ms(begin)); // thread creation + scan + collection, excludes sort
std::sort(hits.begin(),hits.end());
if(hits!=expected) throw std::runtime_error("CPU repeats disagree");
}
std::cout << '{'; show::workload(a);
std::cout << ",\"mode\":\"01_cpu\",\"cpu_threads\":" << a.cpuThreads
<< ",\"samples_ms\":"; show::array(times);
std::cout << ",\"median_ms\":" << show::median(times) << ",\"hits\":";
show::results(expected); std::cout << "}\n";
} catch(const std::exception& e) { std::cerr << e.what() << '\n'; return 1; }
}

13
show/02_cuda_seed.cuh Normal file
View file

@ -0,0 +1,13 @@
#pragma once
// CUDA translation of stage 01: one work item is one seed, its positions are serial.
__global__ void seedKernel(DeviceArgs a, show::Hit* hits, unsigned long long* count) {
const uint64_t stride=uint64_t(gridDim.x)*blockDim.x;
for(uint64_t s=uint64_t(blockIdx.x)*blockDim.x+threadIdx.x;s<a.seeds;s+=stride) {
for(uint64_t xi=0;xi<a.width;++xi) for(uint64_t zi=0;zi<a.height;++zi) {
const int32_t x=int32_t(int64_t(a.x0)+int64_t(xi));
const int32_t z=int32_t(int64_t(a.z0)+int64_t(zi));
if(show::solid<2>(a.seed+s,x,z,a.n)) emit(a,hits,count,a.seed+s,x,z);
}
}
}

52
show/03_cuda_position.cuh Normal file
View file

@ -0,0 +1,52 @@
#pragma once
// Stage 03: one work item is a fixed (x,z). Its private axis terms are computed
// once, then reused for every seed. No host table or global coordinate table.
template<int N>
__device__ __forceinline__ bool solidAxes(uint64_t seed,const uint64_t (&xs)[N],
const uint64_t (&zs)[N]) {
#pragma unroll
for(int dx=0;dx<N;++dx) {
#pragma unroll
for(int dz=0;dz<N;++dz) {
const uint64_t t=xs[dx]+zs[dz];
if(!show::original(seed,t)) return false;
}
}
return true;
}
template<int N>
__global__ void coordinateKernel(DeviceArgs a,show::Hit* hits,unsigned long long* count) {
const uint64_t area=a.width*a.height;
const uint64_t stride=uint64_t(gridDim.x)*blockDim.x;
for(uint64_t p=uint64_t(blockIdx.x)*blockDim.x+threadIdx.x;p<area;p+=stride) {
const int32_t x=int32_t(int64_t(a.x0)+int64_t(p/a.height));
const int32_t z=int32_t(int64_t(a.z0)+int64_t(p%a.height));
uint64_t xs[N],zs[N];
#pragma unroll
for(int i=0;i<N;++i) {
xs[i]=show::xTerm(x+i);
zs[i]=show::zTerm(z+i);
}
for(uint64_t s=0;s<a.seeds;++s)
if(solidAxes<N>(a.seed+s,xs,zs)) emit(a,hits,count,a.seed+s,x,z);
}
}
// Compile-time sizes let the compiler scalarize/unroll the private axis arrays.
// Dispatch once on the host, outside the kernel's timed seed loop.
void launchCoordinate(int blocks,int threads,DeviceArgs a,show::Hit* hits,
unsigned long long* count) {
switch(a.n) {
case 1: coordinateKernel<1><<<blocks,threads>>>(a,hits,count); break;
case 2: coordinateKernel<2><<<blocks,threads>>>(a,hits,count); break;
case 3: coordinateKernel<3><<<blocks,threads>>>(a,hits,count); break;
case 4: coordinateKernel<4><<<blocks,threads>>>(a,hits,count); break;
case 5: coordinateKernel<5><<<blocks,threads>>>(a,hits,count); break;
case 6: coordinateKernel<6><<<blocks,threads>>>(a,hits,count); break;
case 7: coordinateKernel<7><<<blocks,threads>>>(a,hits,count); break;
case 8: coordinateKernel<8><<<blocks,threads>>>(a,hits,count); break;
default: throw std::runtime_error("invalid pattern size");
}
}

View file

@ -0,0 +1,57 @@
#pragma once
// Common rectangle path: use block coordinates directly, avoiding 64-bit tile divisions.
// One thread keeps its coordinate while scanning all seeds, as in stage 03.
__global__ void outerRectKernel(DeviceArgs a,show::Hit* hits,unsigned long long* count) {
__shared__ uint64_t xs[16+show::MAX_N-1],zs[16+show::MAX_N-1];
const unsigned lane=threadIdx.y*16+threadIdx.x;
const uint64_t xb=uint64_t(blockIdx.x)*16,zb=uint64_t(blockIdx.y)*16;
if(lane<unsigned(16+a.n-1)) {
xs[lane]=show::xTerm(int32_t(int64_t(a.x0)+int64_t(xb+lane)));
zs[lane]=show::zTerm(int32_t(int64_t(a.z0)+int64_t(zb+lane)));
}
__syncthreads();
const uint64_t xi=xb+threadIdx.x,zi=zb+threadIdx.y;
if(xi>=a.width || zi>=a.height) return;
for(uint64_t s=0;s<a.seeds;++s) {
bool ok=true;
for(int dx=0;dx<a.n && ok;++dx) for(int dz=0;dz<a.n;++dz) {
if(!show::original(a.seed+s,xs[threadIdx.x+dx]+zs[threadIdx.y+dz])) {
ok=false;break;
}
}
if(ok) emit(a,hits,count,a.seed+s,int32_t(int64_t(a.x0)+int64_t(xi)),
int32_t(int64_t(a.z0)+int64_t(zi)));
}
}
// One block handles a 16x16 anchor tile; each axis needs 16+n-1 terms.
// Fallback for unusually tall rectangles exceeding the grid.y limit.
__global__ void outerKernel(DeviceArgs a, show::Hit* hits, unsigned long long* count) {
__shared__ uint64_t xs[16+show::MAX_N-1], zs[16+show::MAX_N-1];
const uint64_t tilesX=(a.width+15)/16, tilesZ=(a.height+15)/16;
const uint64_t total=tilesX*tilesZ;
const unsigned lane=threadIdx.y*16+threadIdx.x;
for(uint64_t tile=blockIdx.x;tile<total;tile+=gridDim.x) {
const uint64_t tx=tile/tilesZ, tz=tile%tilesZ;
if(lane<unsigned(16+a.n-1)) {
xs[lane]=show::xTerm(int32_t(int64_t(a.x0)+int64_t(tx*16+lane)));
zs[lane]=show::zTerm(int32_t(int64_t(a.z0)+int64_t(tz*16+lane)));
}
__syncthreads();
const uint64_t xi=tx*16+threadIdx.x, zi=tz*16+threadIdx.y;
for(uint64_t s=0;s<a.seeds;++s) {
bool ok=xi<a.width && zi<a.height;
for(int dx=0;dx<a.n && ok;++dx) for(int dz=0;dz<a.n;++dz) {
if(!show::original(a.seed+s,xs[threadIdx.x+dx]+zs[threadIdx.y+dz])) {
ok=false; break;
}
}
if(ok) emit(a,hits,count,a.seed+s,int32_t(int64_t(a.x0)+int64_t(xi)),
int32_t(int64_t(a.z0)+int64_t(zi)));
}
// No final barrier is needed for a block assigned only one tile.
// When a giant workload exceeds the grid limit, synchronize before reuse.
if(tile+gridDim.x<total) __syncthreads();
}
}

247
show/07_residue.cu Normal file
View file

@ -0,0 +1,247 @@
// Teaching snapshot extracted from research/slime_position_residue_cuda.cu.
// Shared Java arithmetic/profile solvers remain in ../research; see README.md for scope.
#include "slime_solver_core.hpp"
#include <cuda_runtime.h>
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
namespace sr = slime_research;
namespace {
constexpr int kResidueBits = 18;
constexpr uint32_t kPeriod = 1U << kResidueBits;
constexpr uint32_t kResidueMask = kPeriod - 1U;
constexpr int kMaxSize = 8;
struct ResiduePair {
uint32_t x;
uint32_t z;
};
struct Position {
int32_t x;
int32_t z;
};
struct Args {
uint64_t seed = 61692151016558ULL;
int32_t x0 = -1875000;
int32_t x1 = 1874999;
int32_t z0 = -1875000;
int32_t z1 = 1874999;
int size = 5;
int threads = 256;
uint32_t maxResidues = 1U << 20;
};
void cudaCheck(cudaError_t status, const char* what) {
if (status != cudaSuccess) {
throw std::runtime_error(std::string(what) + ": " + cudaGetErrorString(status));
}
}
__global__ void residueKernel(
uint64_t seed,
const uint32_t* xTerms,
const uint32_t* zTerms,
int n,
ResiduePair* output,
unsigned long long* count,
uint32_t capacity
) {
constexpr uint64_t total = uint64_t(kPeriod) * kPeriod;
const uint64_t stride = uint64_t(gridDim.x) * blockDim.x;
for (uint64_t index = uint64_t(blockIdx.x) * blockDim.x + threadIdx.x;
index < total;
index += stride) {
const uint32_t xr = static_cast<uint32_t>(index >> kResidueBits);
const uint32_t zr = static_cast<uint32_t>(index) & kResidueMask;
const uint32_t* xs = xTerms + uint64_t(xr) * n;
const uint32_t* zs = zTerms + uint64_t(zr) * n;
bool possible = true;
for (int dx = 0; dx < n && possible; ++dx) {
for (int dz = 0; dz < n; ++dz) {
const uint64_t term = (xs[dx] + zs[dz]) & kResidueMask;
// A multiple of ten is even, so bit 17 of the first next(31)
// result must be zero. This bit depends only on seed, x, and z
// modulo 2^18.
if (((sr::nextStateFromTerm(seed, term) >> 17) & 1ULL) != 0ULL) {
possible = false;
break;
}
}
}
if (possible) {
const unsigned long long slot = atomicAdd(count, 1ULL);
if (slot < capacity) output[slot] = ResiduePair{xr, zr};
}
}
}
Args parseArgs(int argc, char** argv) {
Args args;
for (int i = 1; i < argc; ++i) {
const std::string key = argv[i];
if (key == "--help") {
std::cout
<< "Usage: slime_position_residue_cuda [options]\n"
<< " --seed N --x0 N --x1 N --z0 N --z1 N\n"
<< " -n N --threads N --max-residues N\n";
std::exit(0);
}
if (i + 1 >= argc) throw std::runtime_error("missing value after " + key);
const std::string value = argv[++i];
if (key == "--seed") args.seed = std::stoull(value) & sr::kMask48;
else if (key == "--x0") args.x0 = std::stoi(value);
else if (key == "--x1") args.x1 = std::stoi(value);
else if (key == "--z0") args.z0 = std::stoi(value);
else if (key == "--z1") args.z1 = std::stoi(value);
else if (key == "-n" || key == "--size") args.size = std::stoi(value);
else if (key == "--threads") args.threads = std::stoi(value);
else if (key == "--max-residues") args.maxResidues = std::stoul(value);
else throw std::runtime_error("unknown option " + key);
}
if (args.x1 < args.x0 || args.z1 < args.z0) throw std::runtime_error("invalid bounds");
if (args.size <= 0 || args.size > kMaxSize) throw std::runtime_error("size must be 1..8");
if (args.threads <= 0 || args.threads > 1024) throw std::runtime_error("invalid threads");
if (args.maxResidues == 0) throw std::runtime_error("max-residues must be positive");
if (int64_t(args.x1) + args.size - 1 > INT32_MAX ||
int64_t(args.z1) + args.size - 1 > INT32_MAX) {
throw std::runtime_error("coordinate footprint exceeds int32");
}
return args;
}
std::vector<uint32_t> buildAxisTerms(bool xAxis, int n) {
std::vector<uint32_t> result(uint64_t(kPeriod) * n);
for (uint32_t residue = 0; residue < kPeriod; ++residue) {
for (int offset = 0; offset < n; ++offset) {
const int32_t coordinate = static_cast<int32_t>(residue + offset);
const uint64_t term = xAxis ? sr::coordinateXTerm(coordinate)
: sr::coordinateZTerm(coordinate);
result[uint64_t(residue) * n + offset] = static_cast<uint32_t>(term) & kResidueMask;
}
}
return result;
}
int64_t firstCongruent(int32_t lower, uint32_t residue) {
int64_t lowerResidue = int64_t(lower) % kPeriod;
if (lowerResidue < 0) lowerResidue += kPeriod;
const int64_t delta = (int64_t(residue) - lowerResidue + kPeriod) % kPeriod;
return int64_t(lower) + delta;
}
bool exactSolid(uint64_t seed, int32_t x, int32_t z, int n) {
for (int dx = 0; dx < n; ++dx) {
for (int dz = 0; dz < n; ++dz) {
if (!sr::exactIsSlimeFromTerm(seed, sr::coordinateTerm(x + dx, z + dz))) {
return false;
}
}
}
return true;
}
} // namespace
int main(int argc, char** argv) {
try {
const Args args = parseArgs(argc, argv);
const auto prepareBegin = std::chrono::steady_clock::now();
const std::vector<uint32_t> hostX = buildAxisTerms(true, args.size);
const std::vector<uint32_t> hostZ = buildAxisTerms(false, args.size);
uint32_t* deviceX = nullptr;
uint32_t* deviceZ = nullptr;
ResiduePair* devicePairs = nullptr;
unsigned long long* deviceCount = nullptr;
cudaCheck(cudaMalloc(&deviceX, hostX.size() * sizeof(uint32_t)), "allocate X terms");
cudaCheck(cudaMalloc(&deviceZ, hostZ.size() * sizeof(uint32_t)), "allocate Z terms");
cudaCheck(cudaMalloc(&devicePairs, uint64_t(args.maxResidues) * sizeof(ResiduePair)),
"allocate residue output");
cudaCheck(cudaMalloc(&deviceCount, sizeof(unsigned long long)), "allocate residue count");
cudaCheck(cudaMemcpy(deviceX, hostX.data(), hostX.size() * sizeof(uint32_t),
cudaMemcpyHostToDevice), "copy X terms");
cudaCheck(cudaMemcpy(deviceZ, hostZ.data(), hostZ.size() * sizeof(uint32_t),
cudaMemcpyHostToDevice), "copy Z terms");
cudaCheck(cudaMemset(deviceCount, 0, sizeof(unsigned long long)), "reset residue count");
const auto prepareEnd = std::chrono::steady_clock::now();
cudaEvent_t begin, end;
cudaCheck(cudaEventCreate(&begin), "create begin event");
cudaCheck(cudaEventCreate(&end), "create end event");
cudaCheck(cudaEventRecord(begin), "record begin");
residueKernel<<<65535, args.threads>>>(args.seed, deviceX, deviceZ, args.size,
devicePairs, deviceCount, args.maxResidues);
cudaCheck(cudaGetLastError(), "launch residue kernel");
cudaCheck(cudaEventRecord(end), "record end");
cudaCheck(cudaEventSynchronize(end), "wait for residue kernel");
float kernelMs = 0;
cudaCheck(cudaEventElapsedTime(&kernelMs, begin, end), "measure residue kernel");
unsigned long long residueCount = 0;
cudaCheck(cudaMemcpy(&residueCount, deviceCount, sizeof(residueCount), cudaMemcpyDeviceToHost),
"copy residue count");
if (residueCount > args.maxResidues) {
throw std::runtime_error("residue output truncated; raise --max-residues (needed " +
std::to_string(residueCount) + ")");
}
std::vector<ResiduePair> pairs(residueCount);
if (!pairs.empty()) {
cudaCheck(cudaMemcpy(pairs.data(), devicePairs, pairs.size() * sizeof(ResiduePair),
cudaMemcpyDeviceToHost), "copy residue pairs");
}
cudaEventDestroy(begin);
cudaEventDestroy(end);
cudaFree(deviceCount);
cudaFree(devicePairs);
cudaFree(deviceZ);
cudaFree(deviceX);
const auto exactBegin = std::chrono::steady_clock::now();
uint64_t expanded = 0;
std::vector<Position> hits;
for (const ResiduePair pair : pairs) {
for (int64_t x = firstCongruent(args.x0, pair.x); x <= args.x1; x += kPeriod) {
for (int64_t z = firstCongruent(args.z0, pair.z); z <= args.z1; z += kPeriod) {
++expanded;
if (exactSolid(args.seed, static_cast<int32_t>(x), static_cast<int32_t>(z),
args.size)) {
hits.push_back(Position{static_cast<int32_t>(x), static_cast<int32_t>(z)});
}
}
}
}
const auto exactEnd = std::chrono::steady_clock::now();
const double prepareMs = std::chrono::duration<double, std::milli>(prepareEnd - prepareBegin).count();
const double exactMs = std::chrono::duration<double, std::milli>(exactEnd - exactBegin).count();
constexpr uint64_t residueSpace = uint64_t(kPeriod) * kPeriod;
const uint64_t worldPositions = uint64_t(int64_t(args.x1) - args.x0 + 1) *
uint64_t(int64_t(args.z1) - args.z0 + 1);
std::cout << "residue_space=" << residueSpace
<< " residue_survivors=" << residueCount
<< " survival_rate=" << double(residueCount) / residueSpace << '\n'
<< "prepare_ms=" << prepareMs << " residue_kernel_ms=" << kernelMs << '\n'
<< "world_positions=" << worldPositions
<< " expanded_candidates=" << expanded
<< " exact_ms=" << exactMs << " hits=" << hits.size() << '\n';
std::sort(hits.begin(), hits.end(), [](const Position& a, const Position& b) {
return a.x < b.x || (a.x == b.x && a.z < b.z);
});
for (const Position hit : hits) std::cout << "hit " << hit.x << ' ' << hit.z << '\n';
return 0;
} catch (const std::exception& error) {
std::cerr << "error: " << error.what() << '\n';
return 1;
}
}

239
show/08_seed_cpu.cpp Normal file
View file

@ -0,0 +1,239 @@
// Teaching snapshot extracted from research/slime_pattern_cpp.cpp.
// Shared Java arithmetic/profile solvers live in ../include/slime; see README.md for scope.
#include "slime_solver_core.hpp"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <mutex>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
namespace sr = slime_research;
namespace {
struct CellOffset {
int32_t dx;
int32_t dz;
uint8_t requirement;
};
struct Args {
std::string command;
int32_t x = 0;
int32_t z = 0;
int32_t size = 5;
std::string pattern;
uint64_t seed = 0;
uint64_t lower35 = 0;
int bits = 24;
int threads = std::max(1U, std::thread::hardware_concurrency());
uint64_t maxResults = 1000;
};
[[noreturn]] void usage(int code = 1) {
std::cerr
<< "Usage:\n"
<< " slime_pattern_cpp verify --seed N --x X --z Z [-n N|--pattern ROW/ROW]\n"
<< " slime_pattern_cpp upper --lower35 N --x X --z Z [-n N|--pattern ...]\n"
<< " slime_pattern_cpp roots --x X --z Z [--bits 18..35] [-n N|--pattern ...]\n"
<< " slime_pattern_cpp search --x X --z Z [-p THREADS] [--max-results N] ...\n";
std::exit(code);
}
const char* value(int argc, char** argv, int& i) {
if (++i >= argc) usage();
return argv[i];
}
Args parseArgs(int argc, char** argv) {
if (argc < 2) usage();
Args args;
args.command = argv[1];
for (int i = 2; i < argc; ++i) {
const std::string arg = argv[i];
if (arg == "--x") args.x = std::stoi(value(argc, argv, i));
else if (arg == "--z") args.z = std::stoi(value(argc, argv, i));
else if (arg == "-n" || arg == "--size") args.size = std::stoi(value(argc, argv, i));
else if (arg == "--pattern") args.pattern = value(argc, argv, i);
else if (arg == "--seed") args.seed = std::stoull(value(argc, argv, i));
else if (arg == "--lower35") args.lower35 = std::stoull(value(argc, argv, i));
else if (arg == "--bits") args.bits = std::stoi(value(argc, argv, i));
else if (arg == "-p" || arg == "--threads") args.threads = std::stoi(value(argc, argv, i));
else if (arg == "--max-results") args.maxResults = std::stoull(value(argc, argv, i));
else if (arg == "--help") usage(0);
else usage();
}
if (args.size <= 0 || args.bits < 18 || args.bits > 35 || args.threads <= 0) usage();
return args;
}
std::vector<CellOffset> parsePattern(const Args& args) {
std::vector<CellOffset> offsets;
if (args.pattern.empty()) {
if (int64_t(args.size) * args.size > sr::kMaxPatternCells) {
throw std::runtime_error("pattern exceeds the 64-cell prototype limit");
}
for (int32_t dz = 0; dz < args.size; ++dz) {
for (int32_t dx = 0; dx < args.size; ++dx) {
offsets.push_back({dx, dz, sr::kSlime});
}
}
return offsets;
}
int32_t dx = 0;
int32_t dz = 0;
int32_t width = -1;
for (size_t i = 0; i <= args.pattern.size(); ++i) {
const char c = i == args.pattern.size() ? '/' : args.pattern[i];
if (c == '/') {
if (dx == 0 || (width >= 0 && dx != width)) {
throw std::runtime_error("pattern rows must have equal non-zero width");
}
width = dx;
dx = 0;
++dz;
continue;
}
if (c == '#') offsets.push_back({dx, dz, sr::kSlime});
else if (c == '.') offsets.push_back({dx, dz, sr::kNonSlime});
else if (c != '?') throw std::runtime_error("pattern accepts only #, ., ?, and /");
++dx;
}
if (offsets.empty() || offsets.size() > sr::kMaxPatternCells) {
throw std::runtime_error("pattern must contain 1..64 constrained cells");
}
return offsets;
}
std::vector<sr::CellTerm> makeTerms(const Args& args) {
std::vector<sr::CellTerm> terms;
for (const CellOffset& cell : parsePattern(args)) {
terms.push_back({
sr::coordinateTerm(args.x + cell.dx, args.z + cell.dz),
cell.requirement,
});
}
return terms;
}
std::vector<uint32_t> roots18(const std::vector<sr::CellTerm>& terms) {
std::vector<uint32_t> roots;
for (uint32_t seed = 0; seed < (1U << 18); ++seed) {
if (sr::partialPatternPossible(seed, 18, terms.data(), int(terms.size()))) {
roots.push_back(seed);
}
}
return roots;
}
struct VectorEmitter {
std::vector<uint64_t>& values;
void operator()(uint64_t seed) { values.push_back(seed); }
};
int commandVerify(const Args& args, const std::vector<sr::CellTerm>& terms) {
const bool match = sr::verifyPattern(args.seed & sr::kMask48, terms.data(), int(terms.size()));
std::cout << "seed48=" << (args.seed & sr::kMask48) << ' '
<< (match ? "match" : "no-match") << '\n';
return match ? 0 : 1;
}
int commandUpper(const Args& args, const std::vector<sr::CellTerm>& terms) {
std::vector<uint64_t> results;
VectorEmitter emit{results};
sr::solveUpper13(args.lower35, terms.data(), int(terms.size()), emit);
std::sort(results.begin(), results.end());
std::cout << "lower35=" << (args.lower35 & ((1ULL << 35) - 1ULL))
<< " exact_matches=" << results.size() << '\n';
for (uint64_t seed : results) std::cout << seed << '\n';
return 0;
}
int commandRoots(const Args& args, const std::vector<sr::CellTerm>& terms) {
std::vector<uint32_t> roots = roots18(terms);
std::cout << "bits=18 candidates=" << roots.size() << '\n';
for (int bits = 19; bits <= args.bits; ++bits) {
const uint32_t high = 1U << (bits - 1);
const size_t oldSize = roots.size();
roots.reserve(oldSize * 2);
for (size_t i = 0; i < oldSize; ++i) roots.push_back(roots[i] | high);
roots.erase(std::remove_if(roots.begin(), roots.end(), [&](uint32_t seed) {
return !sr::partialPatternPossible(seed, bits, terms.data(), int(terms.size()));
}), roots.end());
std::cout << "bits=" << bits << " candidates=" << roots.size() << '\n';
}
return 0;
}
int commandSearch(const Args& args, const std::vector<sr::CellTerm>& terms) {
const std::vector<uint32_t> roots = roots18(terms);
constexpr uint64_t extensions = 1ULL << (35 - 18);
const uint64_t workItems = uint64_t(roots.size()) * extensions;
std::atomic<uint64_t> next{0};
std::atomic<uint64_t> totalResults{0};
std::mutex resultMutex;
std::vector<uint64_t> results;
const auto start = std::chrono::steady_clock::now();
auto worker = [&] {
std::vector<uint64_t> local;
local.reserve(64);
VectorEmitter emit{local};
constexpr uint64_t batch = 256;
for (;;) {
const uint64_t begin = next.fetch_add(batch, std::memory_order_relaxed);
if (begin >= workItems) break;
const uint64_t end = std::min(begin + batch, workItems);
for (uint64_t index = begin; index < end; ++index) {
const uint64_t rootIndex = index >> (35 - 18);
const uint64_t extension = index & (extensions - 1);
const uint64_t lower35 = roots[rootIndex] | (extension << 18);
sr::solveUpper13(lower35, terms.data(), int(terms.size()), emit);
}
if (!local.empty()) {
totalResults.fetch_add(local.size(), std::memory_order_relaxed);
std::lock_guard<std::mutex> lock(resultMutex);
const size_t room = results.size() < args.maxResults
? size_t(args.maxResults - results.size()) : 0;
const size_t take = std::min(room, local.size());
results.insert(results.end(), local.begin(), local.begin() + take);
local.clear();
}
}
};
std::vector<std::thread> threads;
for (int i = 0; i < args.threads; ++i) threads.emplace_back(worker);
for (std::thread& thread : threads) thread.join();
std::sort(results.begin(), results.end());
const std::chrono::duration<double> elapsed = std::chrono::steady_clock::now() - start;
std::cout << "lower18_roots=" << roots.size() << " lower35_checked=" << workItems
<< " exact_matches=" << totalResults.load() << " seconds=" << elapsed.count() << '\n';
for (uint64_t seed : results) std::cout << seed << '\n';
return 0;
}
} // namespace
int main(int argc, char** argv) {
try {
const Args args = parseArgs(argc, argv);
const std::vector<sr::CellTerm> terms = makeTerms(args);
if (args.command == "verify") return commandVerify(args, terms);
if (args.command == "upper") return commandUpper(args, terms);
if (args.command == "roots") return commandRoots(args, terms);
if (args.command == "search") return commandSearch(args, terms);
usage();
} catch (const std::exception& error) {
std::cerr << "error: " << error.what() << '\n';
return 2;
}
}

212
show/08_seed_cuda.cu Normal file
View file

@ -0,0 +1,212 @@
// Teaching snapshot extracted from research/slime_pattern_cuda.cu.
// Shared Java arithmetic/profile solvers live in ../include/slime; see README.md for scope.
#include "slime_solver_core.hpp"
#include <cuda_runtime.h>
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
namespace sr = slime_research;
namespace {
#define CUDA_CHECK(call) do { \
const cudaError_t error_ = (call); \
if (error_ != cudaSuccess) { \
std::cerr << #call << ": " << cudaGetErrorString(error_) << '\n'; \
std::exit(1); \
} \
} while (false)
struct Args {
int32_t x = 0;
int32_t z = 0;
int32_t size = 5;
std::string pattern;
int threads = 128;
uint64_t maxResults = 1000;
};
struct CellOffset {
int32_t dx;
int32_t dz;
uint8_t requirement;
};
[[noreturn]] void usage(int code = 1) {
std::cerr << "Usage: slime_pattern_cuda --x X --z Z "
"[-n N|--pattern ROW/ROW] [--threads N] [--max-results N]\n";
std::exit(code);
}
const char* value(int argc, char** argv, int& i) {
if (++i >= argc) usage();
return argv[i];
}
Args parseArgs(int argc, char** argv) {
Args args;
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
if (arg == "--x") args.x = std::stoi(value(argc, argv, i));
else if (arg == "--z") args.z = std::stoi(value(argc, argv, i));
else if (arg == "-n" || arg == "--size") args.size = std::stoi(value(argc, argv, i));
else if (arg == "--pattern") args.pattern = value(argc, argv, i);
else if (arg == "--threads") args.threads = std::stoi(value(argc, argv, i));
else if (arg == "--max-results") args.maxResults = std::stoull(value(argc, argv, i));
else if (arg == "--help") usage(0);
else usage();
}
if (args.size <= 0 || int64_t(args.size) * args.size > sr::kMaxPatternCells ||
args.threads <= 0 || args.threads > 1024) usage();
return args;
}
std::vector<sr::CellTerm> makeTerms(const Args& args) {
std::vector<CellOffset> offsets;
if (args.pattern.empty()) {
for (int32_t dz = 0; dz < args.size; ++dz) {
for (int32_t dx = 0; dx < args.size; ++dx) {
offsets.push_back({dx, dz, sr::kSlime});
}
}
} else {
int32_t dx = 0;
int32_t dz = 0;
int32_t width = -1;
for (size_t i = 0; i <= args.pattern.size(); ++i) {
const char c = i == args.pattern.size() ? '/' : args.pattern[i];
if (c == '/') {
if (dx == 0 || (width >= 0 && dx != width)) {
throw std::runtime_error("pattern rows must have equal non-zero width");
}
width = dx;
dx = 0;
++dz;
continue;
}
if (c == '#') offsets.push_back({dx, dz, sr::kSlime});
else if (c == '.') offsets.push_back({dx, dz, sr::kNonSlime});
else if (c != '?') throw std::runtime_error("pattern accepts only #, ., ?, and /");
++dx;
}
}
if (offsets.empty() || offsets.size() > sr::kMaxPatternCells) {
throw std::runtime_error("pattern must contain 1..64 constrained cells");
}
std::vector<sr::CellTerm> terms;
for (const CellOffset& cell : offsets) {
terms.push_back({
sr::coordinateTerm(args.x + cell.dx, args.z + cell.dz),
cell.requirement,
});
}
return terms;
}
std::vector<uint32_t> roots18(const std::vector<sr::CellTerm>& terms) {
std::vector<uint32_t> roots;
for (uint32_t seed = 0; seed < (1U << 18); ++seed) {
if (sr::partialPatternPossible(seed, 18, terms.data(), int(terms.size()))) {
roots.push_back(seed);
}
}
return roots;
}
struct DeviceEmitter {
unsigned long long* count;
uint64_t* results;
uint64_t capacity;
__device__ void operator()(uint64_t seed) {
const unsigned long long index = atomicAdd(count, 1ULL);
if (index < capacity) results[index] = seed;
}
};
__global__ void solveKernel(
const uint32_t* roots,
uint64_t workItems,
const sr::CellTerm* cells,
int cellCount,
unsigned long long* resultCount,
uint64_t* results,
uint64_t resultCapacity
) {
constexpr uint64_t extensions = 1ULL << (35 - 18);
const uint64_t index = uint64_t(blockIdx.x) * blockDim.x + threadIdx.x;
if (index >= workItems) return;
const uint64_t rootIndex = index >> (35 - 18);
const uint64_t extension = index & (extensions - 1ULL);
const uint64_t lower35 = roots[rootIndex] | (extension << 18);
DeviceEmitter emit{resultCount, results, resultCapacity};
sr::solveUpper13(lower35, cells, cellCount, emit);
}
} // namespace
int main(int argc, char** argv) {
const Args args = parseArgs(argc, argv);
const std::vector<sr::CellTerm> terms = makeTerms(args);
const std::vector<uint32_t> roots = roots18(terms);
const uint64_t workItems = uint64_t(roots.size()) << (35 - 18);
if (workItems == 0) {
std::cout << "lower18_roots=0 lower35_checked=0 exact_matches=0 milliseconds=0\n";
return 0;
}
uint32_t* deviceRoots = nullptr;
sr::CellTerm* deviceTerms = nullptr;
uint64_t* deviceResults = nullptr;
unsigned long long* deviceCount = nullptr;
CUDA_CHECK(cudaMalloc(&deviceRoots, roots.size() * sizeof(uint32_t)));
CUDA_CHECK(cudaMalloc(&deviceTerms, terms.size() * sizeof(sr::CellTerm)));
CUDA_CHECK(cudaMalloc(&deviceResults, args.maxResults * sizeof(uint64_t)));
CUDA_CHECK(cudaMalloc(&deviceCount, sizeof(unsigned long long)));
CUDA_CHECK(cudaMemcpy(deviceRoots, roots.data(), roots.size() * sizeof(uint32_t), cudaMemcpyHostToDevice));
CUDA_CHECK(cudaMemcpy(deviceTerms, terms.data(), terms.size() * sizeof(sr::CellTerm), cudaMemcpyHostToDevice));
CUDA_CHECK(cudaMemset(deviceCount, 0, sizeof(unsigned long long)));
cudaEvent_t start;
cudaEvent_t stop;
CUDA_CHECK(cudaEventCreate(&start));
CUDA_CHECK(cudaEventCreate(&stop));
CUDA_CHECK(cudaEventRecord(start));
const uint64_t blocks64 = (workItems + uint64_t(args.threads) - 1) / uint64_t(args.threads);
if (blocks64 > 0x7FFFFFFFULL) throw std::runtime_error("grid exceeds prototype limit");
solveKernel<<<static_cast<unsigned>(blocks64), args.threads>>>(
deviceRoots, workItems, deviceTerms, int(terms.size()), deviceCount,
deviceResults, args.maxResults
);
CUDA_CHECK(cudaGetLastError());
CUDA_CHECK(cudaEventRecord(stop));
CUDA_CHECK(cudaEventSynchronize(stop));
unsigned long long count = 0;
CUDA_CHECK(cudaMemcpy(&count, deviceCount, sizeof(count), cudaMemcpyDeviceToHost));
const size_t stored = size_t(std::min<unsigned long long>(count, args.maxResults));
std::vector<uint64_t> results(stored);
CUDA_CHECK(cudaMemcpy(results.data(), deviceResults, stored * sizeof(uint64_t), cudaMemcpyDeviceToHost));
std::sort(results.begin(), results.end());
float milliseconds = 0.0F;
CUDA_CHECK(cudaEventElapsedTime(&milliseconds, start, stop));
std::cout << "lower18_roots=" << roots.size() << " lower35_checked=" << workItems
<< " exact_matches=" << count << " milliseconds=" << milliseconds << '\n';
for (uint64_t seed : results) std::cout << seed << '\n';
cudaEventDestroy(start);
cudaEventDestroy(stop);
cudaFree(deviceRoots);
cudaFree(deviceTerms);
cudaFree(deviceResults);
cudaFree(deviceCount);
}

114
show/08_upper_compare.cu Normal file
View file

@ -0,0 +1,114 @@
// Isolate the upper-13 optimization on identical supplied lower35 planes.
// The full seed pipeline lives in 08_seed_cpu.cpp / 08_seed_cuda.cu.
#include "common.hpp"
#include "../research/slime_solver_core.hpp"
#include <cuda_runtime.h>
#include <array>
namespace sr=slime_research;
void check(cudaError_t e) { if(e!=cudaSuccess) throw std::runtime_error(cudaGetErrorString(e)); }
struct Solution {
uint64_t lower35,seed;
bool operator<(const Solution& o) const { return std::tie(lower35,seed)<std::tie(o.lower35,o.seed); }
bool operator==(const Solution& o) const { return lower35==o.lower35 && seed==o.seed; }
};
struct Emitter {
uint64_t lower35,capacity;
Solution* out;
unsigned long long* count;
__device__ void operator()(uint64_t seed) {
const auto slot=atomicAdd(count,1ULL);
if(slot<capacity) out[slot]={lower35,seed};
}
};
template<bool analytic> __global__ void upperKernel(
uint64_t lower35,uint64_t planes,const sr::CellTerm* cells,int n,
Solution* out,unsigned long long* count,uint64_t capacity
) {
const uint64_t stride=uint64_t(gridDim.x)*blockDim.x;
const uint64_t total=analytic?planes:planes*8192;
for(uint64_t i=uint64_t(blockIdx.x)*blockDim.x+threadIdx.x;i<total;i+=stride) {
const uint64_t plane=analytic?i:i/8192;
const uint64_t lower=(lower35+plane)&((1ULL<<35)-1);
Emitter emit{lower,capacity,out,count};
if constexpr(analytic) sr::solveUpper13(lower,cells,n,emit);
else {
const uint64_t seed=lower+((i%8192)<<35);
// Same first-draw candidate model as analytic, then exact verification.
bool candidate=true;
for(int c=0;c<n;++c) {
if((sr::nextStateFromTerm(seed,cells[c].term)>>17)%10!=0) { candidate=false;break; }
}
if(candidate && sr::verifyPattern(seed,cells,n)) emit(seed);
}
}
}
int main(int argc,char** argv) {
try {
uint64_t lower=16420645998ULL,planes=4096;
int repeats=5;
for(int i=1;i<argc;++i) {
const std::string key=argv[i];
if(++i>=argc) throw std::runtime_error("missing value");
if(key=="--lower35") lower=show::positive(argv[i]);
else if(key=="--planes") planes=show::positive(argv[i]);
else if(key=="--repeats") repeats=std::stoi(argv[i]);
else throw std::runtime_error("unknown option");
}
if(!planes || planes>1000000 || repeats<1 || lower>=(1ULL<<35))
throw std::runtime_error("invalid planes/repeats/lower35");
std::array<sr::CellTerm,25> terms;
int cell=0;
for(int dx=0;dx<5;++dx) for(int dz=0;dz<5;++dz)
terms[cell++]={sr::coordinateTerm(51069+dx,419124+dz),sr::kSlime};
sr::CellTerm* deviceTerms=nullptr;
Solution* out=nullptr;
unsigned long long* count=nullptr;
constexpr uint64_t capacity=100000;
check(cudaMalloc(&deviceTerms,sizeof(terms))); check(cudaMalloc(&out,capacity*sizeof(Solution)));
check(cudaMalloc(&count,sizeof(unsigned long long)));
check(cudaMemcpy(deviceTerms,terms.data(),sizeof(terms),cudaMemcpyHostToDevice));
cudaEvent_t begin,end; check(cudaEventCreate(&begin)); check(cudaEventCreate(&end));
std::array<std::vector<double>,2> times;
std::vector<Solution> expected;
bool first=true;
int dev=0; cudaDeviceProp prop{};
check(cudaGetDevice(&dev)); check(cudaGetDeviceProperties(&prop,dev));
for(int r=-1;r<repeats;++r) for(int j=0;j<2;++j) {
const int mode=(j+std::max(0,r))%2;
check(cudaMemset(count,0,sizeof(unsigned long long)));
check(cudaEventRecord(begin));
const uint64_t work=mode?planes:planes*8192;
const int blocks=int(std::min<uint64_t>((work+255)/256,prop.multiProcessorCount*32));
if(mode) upperKernel<true><<<blocks,256>>>(lower,planes,deviceTerms,25,out,count,capacity);
else upperKernel<false><<<blocks,256>>>(lower,planes,deviceTerms,25,out,count,capacity);
check(cudaGetLastError()); check(cudaEventRecord(end)); check(cudaEventSynchronize(end));
float elapsed=0; check(cudaEventElapsedTime(&elapsed,begin,end));
unsigned long long size=0; check(cudaMemcpy(&size,count,sizeof(size),cudaMemcpyDeviceToHost));
if(size>capacity) throw std::runtime_error("upper results truncated");
std::vector<Solution> hits(size);
if(size) check(cudaMemcpy(hits.data(),out,size*sizeof(Solution),cudaMemcpyDeviceToHost));
std::sort(hits.begin(),hits.end());
if(first) { expected=hits;first=false; }
else if(hits!=expected) throw std::runtime_error("enumerated/analytic upper results differ");
if(r>=0) times[mode].push_back(elapsed);
}
std::cout.precision(10);
std::cout << "{\"lower35_start\":" << lower << ",\"planes\":" << planes
<< ",\"candidate_model\":\"first_draw\",\"comparison_equal\":true,\"rows\":[";
for(int m=0;m<2;++m) {
if(m) std::cout << ',';
std::cout << "{\"mode\":\"" << (m?"analytic_upper13":"enumerate_upper13")
<< "\",\"kernel_samples_ms\":"; show::array(times[m]);
std::cout << ",\"kernel_median_ms\":" << show::median(times[m]) << '}';
}
std::cout << "],\"solutions\":[";
for(size_t i=0;i<expected.size();++i) {
if(i) std::cout << ',';
std::cout << '[' << expected[i].lower35 << ',' << expected[i].seed << ']';
}
std::cout << "]}\n";
cudaEventDestroy(begin);cudaEventDestroy(end);cudaFree(count);cudaFree(out);cudaFree(deviceTerms);
} catch(const std::exception& e) { std::cerr << e.what() << '\n';return 1; }
}

129
show/BENCHMARK.md Normal file
View file

@ -0,0 +1,129 @@
# show:逐步实现与本次实测
测量时间:2026-10-08T00:28:41.318982+08:00(Asia/Hong_Kong)。所有表格由 `benchmark.py` 自动生成。
CPU:AMD Ryzen 7 5800H,WSL,16 个逻辑线程。GPU 型号见下面逐项输出;编译器、驱动、源码 SHA256 和全部样本保存在 `results/latest.json`。
## 1. 分段对比:相邻版本在同一负载下测试
主线分成 A、B、C 三组,横线表示更换测试负载。分界两边重复测量衔接版本:A/B 都有 02,B/C 都有 03;只在组内计算速度比。展平实现已移到 experiments,不属于这个阶段。
原来只有 64 个 seed 的极端负载移到 `results/mapping_diagnostics.json`,用于解释并行度,不再作为主线 02→03 的优化倍数。
GPU 各对照版本核对完整 `(seed,x,z)` 命中列表;所有 GPU 命中另由独立 Python Java 算术核验。A/C 与完整 CPU 扫描核对,B 的独立 CPU 验证采用明确列出的窗口。每个版本先跑实际工作量预热,GPU 测量顺序轮换。
CPU 计时包含线程创建、搜索与结果收集;GPU kernel 时间用 CUDA event,scan 时间另含清零、同步与结果回传。两者均不含结果排序,GPU scan 不含一次性的上下文创建及内存分配。
A 的 01→02 用 CPU 搜索时间和 GPU scan 时间比较;B/C 的相邻 CUDA 版本用 kernel 时间比较。B/C 的 CPU 仅用于正确性验证,不列入性能表。
线程映射分段改测时间:2026-10-08T23:15:42.590910+08:00。后面的算法实验沿用上方日期的实测数据,分段实验测量时的源码 SHA256 记录在 metadata.mapping_source_sha256,C 单独复测的源码记录在 metadata.C_source_sha256。
C 组复测时间:2026-10-08T23:58:18.383398+08:00,100 轮轮换测量。
### A:CPU → 按 seed 的 CUDA
`seed=61692151016558` 起共 65,536 个 seed,257×257 个锚点,实心 5×5。候选总数 4,328,587,264。
GPU:NVIDIA GeForce RTX 3060 Laptop GPU。结果完全一致,命中 1 个元组;每版完整预热一次,计时 5 轮。
| 版本 | CPU 搜索/GPU kernel 中位数 | GPU scan 中位数 |
| --- | ---: | ---: |
| 01 CPU | 3550.398 ms | — |
| 02 CUDA/每个 thread 一个 seed | 102.147 ms | 111.110 ms |
| 相邻/分支对照 | 同口径速度比(前项时间/后项时间) |
| --- | ---: |
| 01 → 02(CPU 搜索/GPU scan) | 31.954× |
65536 个 seed 为版本 02 提供足够的独立线程,在 257×257 的完整地图上测量 CPU→CUDA。
---
**测试负载分界:下一组重新测量衔接版本,不跨横线计算加速比。**
### B:大坐标范围的固定坐标策略
`seed=61692151016558` 起共 65,536 个 seed,4097×4097 个锚点,实心 5×5。候选总数 1,100,048,564,224。
GPU:NVIDIA GeForce RTX 3060 Laptop GPU。结果完全一致,命中 1 个元组;每版完整预热一次,计时 5 轮。
| 版本 | CPU 搜索/GPU kernel 中位数 | GPU scan 中位数 |
| --- | ---: | ---: |
| 02 CUDA/每个 thread 一个 seed | 31598.539 ms | 34152.855 ms |
| 03 CUDA/固定坐标,复用多项式后遍历 seed | 17600.387 ms | 19014.153 ms |
| 相邻/分支对照 | 同口径速度比(前项时间/后项时间) |
| --- | ---: |
| 02 → 03(GPU kernel) | 1.795× |
本组使用 65536 个 seed,坐标范围扩大到 4097×4097(16785409 个坐标任务)。02/03 都完整扫描相同的笛卡尔积,GPU 命中列表完全一致。
独立 CPU 核对全部 65536 个 seed 在 `[50941,51197] × [418996,419252]` 的 257×257 窗口;这是窗口验证,不是完整大范围 CPU 重扫。所有 GPU 命中均另作独立精确算术验证。
---
**测试负载分界:下一组重新测量衔接版本,不跨横线计算加速比。**
### C:固定 seed 的坐标扫描与 outer sum
`seed=61692151016558` 起共 1 个 seed,8193×8193 个锚点,实心 5×5。候选总数 67,125,249。
GPU:NVIDIA GeForce RTX 3060 Laptop GPU。结果完全一致,命中 1 个元组;每版完整预热一次,计时 100 轮。
| 版本 | CPU 搜索/GPU kernel 中位数 | GPU scan 中位数 |
| --- | ---: | ---: |
| 03 CUDA/固定坐标,复用多项式后遍历 seed | 2.715 ms | 3.127 ms |
| 04 CUDA/tiled outer sum | 1.598 ms | 1.913 ms |
| 相邻/分支对照 | 同口径速度比(前项时间/后项时间) |
| --- | ---: |
| 03 → 04(GPU kernel) | 1.698× |
本组改为固定 seed,重新测量 03 后再比较 04。03、04 均保持固定坐标后遍历 seed 的策略。
随机数性质的探索独立成章,见 [RANDOM_PROPERTIES.md](RANDOM_PROPERTIES.md)。从状态空间、低位依赖和模运算约束推导搜索方法,不列为局部计时优化。
## 2. 固定 seed:全坐标范围余数过滤
固定 seed `61692151016558`;两轴锚点范围 `[-1875000,1874999]`,总计 14,062,500,000,000 个位置。
完整扫描 2^36 个余数对,保留 180,476 个,展开 36,937,048 个候选,得到 `(51069,419124)`。
| 时间区域 | 中位数 |
| --- | ---: |
| 余数 GPU kernel(实测) | 2.967 s |
| 表准备+kernel+CPU 精查(实测) | 3.622 s |
| 独立进程全流程(实测) | 3.939 s |
| 按 8193×8193 直接扫描吞吐外推全范围 | 568.7 s |
外推直接 kernel 相对余数实测计算流程约 157.0×。分母包含余数方法的准备和 CPU 精查;没有声称实际跑完直接全范围搜索。
07 沿用 research 的首次输出候选模型,尚未覆盖 Java 拒绝尾部例外。锚点范围允许图案向外延伸四格。
## 3. 固定坐标:seed 搜索
锚点 `(51069,419124)`、实心 5×5。CPU/CUDA 解析流程均恢复同一组 24 个已知 seed。
| 版本 | 实测规模 | 搜索/kernel 中位数 | 进程全流程中位数 | 完整 2^48 外推 |
| --- | --- | ---: | ---: | ---: |
| brute_cpu | 2^28 seed | 174.542 ms | 181.317 ms | 183020.0 s |
| brute_cuda | 2^34 seed | 386.768 ms | 777.132 ms | 6336.8 s |
| solver_cpu | 全部 lower35 候选+upper13 解析 | 1013.310 ms | 1022.607 ms | — |
| solver_cuda | 全部 lower35 候选+upper13 解析 | 80.108 ms | 446.468 ms | — |
同 GPU 算法比值:暴力完整空间外推/解析 kernel 约 79,104×。
solver_cpu 的程序内 seconds 计时从通过筛选的低 18 位取值 生成之后开始;进程全流程覆盖全部准备。CUDA 的 milliseconds 为 kernel 时间。
解析流程是首次输出候选模型,不是含全部拒绝尾部分支的严格穷尽。
### 高 13 位优化的受控拆解
相同的 65536 个连续 lower35 平面,从 `16420645998` 开始。枚举与解析都使用首次输出候选模型,并准确验证最终候选。
这里的 lower35 指世界种子的低 35 位。
| 高位策略 | kernel 中位数 |
| --- | ---: |
| enumerate_upper13 | 8.793 ms |
| analytic_upper13 | 0.087 ms |
完整候选结果相同。局部解析加速 101.024×,只适用于这组供给的 lower35 平面。
## 复现与边界
```bash
make -C show -j4
python3 show/test_steps.py --gpu
python3 show/benchmark.py --repeats 5
```
GPU 运行需要宿主 GPU 可用。计时期间 GPU 同时承担桌面工作,时钟、温度和系统负载会影响数字;全部原始样本可检查波动。
01–04 精确覆盖被请求的有限输入范围。07/08 的生成器保留研究原型的拒绝尾部覆盖限制;全 seed × 全世界联合生成仍未实现。
本报告不把“潜在笛卡尔积大小”当作实际访问次数,也不把局部加速比连乘。

35
show/Makefile Normal file
View file

@ -0,0 +1,35 @@
CXX ?= g++
NVCC ?= nvcc
CXXFLAGS ?= -O3 -std=c++17 -march=native -pthread -Wall -Wextra -Wpedantic
NVCCFLAGS ?= -O3 -std=c++17 -arch=sm_86
RESEARCH_HEADERS = ../research/slime_solver_core.hpp ../include/slime/solver_core.hpp
.PHONY: all test test-cpu benchmark clean
all: bin/01_cpu bin/cuda_steps bin/07_residue bin/08_seed_cpu bin/08_seed_cuda bin/08_upper_compare bin/seed_brute_cpu bin/seed_brute_cuda
bin:
mkdir -p bin
bin/01_cpu: 01_cpu.cpp common.hpp | bin
$(CXX) $(CXXFLAGS) $< -o $@
bin/cuda_steps: cuda_steps.cu common.hpp 02_cuda_seed.cuh 03_cuda_position.cuh 04_cuda_outer_sum.cuh | bin
$(NVCC) $(NVCCFLAGS) $< -o $@
bin/08_upper_compare: 08_upper_compare.cu common.hpp $(RESEARCH_HEADERS) | bin
$(NVCC) $(NVCCFLAGS) $< -o $@
bin/07_residue: 07_residue.cu $(RESEARCH_HEADERS) | bin
$(NVCC) $(NVCCFLAGS) -I../research $< -o $@
bin/08_seed_cpu: 08_seed_cpu.cpp $(RESEARCH_HEADERS) | bin
$(CXX) $(CXXFLAGS) -I../research $< -o $@
bin/08_seed_cuda: 08_seed_cuda.cu $(RESEARCH_HEADERS) | bin
$(NVCC) $(NVCCFLAGS) -I../research $< -o $@
bin/seed_brute_cpu: ../research/slime_bruteforce_cpp_bench.cpp $(RESEARCH_HEADERS) | bin
$(CXX) $(CXXFLAGS) -I../research $< -o $@
bin/seed_brute_cuda: ../research/slime_bruteforce_cuda_bench.cu $(RESEARCH_HEADERS) | bin
$(NVCC) $(NVCCFLAGS) -I../research $< -o $@
test-cpu: bin/01_cpu
python3 test_steps.py
test: all
python3 test_steps.py --gpu
benchmark: all
python3 benchmark.py
clean:
rm -rf bin

View file

@ -0,0 +1,371 @@
# 从暴力搜索到位运算:寻找 Minecraft 史莱姆区块的优化之旅
这是一份视频/博客共用的叙事提纲。依据当前源码与研究记录整理,性能数字引用已有实验,没有在整理时重新测量。
后续演示代码已放在当前 `show/` 目录,见 [README.md](README.md);
本次重新测量的速度见 [BENCHMARK.md](BENCHMARK.md)。下文保留构思时的研究数字与补测建议,
制作时以新报告取值。主线线程策略对比只包含按 seed 与固定坐标后遍历 seed;展平实现移到实验目录,不列为本阶段优化。
**主线的测试分段已调整:**A 用 65536 seed × 257×257 对比 CPU 与按 seed 的 CUDA;
B 用 65536 seed × 4097×4097 对比 02、03,扩大每个 seed 的地图;
C 用固定 seed × 8193×8193 对比 03 与后续坐标优化。
A/B、B/C 之间画测试负载分界线,分别重测 02、03,只在每组内部计算速度比。
原来的 64 seed 极端负载用于补充说明并行度,不作为主线“249 倍优化”的依据。
主线:**先让相同的工作执行得更快,再利用问题的结构减少需要执行的工作。**
建议标题:
- 《为了找一片史莱姆区块,我从 CPU 写到了 CUDA,最后开始拆随机数》
- 《从暴力枚举到解析求解:Minecraft 史莱姆区块搜索优化实录》
## 1. 开场:我们到底在找什么?
用一个 5×5 区块网格开场:想找一片全是史莱姆区块的区域。
一个区块是否满足条件,由世界种子和区块坐标共同决定。
把这个判断看成一个确定的函数:
```text
isSlime(seed, chunkX, chunkZ) -> true / false
```
如果粗略把每格看作独立的十分之一概率,一个指定的实心 5×5 图案出现概率约为 `10^-25`。
这只是直觉估算;同一 seed 下各格共享生成机制,后面的算法恰恰要利用这些关系。
引出三个问题,后续每次切换任务时在画面上注明:
| 任务 | 固定什么 | 搜索什么 | 对应实现 |
| --- | --- | --- | --- |
| 原始联合暴力搜索 | seed 区间与坐标范围 | `(seed, x, z)` | 根目录 `slime.cpp`、`slime.cu` |
| 固定 seed 找位置 | 一个世界种子 | `(x, z)` | `slime_position_cuda.cu`、`slime_position_residue_cuda.cu` |
| 固定位置反查 seed | 图案与锚点坐标 | seed | `slime_pattern_cpp.cpp`、`slime_pattern_cuda.cu` |
视频不必一开始列出所有程序。先展示搜索空间像一个三维盒子:seed、X、Z。
后续固定其中一维或两维,就是取这个盒子的一个切片。
贯穿演示的已知样例:世界 seed `61692151016558`,5×5 锚点 `(51069, 419124)`。
固定这个位置,当前求解器找到了 24 个通过精确验证的低 48 位 seed。
## 2. 第一版:CPU 老老实实枚举
源码入口:[`../slime.cpp`](../slime.cpp)。
按“种子 → 坐标 → 图案中的每一格”解释三层循环:
```text
for seed in seeds:
for (x, z) in positions:
if patternMatches(seed, x, z):
record(seed, x, z)
```
CPU 多线程把 seed 区间分给不同工作线程。
判断实心图案时,只要一格失败,就能提前退出;一般不会把 25 格全部算完。
根目录还支持“n×n 中至少 d 格满足”的条件,这里为了讲清楚后续原理,统一使用 `n=5, d=25`。
**画面:**几名工人各自拿一摞 seed,对每个 seed 从头扫描同一张坐标地图。
**转场台词:**“这些候选互不依赖,很适合并行。既然 CPU 能分给十几个线程,能不能分给 GPU 上更多的线程?”
## 3. 第二版:搬到 CUDA
源码入口:[`../slime.cu`](../slime.cu),关注 `checkSeedsKernel` 和 `checkSingleSeed`。
原始 CUDA 实现让一个 GPU thread 负责一个 seed,线程内部继续扫描整个坐标矩形。
这一步改变了执行硬件,但仍然枚举相同类型的候选。
讲解只保留必要概念:thread 是一个工作者,block 是一组工作者,kernel 是交给 GPU 执行的函数。
不必在科普主线中展开 CUDA API。
**画面:**原先几名工人变成很多工人,但每人手上的地图仍然很大。
现有记录中,专用的固定坐标暴力 benchmark,GPU 吞吐约为 16 线程 CPU 的 23.6 倍。
这里必须注明:它来自 `slime_bruteforce_*_bench`,不是根目录两个程序的同任务计时。
根目录 CPU/CUDA 的直接对比留待补测,不能把这个倍数放在根目录版本的柱子上。
**当前演示对照 A:**使用 `show/` 中统一语义后的 01/02,65536 个 seed、257×257 锚点、实心 5×5。
以 CPU 搜索时间与 GPU scan 时间作硬件转换对比,完整命中列表必须一致。新报告已提供这组实测。
---
**测试负载分界:下一章保持相同坐标矩形,把 seed 数增至 65536,重新测量 02,再比较线程策略。跨横线不计算速度比。**
## 4. 第三版:重新分配工作,让 thread 负责坐标
源码入口:[`03_cuda_position.cuh`](03_cuda_position.cuh) 的 `coordinateKernel`。
固定 seed 的研究参考实现另见 [`slime_position_cuda.cu`](../research/slime_position_cuda.cu)。
把坐标矩形编号,每个 thread 固定一个锚点坐标,在内部遍历 seed 并检查完整图案。
固定 seed 时,这个循环只运行一次。
每个 thread 先计算图案需要的 `F(x+dx)` 和 `G(z+dz)`,再将这些多项式结果传给检查函数。
遍历 seed 时只计算两项之和与 Random,不重新计算坐标多项式。这里复用 thread 私有项,下一章才扩展为相邻 thread 共享轴项。
图案大小通过模板展开,私有轴项无需每次动态索引;旧/新 03 的独立样本见
[stage3_polynomial_reuse.json](results/stage3_polynomial_reuse.json),其中同时包含该编译期展开的效果。
当前实现用 grid-stride loop,因此一个 thread 也可能在之后检查其他锚点。
“一个 thread 一个位置”描述的是工作单元,不是永久绑定关系。
```text
之前:thread(seed) -> 顺序扫描所有 (x, z)
之后:thread(x,z) -> 顺序检查所有 seed
```
好处是把长循环拆成更细的任务,使空间维度也能提供足够并行度;固定坐标项可供多个 seed 使用。
当 seed 数很少、位置很多时,这尤其值得尝试。
它是否更快仍受任务规模、调度成本、寄存器使用等因素影响,不能把“坐标线程化必然更快”当成定理。
坐标线程内部仍然有提前退出,warp 分支差异也仍然存在。
如果希望这一章展示“更快多少”,应补一个受控对比:相同 seed 列表、相同坐标矩形、相同图案,一线程一个 seed 与一线程固定一个 `(x,z)`、内部遍历 seed 分别运行。
对多个 seed 的总工作量都应是 `seed_count × width × height`,两边收集相同结果。
目前已有的直接/outer-sum 对比不能证明这个线程粒度转换的独立收益。
**当前演示对照 B:**65536 个 seed、4097×4097 锚点,只运行 02、03。
新版 03 已包含坐标多项式的 thread 内复用与图案大小的模板展开。5 轮 kernel 中位数:02 为 31.599 秒,03 为 17.600 秒,速度比 1.795×;不能再引用仅交换线程策略时的约 1.07×。
03 严格按固定坐标遍历 seed,不引入展平。
只比较同一组输入的 kernel 时间;02/03 的 GPU 完整命中列表必须相同。
独立 CPU 核对同一 seed 列表下的 257×257 窗口,所有 GPU 命中再由独立 Java 算术验证;不把窗口验证写成完整大范围 CPU 重扫。
原先 64 seed 的任务用于解释并行度不足,主线不使用该任务的 249 倍数字。
**画面:**把每个 seed 的大地图拆成小格,工作者各拿一格。
**转场台词:**“现在能同时检查很多位置。但邻近位置算的东西,有不少其实是重复的。”
---
**测试负载分界:接下来保持地图,固定一个 seed;重新测量 03,再与 outer sum 比较。**
## 5. 第四版:outer sum,把二维坐标项拆成两条轴
共享算术:[`slime_solver_core.hpp`](../research/slime_solver_core.hpp) 的 `coordinateXTerm`、`coordinateZTerm`。
史莱姆判断的坐标部分可以写成:
```text
T(x,z) = F(x) + G(z)
```
先展示原式,再按变量分组证明这一点。记 `J32(a)` 为 Java 有符号 int 回绕,则:
```text
F(x) = J32(4987142*x*x) + J32(5947611*x)
G(z) = 4392871*J32(z*z) + J32(389711*z)
```
前两项仅依赖 X,后两项仅依赖 Z,所以拆分是精确恒等式。
取 `X_i=F(x_i)`、`Z_j=G(z_j)`,有 `T_ij=X_i+Z_j`。
矩阵形式是 `T = X·1_n^T + 1_m·Z^T`:把 X 列复制到每列,把 Z 行复制到每行,再逐项相加。
动画先用 `X=(2,5)`、`Z=(10,20,30)` 展示 2×3 数值表,再引出 block 共享。
完整 LaTeX 推导与整数语义见 [`video/OUTER_SUM.md`](video/OUTER_SUM.md)。
这里 F、G 必须保留 Java 原本的 int 溢出、long 转换及符号扩展规则。
按共享核心的定义,轴项相加能用于精确判断;不能把它直接替换为无限精度下的普通二次多项式。
outer sum 是“每个 X 值加上每个 Z 值”,不是 outer product。
用一列 X 数值和一行 Z 数值,动画生成二维表格,便能直观解释。
可分离的是进入随机数生成器之前的坐标项;最终的史莱姆布尔图案没有因此直接变成可分离函数。
CUDA 的 `outerKernel` 用一个 block 覆盖 16×16 个锚点。
5×5 图案需要每条轴上 `16+5-1=20` 个项,放到 shared memory,让同一个 block 复用。
不要说“每个 tile 算 40 次就完成所有判断”:随机数运算和图案验证仍要做,节省的是重复计算的坐标项。
**C 组同任务实测,RTX 3060 Laptop GPU,67,125,249 个锚点,100 轮中位数:**
| 实现 | kernel 时间 | 结果 |
| --- | ---: | --- |
| 03 固定坐标,线程私有轴项 | 2.715 ms | 唯一命中已知位置 |
| 04 tiled outer-sum,共享轴项 | 1.598 ms | 完整命中列表相同 |
加速约 **1.698 倍**。更多数据与命令见 [`BENCHMARK.md`](BENCHMARK.md)。
这个收益适合用来讲提前退出:大多数位置很快就失败,直接版本来就没有计算所有 25 格。
重复计算减少很多,不代表总耗时也按相同比例减少。
outer sum 仍扫描坐标面积,后面的算法才开始减少候选数量。
## 6. 转折:随机数其实是一个确定的位运算过程
源码入口:[`slime_solver_core.hpp`](../research/slime_solver_core.hpp) 的 `nextStateFromTerm`、`exactIsSlimeFromTerm`。
前面的 01–04 是实现层面的优化,仍然检查同一批候选。现在画出章节分界,开始探索 Random 的性质:
状态空间有多大?已知低位能推断什么?坐标与 seed 的约束能否分别组织?哪些未知位可以直接求解?
这部分不用“又快了多少”的表格串联,而是按“发现一个性质 → 举例验证 → 推导新搜索策略”展开。
详细推导见 [RANDOM_PROPERTIES.md](RANDOM_PROPERTIES.md)。XOR 合并只是把公式整理成方便分析的形式,不单列优化版本。
合并史莱姆公式与 Java Random 构造器中的两个 XOR,记:
```text
A = 0x5DEECE66D
B = 11
C = 987234911 xor A
u = ((worldSeed + T(x,z)) xor C) mod 2^48
v = (A*u + B) mod 2^48
q = v >> 17 // next(31) 的首次输出
```
通常 `q % 10 == 0` 就满足这一格;Java `nextInt(10)` 还有拒绝后重抽的分支,后文单独交代。
### 发现一:64 位世界 seed,这个判断只看低 48 位
更准确的说法是“**忽略第 48 位及以上的输入位,也就是 64 位中的高 16 位**”。
不能说“忽略高 48 位”。内部状态只有 48 位,具有相同低 48 位的 world seed 对史莱姆判断等价。
这不意味着它们生成的整个 Minecraft 世界都相同。
**举一反三:**可以按低 48 位给 world seed 分等价类;验证算法时,也可用相差 2^48 的输入检查等价性,而不是把它写成额外的 kernel 加速。
**画面:**64 个位格子,高 16 位淡出,留下 48 位。
### 发现二:要整除 10,至少必须是偶数
首次输出 `q` 的最低位,是内部状态 `v` 的第 17 位,位编号从 0 开始。
乘法、加法、XOR 在模 `2^18` 下只依赖输入的低 18 位。
所以不用知道全部 48 位,就可以判断“首次输出是否为偶数”。
对实心 5×5,要求 25 格的这个必要条件同时成立。
已知位置枚举种子低 18 位的 `2^18=262144` 种取值,只保留 **84 种**。
这里是首次输出模型中的必要条件,不是完整的模 10 判断;Java 拒绝重抽可能产生例外,后面的候选生成器需要明确这一覆盖边界。
**举一反三:**低位可以先决定部分条件,就可以从“逐个完整 seed 判断”改为“先生成符合条件的低位取值,再补上更高位”。
**画面:**48 位色条分出低 18 位;262144 个小点通过 25 格筛子,剩下 84 个。
## 7. 坐标侧分支:先查余数,再展开整个世界范围
为了延续前面的“固定 seed 找坐标”,先用新发现优化坐标扫描。
源码入口:[`slime_position_residue_cuda.cu`](../research/slime_position_residue_cuda.cu)。
上述首次输出奇偶条件,对 X、Z 都具有 `2^18` 的坐标周期。
先检查 `2^18 × 2^18 = 2^36` 个余数对,把通过的类展开成具体坐标,再精确验证。
必须明确:周期属于低位必要条件,完整史莱姆判断不会因此在这些坐标间完全相同。
已知 seed、两轴锚点范围 `[-1875000,1874999]`:
```text
14,062,500,000,000 个锚点
-> 检查 68,719,476,736 个余数对
-> 保留 180,476 个余数对
-> 展开 36,937,048 个具体候选
-> 精确验证,找到已知位置
```
余数 kernel 实测 1.931 秒,精确 CPU 验证约 284 ms,连同轴表准备,全流程约 2.5 秒。
直接扫描全范围约 429 秒是根据较大范围实测吞吐量线性外推;约 170 倍加速的分母是这个外推值。
“候选数量减少约 381000 倍”和“总耗时改善约 170 倍”应分别展示。
默认边界约束的是锚点,5×5 图案还会从锚点向外延伸四格。
**画面:**地图由重复的低位坐标类组成;先筛选类,再只点亮类中需要精查的具体位置。
## 8. 种子侧分支:18 + 17 + 13,解析求解剩余高位
现在明确切换问题:固定 `(51069,419124)`,反过来找世界 seed。
源码入口:`slime_pattern_cpp.cpp`、`slime_pattern_cuda.cu`,共享核心的 `solveUpper13`。
低 18 位筛完后,中间 17 位目前仍然枚举:
```text
84 种通过筛选的低 18 位取值 × 2^17 = 11,010,048 种低 35 位取值
```
不要把“逐位 lifting”讲成每一位都不断淘汰候选。
对首次输出奇偶条件,低 18 位确定后,后续高位不会改变它;这部分仍是扩展枚举。
最后的高 13 位有更强的结构:
```text
seed = lower35 + m * 2^35, 0 <= m < 8192
```
合并后的 XOR 常数小于 `2^35`。固定 lower35,改变 m,所有图案单元的首次输出状态都获得同一个高位位移。
采用 `n = 1645*m mod 8192` 的可逆重编号后,每格的整除条件变成“某个区间里的 n 应满足哪个模 5 余数”。
每格只有一个回绕阈值,把阈值排序,按区间求约束交集,就能获得可行高位,无需逐个试完 8192 个 m。
候选最终仍用准确的 Java `nextInt(10)` 检查。
**主视频画面:**25 条长度为 8192 的候选带,阈值前后颜色变化;叠加后只剩少数共同位置。
1645、4965 等常数的推导放博客附录,视频只讲“共同位移、回绕阈值、集合交集”。
**固定位置结果,RTX 3060 Laptop GPU/16 线程 CPU:**
| 实现 | 时间 | 口径 |
| --- | ---: | --- |
| 旧 CPU 暴力 | 约 33.7 小时 | 根据实测 `2^32` 个 seed 外推 `2^48` |
| 新 CPU 求解 | 约 0.61 秒 | 实测搜索,24 个已知结果全部找回 |
| 旧 CUDA 暴力 | 约 85.7 分钟 | 根据实测 `2^36` 个 seed 外推 `2^48` |
| 新 CUDA 求解 | 59.6 ms | 实测 kernel |
| 新 CUDA 全流程 | 约 0.36 秒 | 包含启动、准备、传输等 |
GPU 算法比较约 **86300 倍**,全流程约 **14300 倍**。
这组数字属于固定坐标反查,不能接在坐标扫描的 1.21 倍、170 倍后相乘。
另有 64 个位置的性能分布,应在博客中引用,避免只展示一个有利样例。
这里的“搜索完成”限于当前首次输出候选模型,不能据此宣称穷尽含拒绝尾部分支的所有 Java seed。
## 9. 科研也包括试错:看起来聪明,不一定更快
主视频选一到两个即可,其他放博客侧栏:
- **FFT:**全匹配写成相关运算很诱人,但直接扫描大量候选会在第一两格失败;FFT 还要付准备、变换与内存成本。已有测试比直接扫描慢约 12.5–25.2 倍。
- **缓存:**1024 种互不相同的低 18 位取值,始终开启缓存零命中,反而更慢;该路线已移除,详见试错报告。
- **在十格筛选之后微调:**约 235 亿个 lower35 状态,只有 133815 个进入这个阶段,优化尾部难以改变总时间。
- **错误的逐位剪枝:**低于 18 位时拿不同输出位的奇偶当真实必要条件,会把已知有效 seed 删掉。更高位拆分也不会自动产生新剪枝。
转场可以说:“不仅要看淘汰了多少,还要看为了做这个筛选,花掉了多少工作。”
来源:[`REJECTED_EXPERIMENTS.md`](../research/REJECTED_EXPERIMENTS.md)、[`FFT_EXPERIMENT.md`](../research/FFT_EXPERIMENT.md)、[X/Z 筛轴试错报告](../research/XZ_FILTER_TRIAL_REPORT.md)。
## 10. 结尾:哪些问题解决了,哪些还没有?
当前最有成果的两条路线,是固定 seed 的坐标搜索,以及固定坐标的 seed 反查。
X/Z 独立筛轴路线因低位生成和过早配对瓶颈停止维护,见试错报告。
全 seed × 全世界目录仍未完成。
Java `nextInt(10)` 在首次输出落到 `2^31` 个可能值中的最后八个值时会拒绝并重抽。
单次概率为 `8/2^31`。现有候选过滤与解析求解主要描述首次抽取,所以可能遗漏依赖后续抽取才成立的解。
精确最终验证能消除误报,不能补回之前被过滤掉的候选。
博客写清楚这个边界;视频可用一句话和字幕交代,避免称为严格穷尽。
结束台词建议:
> 一开始,我们让更多线程帮忙试。后来,我们复用了相同的计算。再后来,我们开始问:哪些候选根本不值得试?最显著的加速,来自对随机数结构的理解。
## 11. 制作安排与需要补齐的实验
### 视频节奏建议
第一集约 15–20 分钟:开场与 CPU(2 分钟)、CUDA 与线程任务拆分(3 分钟)、outer sum(2 分钟)、Random 与低位过滤(3 分钟)、余数坐标搜索(2 分钟)、高位解析求解(3–5 分钟)、结果与边界(1 分钟)。
试错部分以已停止路线的报告为依据。博客可保留全部章节,用公式与源码链接补充推导。
每次优化都采用相同的叙述节奏:**发现重复/浪费 → 改法 → 相同任务验证 → 时间结果 → 剩余瓶颈**。
### 图表口径
使用三组独立图表,图上标出任务、硬件、规模、时间区域和“实测/外推”:
1. 联合暴力枚举:CPU、seed-thread CUDA、细粒度候选 CUDA。需要补测。
2. 固定 seed 找位置:直接扫描、outer-sum、余数过滤。分别说明小矩形实测与全范围外推。
3. 固定位置找 seed:CPU/CUDA 暴力与新求解器,kernel 和全流程分开。
不要把毫秒级扫描、秒级全范围搜索和反查 seed 混在一张“版本越来越快”的图里。
跨度大的 seed 求解时间适合用对数坐标,但柱子上仍标明原始时间。
### 根目录基线的修正与统一
录制性能对照前,根目录实现存在几处需要处理的可比性问题:
- CPU 分线程时使用从 0 开始的区间,未加命令行 startSeed;非零 `-s` 时实际扫描区间不符。
- CUDA kernel 没有 endSeed 边界判断,批次大小非 block 整数倍时会多查 seed。
- CPU 记录所有满足的位置,CUDA 每个 seed 找到第一个位置就返回;计数和工作量语义不同。
- 根目录坐标计算及 Random 运算使用有符号整数溢出写法;受控基线应采用显式无符号回绕与 Java 符号扩展,避免依赖 C++ 未定义行为。
- 测量应统一收集命中元组,避免循环内 printf 和逐 seed 原子计数掩盖搜索本身。
这些是从源码读取出的基线问题,本提纲没有修改实现。
如果想展示旧程序原貌,可保留旧版本作为历史片段;正式性能图使用明确标注的修正基线。
对照先做小范围命中列表一致性验证,再做预热后的多次交替计时。
固定 seed 数、坐标面积、图案、输出方式,使用 CUDA event 测 kernel,另报全流程时间。
线程粒度实验适合覆盖“少 seed/大地图”和“多 seed/小地图”两种任务,而不是预设一种策略总是胜出。
### 素材来源
- 当前保留方案与范围:[`OPTIMIZATION_SUMMARY.md`](../research/OPTIMIZATION_SUMMARY.md)
- 性能、分布与复现命令:[`BENCHMARK.md`](../research/BENCHMARK.md)
- 算术与高位求解:[`slime_solver_core.hpp`](../research/slime_solver_core.hpp)
- 运行入口和已知样例:[`README.md`](../research/README.md)
正文中的数字来自这些已有记录,不应写成在此次提纲整理中重新跑出的结果。

470
show/RANDOM_PROPERTIES.md Normal file
View file

@ -0,0 +1,470 @@
# 随机数性质:怎样从坐标图案反推种子候选
前面的 01–04 在做同一件事:给定一批种子和坐标,把它们检查得更快。这一章换一个角度:如果我们已经知道哪些坐标应该是史莱姆区块,能否利用随机数公式,先排除大量种子,再求出值得检查的候选?
我们以一个固定坐标的实心 5×5 图案为例。目标是寻找一个世界种子,让这 25 个格子同时满足要求。我们不知道每格产生的随机数具体是多少,只知道它们应该通过史莱姆判断。因此,这里的“反推”是根据多个条件缩小候选范围,结果可能有多个种子。
当前方法分成三步:**筛低 18 位的可能取值,补上中间 17 位,再求出高 13 位的共同解。** 最后执行完整的 Java Random 验证。
下面解释筛选和求解的过程,以及每一步的数学依据。首次输出筛选还没有完整覆盖拒绝重抽的例外;这个限制在文末说明。
## 1. 先区分种子、内部状态和随机输出
对坐标 $(x_i,z_i)$,原始史莱姆公式先计算坐标项:
$$
T(x_i,z_i)=F(x_i)+G(z_i).
$$
坐标项包含平方、乘法和加法,必须遵循原始 Java 的整数回绕与符号扩展规则,完整说明见 [OUTER_SUM.md](video/OUTER_SUM.md)。下面用 $d_i=T(x_i,z_i)$ 表示第 $i$ 格的坐标项。
设世界种子的低 48 位为 $w$,合并史莱姆公式与 Java Random 初始化中的两个 XOR,得到:
$$
A=\mathtt{0x5DEECE66D},\qquad
C=A\mathbin{\mathrm{xor}}987234911=\mathtt{0x5E434E432}.
$$
每个格子的首次随机输出按下面三步产生:
$$
u_i=((w+d_i)\mathbin{\mathrm{xor}}C)\bmod2^{48},
$$
$$
v_i=(Au_i+11)\bmod2^{48},
$$
$$
q_i=\left\lfloor v_i/2^{17}\right\rfloor.
$$
这三个数的含义不同:
| 符号 | 含义 |
| --- | --- |
| $w$ | 待求的世界种子低 48 位 |
| $d_i$ | 第 $i$ 格的坐标项,坐标确定后就能计算 |
| $u_i$ | Random 初始化后的 48 位内部状态 |
| $v_i$ | 内部状态推进一次后的结果 |
| $q_i$ | 取出 $v_i$ 的高 31 位得到的首次输出 |
其中,`mod` 表示取余数。例如 $8194\bmod8192=2$。模 $2^k$ 相当于只保留一个数的低 $k$ 位。
对被接受的首次输出,`nextInt(10)` 返回 $q_i\bmod10$,返回 0 就表示史莱姆区块。我们先利用“$q_i$ 能被 10 整除”的条件生成候选,随后完整验证。
## 2. 为什么只考虑世界种子的低 48 位
Random 初始化时只保留低 48 位。因此,在同一个坐标上,如果两个原始世界种子 $W_1,W_2$ 相差整数倍的 $2^{48}$,初始化状态就相同,以后的随机数序列也相同:
$$
W_2=W_1+k2^{48}
\quad\Longrightarrow\quad
\mathrm{slime}(W_2,x,z)=\mathrm{slime}(W_1,x,z).
$$
这条性质覆盖完整的随机数序列,包括拒绝重抽。搜索时可以先求低 48 位,原始 64 位输入的高 16 位对应同一组史莱姆判断结果。
这里讨论的是史莱姆判断的等价关系,不能据此断言整个 Minecraft 世界相同。
## 3. 把待求的 48 位分成三部分
按通常从高位到低位的书写顺序,这三部分是:
```text
高 13 位 h | 中间 17 位 m | 低 18 位 p
```
对应数值关系:
$$
w=p+2^{18}m+2^{35}h.
$$
各部分的取值范围是:
$$
0\le p<262144,\qquad
0\le m<131072,\qquad
0\le h<8192.
$$
补上中间 17 位后,得到低 35 位:
$$
\ell=p+2^{18}m,\qquad w=\ell+2^{35}h.
$$
这三个部分的处理方式不同:低 18 位先筛选,中间 17 位目前主要枚举,高 13 位通过分段同余条件求解。
## 4. 低 18 位为什么能提前筛选
首次输出是:
$$
q_i=v_i\gg17.
$$
右移 17 位,就是去掉内部状态的低 17 位。于是,**$q_i$ 的最低位,对应 $v_i$ 的第 17 位**,位编号从 0 开始。
一个数要能被 10 整除,首先必须是偶数。我们只要知道 $v_i$ 的低 18 位,就能知道它的第 17 位,从而判断首次输出的奇偶。
为什么计算这 18 位时,可以暂时不管世界种子的高位?因为加法、乘法和逐位 XOR 的低 $k$ 位,只依赖输入的低 $k$ 位。更高位不会反过来改变低位。
因此,令 $p=w\bmod2^{18}$、$d_{i,18}=d_i\bmod2^{18}$,只需要计算:
$$
a_i=(p+d_{i,18})\bmod2^{18},
$$
$$
u_{i,18}=a_i\mathbin{\mathrm{xor}}(C\bmod2^{18}),
$$
$$
v_{i,18}=(Au_{i,18}+11)\bmod2^{18}.
$$
最后检查:
$$
v_{i,18}<2^{17}=131072.
$$
小于这个分界值,说明第 17 位为 0,首次输出是偶数;否则就是奇数。
**坐标项也参与计算。** 这条性质说的是“种子的低 18 位,加上该格坐标项的低 18 位,足以决定首次输出奇偶”。换坐标后,结果可能改变。
## 5. 怎样从一个图案生成低 18 位候选表
只有一个“5×5”的形状还不够,需要知道锚点在哪。已有样例的锚点是:
$$
(x_0,z_0)=(51069,419124).
$$
图案包含的 25 个坐标为:
$$
(x_i,z_i)=(x_0+\Delta x,z_0+\Delta z),
\qquad\Delta x,\Delta z\in\{0,1,2,3,4\}.
$$
生成表的过程可以按下面的顺序理解:
1. 计算 25 个坐标项,分别保留低 18 位。
2. 尝试一种种子的低 18 位取值 $p$。
3. 用这个 $p$ 检查 25 格的首次输出奇偶。
4. 有一格为奇数,就淘汰这个 $p$;全部为偶数,就保存。
5. 对 $p=0,1,\ldots,262143$ 重复这个过程。
这相当于为每格求出一个允许集合,再对 25 个集合求交。
看两个实际取值,就能理解“保留”和“淘汰”是什么意思:
| 种子的低 18 位 $p$ | 检查的格子 | $v_{i,18}$ | 结果 |
| --- | --- | ---: | --- |
| 0 | $(51069,419124)$ | 53282 | 偶数条件通过 |
| 0 | $(51069,419125)$ | 178468 | 失败,整个 $p=0$ 淘汰 |
| 3948 | $(51069,419124)$ | 1118 | 偶数条件通过 |
| 3948 | $(51069,419125)$ | 73664 | 偶数条件通过 |
$p=3948$ 在其余格子上也通过,所以进入最后的候选表。
固定锚点 $(51069,419124)$,把要求通过的图案从 1 格逐步扩大到 5×5。每次保留已有格子,再加入扩大后新增的一圈格子,剩余取值数量如下。表中的 0 表示尚未加入任何格子条件,1 表示只检查锚点这一格:
| 要求通过的图案 | 剩余低 18 位取值数 |
| ---: | ---: |
| 0 | 262144 |
| 1 | 131072 |
| 2×2 | 20383 |
| 3×3 | 3385 |
| 4×4 | 427 |
| 5×5 | 84 |
单格恰好保留一半取值,是因为加坐标项、XOR 和奇数乘法在模 $2^{18}$ 下都是一一对应的变换。继续加入格子,只会增加条件,所以候选数量只能保持不变或减少。
这 84 种取值就是“该坐标图案允许的世界种子低 18 位候选表”。
对固定图案,这一步可以作为预处理,只做一次。后续直接遍历这 84 种取值,给它们补上更高位,不必让所有完整种子先查一次表。
这里的 84 只适用于样例中的具体坐标。移动锚点或改变形状后,需要重新筛选;如果两个图案对应格子的坐标项低 18 位全部相同,就可以复用这张低位候选表,但完整随机输出仍可能不同。
## 6. 中间 17 位目前怎样处理
取出一个通过筛选的 $p$,再枚举:
$$
m=0,1,\ldots,131071.
$$
每次拼出一种低 35 位取值:
$$
\ell=p+2^{18}m.
$$
然后把这个 $\ell$ 交给高 13 位求解器:是否存在某个 $h$,能让所有格子满足首次输出的模 10 条件?如果没有,就淘汰这个 $\ell$。
因此,当前没有另一张能直接消除中间 17 位枚举的通用表。样例需要处理:
$$
84\times2^{17}=11010048
$$
种低 35 位取值。低 18 位筛选避免了大量无效工作,但中间位的枚举成本仍然存在。
## 7. 为什么固定低 35 位后,高位可以单独求解
现在固定一种 $\ell$,世界种子写成:
$$
w=\ell+2^{35}h.
$$
选择 35 的原因是 XOR 常量 $C$ 恰好需要 35 位:
$$
2^{34}\le C<2^{35}.
$$
它只会翻转低 35 位,不会碰到未知的高 13 位。
这里还要保留坐标加法的进位。对第 $i$ 格,把 $h=0$ 时的输入拆成:
$$
(\ell+d_i)\bmod2^{48}=L_i+2^{35}H_i.
$$
$H_i$ 包含坐标加法产生的进位,也包含负坐标项在模 $2^{48}$ 下的表示。加入 $h$ 后,低位 $L_i$ 不变,高位成为 $(H_i+h)\bmod8192$。
XOR 只改动 $L_i$,因此初始化状态满足:
$$
u_i(h)=(u_i(0)+2^{35}h)\bmod2^{48}.
$$
再推进一次状态:
$$
v_i(h)=(v_i(0)+Ah\,2^{35})\bmod2^{48}.
$$
最后取首次输出,去掉状态低 17 位。新增量本来就是 $2^{35}$ 的整数倍,不会改变被去掉的低位余数,所以:
$$
q_i(h)=(q_i(0)+Ah\,2^{18})\bmod2^{31}.
$$
这里的 $18$ 来自 $35-17$。
这是利用不同位域分离得到的平移关系。一般的 XOR 并不具有这种加法性质。若切在 34 位,$C$ 仍会翻转未知部分的一位,上面的直接平移就不成立;切在 36 位等更高位置也可以分离,但剩下可解析的高位就更少了。
## 8. $r_i,b_i$ 是怎样实际算出来的
它们来自一个确定的计算结果。选定 $\ell$ 和第 $i$ 格的坐标,先暂时令 $h=0$,按照原始公式计算:
$$
u_i(0)=((\ell+d_i)\mathbin{\mathrm{xor}}C)\bmod2^{48},
$$
$$
v_i(0)=(Au_i(0)+11)\bmod2^{48},\qquad
q_i(0)=\left\lfloor v_i(0)/2^{17}\right\rfloor.
$$
再把 $q_i(0)$ 除以 $262144=2^{18}$:
$$
r_i=q_i(0)\bmod262144,\qquad
b_i=\left\lfloor q_i(0)/262144\right\rfloor.
$$
$r_i$ 是余数,也就是输出的低 18 位;$b_i$ 是商,也就是输出的高 13 位。例如,如果某格算出 $q_i(0)=524290$:
$$
524290=2+262144\times2,
$$
那么 $r_i=2,b_i=2$。这只是拆分数字的例子,不是前面样例某格的实测输出。
改变 $h$ 后,该格的首次输出就是:
$$
\boxed{q_i(h)=r_i+2^{18}\bigl((b_i+Ah)\bmod8192\bigr).}
$$
**$b_i$ 是随机输出的一部分,$h$ 是待求种子的一部分,不能把两者当成同一个数。** $r_i,b_i$ 在当前 $\ell$ 和坐标下固定,不同格子通常具有不同的 $r_i,b_i$,但所有格子共用同一个未知 $h$。
## 9. 为什么先求 $t$,再换回 $h$
为了让规则更容易处理,令:
$$
t=Ah\bmod8192.
$$
用 $e_i$ 表示换元后该格输出的高 13 位:
$$
e_i=(b_i+t)\bmod8192,\qquad
q_i=r_i+2^{18}e_i.
$$
$A$ 是奇数,和 $8192=2^{13}$ 互质,所以这个换元只是在重新排列 8192 个编号,不会重复或遗漏。
具体来说:
$$
A\bmod8192=1645,\qquad
1645\times4965=997\times8192+1.
$$
因此,求到 $t$ 后,可以直接换回:
$$
\boxed{h=4965t\bmod8192.}
$$
这里用到了“奇数乘数在模 $2^k$ 下有逆元”的性质。如果完整状态 $v$ 已知,同样可以用模逆恢复上一状态:
$$
u=A^{-1}(v-11)\bmod2^{48}.
$$
但首次输出已经丢掉状态低 17 位,而史莱姆观察只提供一个布尔结果。因此,当前搜索仍需组合多个格子的条件来求候选。
## 10. 每格到底对 $t$ 提出什么要求
我们要让首次输出能被 10 整除。偶数条件已经由低 18 位筛选保证,现在还需要能被 5 整除。
因为:
$$
2^{18}\equiv4\equiv-1\pmod5,
$$
所以:
$$
q_i=r_i+2^{18}e_i\equiv r_i-e_i\pmod5.
$$
于是,每格提出的要求是:**$e_i$ 除以 5 的余数,必须与 $r_i$ 相同。**
完整写法为:
$$
q_i\equiv0\pmod{10}
\iff r_i\equiv0\pmod2\ \text{且}\ e_i\equiv r_i\pmod5.
$$
如果高位求解器单独接收一个没有预先筛过的低 35 位取值,发现某格 $r_i$ 为奇数,就会立即返回无候选。
## 11. 回绕点是什么,为什么它会改变规则
考虑:
$$
e_i=(b_i+t)\bmod8192.
$$
可以把它想成一个从 0 数到 8191 的计数器。到 8192 时,就回到 0。
例如 $b_i=8190$:
| $t$ | $b_i+t$ | 取模后的 $e_i$ |
| ---: | ---: | ---: |
| 0 | 8190 | 8190 |
| 1 | 8191 | 8191 |
| 2 | 8192 | 0 |
| 3 | 8193 | 1 |
| 4 | 8194 | 2 |
$t=2$ 就是回绕点。一般来说,回绕点为:
$$
\tau_i=8192-b_i.
$$
点之前,$e_i=b_i+t$;从这个点开始,$e_i=b_i+t-8192$。由于 $0\le b_i,t<8192$,相加最多回绕一次。
把上一节的要求代入,就得到:
$$
\begin{cases}
t\equiv r_i-b_i\pmod5,&0\le t<\tau_i,\\
t\equiv r_i-b_i+2\pmod5,&\tau_i\le t<8192.
\end{cases}
$$
第二种要求多了 2,是因为 $8192\bmod5=2$。若 $b_i=0$,回绕点在 8192,超出 $t$ 的有效整数范围,所以这个格子在整个范围里都不回绕。
## 12. 用两个格子的数值例子,把求解串起来
下面的 $r_i,b_i$ 是用于讲解的示意数值,不是 5×5 样例的实测参数。
第一格取 $r_1=2,b_1=8190$。它要求 $e_1\bmod5=2$,回绕点在 $t=2$:
- $t<2$ 时,要求 $t\bmod5=2$。
- $t\ge2$ 时,要求 $t\bmod5=4$。
第二格取 $r_2=0,b_2=1$。它要求 $e_2\bmod5=0$,回绕点在 $t=8191$:
- $t<8191$ 时,要求 $t\bmod5=4$。
- $t\ge8191$ 时,要求 $t\bmod5=1$。
把两格条件合并:
| $t$ 的区间 | 第一格要求的余数 | 第二格要求的余数 | 共同解 |
| --- | ---: | ---: | --- |
| $[0,2)$ | 2 | 4 | 无 |
| $[2,8191)$ | 4 | 4 | 区间中除以 5 余 4 的数 |
| $[8191,8192)$ | 4 | 1 | 无 |
因此,允许的 $t$ 是:
$$
t=4,9,14,\ldots,8189.
$$
取 $t=4$,换回种子的高位:
$$
h=4965\times4\bmod8192=3476.
$$
检查两个格子的首次输出:
$$
e_1=(8190+4)\bmod8192=2,\qquad
q_1=2+2^{18}\times2=524290,
$$
$$
e_2=(1+4)\bmod8192=5,\qquad
q_2=2^{18}\times5=1310720.
$$
两个输出都能被 10 整除,所以 $h=3476$ 是这两格共同允许的高位取值。真实图案对 25 格做同样的求交,最后拼成完整种子,再验证。
## 13. 最后说明首次输出模型的边界
上面的状态与输出变换恒等式是精确的。覆盖限制来自用“首次输出能被 10 整除”生成候选。
Java `nextInt(10)` 遇到:
$$
q\ge2147483640=2^{31}-8
$$
时,会拒绝这个输出,继续推进内部状态,再取一个输出。例如 $2147483640$ 虽然能被 10 整除,也不会直接被接受。
因此会有两种情况:
- 首次输出符合我们的候选条件,但被拒绝,重抽后失败。最终精确验证会剔除这种假阳性。
- 首次输出不符合候选条件,例如是奇数,但处在拒绝尾部,重抽后成功。它可能在低位筛选阶段就被淘汰,最终验证无法补回这种漏解。
所以,当前流程准确验证所有输出的候选,但不能宣称已经找齐完整 Java Random 语义下的所有解。
低 18 位的 84 种取值、正文中的筛选数量和两个格子的示意例子,都可以按照上面的计算步骤独立核对。

145
show/README.md Normal file
View file

@ -0,0 +1,145 @@
# 史莱姆区块搜索:逐步演示
这里保存视频/博客草稿、按步骤编号的代码,以及本次实测结果。
演示代码在本目录,研究实现位于 `research/`。草稿见
[POPULAR_SCIENCE_OUTLINE.md](POPULAR_SCIENCE_OUTLINE.md)。
先读 [BENCHMARK.md](BENCHMARK.md) 看实测结果;命令、原始 stdout、全部计时样本、
编译器与源码 SHA256 保存在 [results/latest.json](results/latest.json)。
Manim 视频工程位于 [video/](video/README.md),使用 uv 管理依赖;包含完整动画、
两集独立场景,以及渲染时生成的字幕、口播稿和章节时间轴。
## 位数与术语
“低 18 位取值”指种子除以 `2^18` 的余数;`lower35` 指低 35 位,`upper13` 指剩余的高 13 位。补上中间 17 位,就是保留已有的低 18 位,再枚举紧挨着它们的更高 17 位。
“低位筛选结果缓存”按低 18 位取值复用筛选结果。世界种子与锚点有效种子分别指原始输入和吸收锚点坐标项后的输入,不能混用。
## 代码顺序
| 步骤 | 文件 | 这一版改变什么 |
| --- | --- | --- |
| 01 | `01_cpu.cpp` | CPU 按 seed 分任务,内部顺序扫描位置 |
| 02 | `02_cuda_seed.cuh` | 相同循环搬到 CUDA,一 thread 处理一个 seed |
| 03 | `03_cuda_position.cuh` 的 `coordinateKernel` | 一 thread 固定坐标,先算图案的 X/Z 项,再遍历 seed 复用 |
| 04 | `04_cuda_outer_sum.cuh` | 16×16 tile,共享扩展后的 X/Z 轴项 |
| 性质探索 | [RANDOM_PROPERTIES.md](RANDOM_PROPERTIES.md) | 从状态空间、低位依赖与模运算结构推导搜索方法 |
| 07 | `07_residue.cu` | 固定 seed,完整低位坐标余数过滤后展开 |
| 08 | `08_upper_compare.cu` | 相同 lower35 平面,高 13 位枚举/解析受控比较 |
| 08 完整流程 | `08_seed_cpu.cpp`、`08_seed_cuda.cu` | 固定坐标,筛低 18 位取值 → 补上中间 17 位 → 求高 13 位 |
`cuda_steps.cu` 统一负责 02–04 的启动、预热、计时和结果一致性检查。
核心算术在 `common.hpp`,必须保留 Java int 回绕和符号扩展;这里使用显式无符号运算,
避免直接照抄根目录 C++ 中的有符号溢出。
03、04 都保持固定坐标后遍历 seed 的策略。
03 在每个坐标的 seed 循环外预计算 n 个 X 项和 n 个 Z 项,传给 `solidAxes`。
03 的坐标项为 thread 私有;04 进一步让相邻 thread 共享 tile 的轴项。
图案大小 1–8 在 host 端选择模板,让私有数组索引和检查循环可在编译时展开。
固定坐标旧版/新版的单独对照见 [results/stage3_polynomial_reuse.json](results/stage3_polynomial_reuse.json):
65536 seed × 1025×1025,5 轮 kernel 中位数 1537.888 → 915.371 ms,速度比 1.680×。
这个对照包含多项式复用及编译期图案大小展开,两个版本先后运行;主线 02/03 则轮换顺序测量。
01–04 都记录每一个命中的 `(world_seed,x,z)`,没有“一 seed 找到第一个位置就返回”的差异。
性能主线到 04 结束。随后读 [RANDOM_PROPERTIES.md](RANDOM_PROPERTIES.md),
探索随机数的状态空间、低位依赖和模运算结构,再引出 07–08 的候选生成与解析求解。
这部分用推导和例子说明性质,不把 XOR 的等价改写列作计时优化。
07、08 完整流程是从对应 `research/` 实现提取的教学快照,文件头注明来源,
继续共享 `../research/slime_solver_core.hpp`。
08 的单独高位比较是新写的受控实验;其他成熟算法没有重新复制一套数学核心。
因此 `show/` 在本仓库内构建,不能只拷贝这个目录后省略 `research` 头文件。
## 编译和测试
```bash
make -C show -j4
python3 show/test_steps.py --gpu
python3 show/benchmark.py --repeats 5
```
默认 CUDA 编译目标是本机 RTX 3060 Laptop 的 `sm_86`,需要 CUDA 12 或兼容工具链。
其他 GPU 可覆盖,例如 `make -C show NVCCFLAGS='-O3 -std=c++17 -arch=sm_89'`。
无 GPU 时可运行 `make -C show test-cpu`;GPU 测试需要宿主 GPU 可访问。
测试用独立 Python oracle 对比完整列表,包括非零起始 seed、跨低 48 位边界、
负坐标、int32 边界、部分 tile、已知命中、8 种首次输出拒绝尾部和输出溢出。
另外核对 upper13 的直接枚举、完整 CPU/CUDA seed 流程,以及余数坐标展开验证。
本次验证日志保存在 [results/tests.txt](results/tests.txt)。
## 单独演示前四步
主线在测试负载变化处画分界线,衔接版本在两边分别重测。A/B 使用 5 轮计时,C 使用 100 轮轮换计时,每版另做一次完整预热。
```bash
python3 show/benchmark.py --only-mapping --repeats 5
```
完整 CPU 对照和大地图 CUDA 扫描耗时较长,脚本为每项允许最多 10 分钟。
各组配置:
| 组 | 用途 | 相同工作量下比较的版本 | 负载 |
| --- | --- | --- | --- |
| A | CPU 搬到 CUDA | 01 → 02 | 65536 seed × 257×257 锚点 |
| B | 改变并行策略 | 02 → 03 | 65536 seed × 4097×4097 锚点 |
| C | 坐标扫描与 outer sum | 03 → 04 | 1 seed × 8193×8193 锚点 |
**A/B 分界处重测 02,B/C 分界处重测 03,只在组内计算速度比。**
A、B 保持 65536 个 seed,以已知命中点为中心扩大地图:A 为 257×257,B 为 4097×4097。
A 完整重扫 CPU,与 CUDA 的相同输入比较;B 改变线程策略,两版完整扫描相同候选集合。
B 的 GPU 两版本完整核对命中列表,独立 CPU 则核对同一 seed 列表下的 257×257 窗口;所有 GPU 命中另用 Python 精确验证。
原来的极端负载保存在 [results/mapping_diagnostics.json](results/mapping_diagnostics.json),可用于讲解并行度。
```bash
# A:CPU 与按 seed 的 CUDA,两边相同输入
show/bin/01_cpu --seed 61692151016558 --seeds 65536 \
--x0 50941 --z0 418996 --width 257 --height 257 -n 5 --repeats 5
show/bin/cuda_steps --seed 61692151016558 --seeds 65536 \
--x0 50941 --z0 418996 --width 257 --height 257 -n 5 --modes 2 --repeats 5
# ── 分界:换负载,重新测 02,再与 03 比 ──
# B:保持 seed 数量,把地图扩大
show/bin/cuda_steps --seed 61692151016558 --seeds 65536 \
--x0 49021 --z0 417076 --width 4097 --height 4097 \
-n 5 --modes 2,3 --repeats 5
# ── 分界:扩大地图、改为固定 seed,重新测 03 ──
# C:固定坐标与 outer sum
show/bin/cuda_steps --seed 61692151016558 --seeds 1 \
--x0 46973 --z0 415028 --width 8193 --height 8193 \
-n 5 --modes 3,4 --repeats 100
```
输出是 JSON,包含每轮耗时、中位数和完整排序后的命中元组。
`--capacity` 是可存的命中数,超过容量会失败,不能用截断的列表假装验证通过。
所有 CUDA 版本均有范围判断。高 16 位输入不同会保留原始 seed 标签,判断结果等价;
输出标签归一化后才能进行这种等价比较。
CPU 计时包含创建线程,GPU kernel 不含上下文创建,因此报告另给 GPU scan 计时,
完整 seed 流程还给独立进程全流程计时。
## 后面的步骤
```bash
show/bin/07_residue --seed 61692151016558 -n 5
show/bin/08_upper_compare --lower35 16420645998 --planes 65536 --repeats 5
show/bin/08_seed_cpu search --x 51069 --z 419124 -n 5 -p 16 --max-results 1000
show/bin/08_seed_cuda --x 51069 --z 419124 -n 5 --threads 256 --max-results 1000
```
`benchmark.py` 把三类任务分开记录:联合暴力候选的映射、固定 seed 的坐标搜索、
固定坐标的 seed 搜索。X/Z 独立筛轴试验已撤下,见 [试错报告](../research/XZ_FILTER_TRIAL_REPORT.md)。
每阶段完成后保存 JSON,整套完成后生成报告。命令默认超时 60 秒,A/B/C 分段测速允许最多 10 分钟。
默认整套会重跑 GPU 和 CPU 数据,不需要依赖研究记录中的旧数字。
只修改前四步的测试参数时,可以运行 `python3 show/benchmark.py --only-mapping --repeats 5`;
它先检查源代码哈希,再重测 A/B/C,保留后面的已有算法实验,在报告中分别注明测量日期。
## 视频里必须交代的边界
- 固定坐标线程与按 seed 线程是转置关系,哪种快取决于任务规模与循环中的算术复用。展平实现移至 `experiments/`,不属于这一阶段。
- 低 48 位不是新加入的剪枝:JavaRandom 从第一版起就只有 48 位状态。高 16 位等价性是一条性质,不是额外实测加速比。
- 07–08 的全局生成器仍用首次输出候选模型,可能漏掉依赖拒绝后重抽的解;最终准确验证不能补回漏掉的候选。
- 潜在笛卡尔积规模不是实际访问次数。收益不能跨任务相乘,全 seed × 全世界目录仍未解决。
- FFT 等失败方案的代码和数据仍在 `research` 归档,见草稿中的链接;本目录实测保留方案。
可直接用于叙事的结果应从本次报告取值,草稿里原有的研究数字作为历史记录保留。
独立筛轴的教学实现、测速结果和视频章节已移除,历史结论保存在 [X/Z 筛轴试错报告](../research/XZ_FILTER_TRIAL_REPORT.md)。

324
show/benchmark.py Normal file
View file

@ -0,0 +1,324 @@
#!/usr/bin/env python3
"""Run the complete teaching sequence; preserve commands, raw output and samples."""
import argparse
from datetime import datetime
import hashlib
import json
from pathlib import Path
import platform
import re
import statistics
import subprocess
import time
from zoneinfo import ZoneInfo
from test_steps import slime
ROOT = Path(__file__).resolve().parent
BIN = ROOT/'bin'
def capture(command, timeout=60):
started = time.perf_counter()
p = subprocess.run(list(map(str, command)), capture_output=True, text=True,
check=True, timeout=timeout)
return {'command': list(map(str, command)), 'process_ms': (time.perf_counter()-started)*1000,
'stdout': p.stdout, 'stderr': p.stderr}
def native(name, args, timeout=60):
return capture([BIN/name, *args], timeout=timeout)
def fields(text):
return {key: float(value) if '.' in value or 'e' in value.lower() else int(value)
for key, value in re.findall(r'([a-zA-Z0-9_]+)=([0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?)', text)}
def line_fields(text, prefix):
line = next(line for line in text.splitlines() if line.startswith(prefix))
return fields(line)
def seed_results(text):
return sorted(int(line) for line in text.splitlines() if line.isdigit())
def summarize(samples):
return {'samples_ms': samples, 'median_ms': statistics.median(samples),
'min_ms': min(samples), 'max_ms': max(samples)}
def args_for(case, repeats):
return [str(v) for pair in [('--seed', case['seed']), ('--seeds', case['seeds']),
('--x0', case['x0']), ('--z0', case['z0']),
('--width', case['width']), ('--height', case['height']),
('-n', case.get('n', 5)), ('--repeats', repeats)] for v in pair]
def mapping(repeats):
cases = [
dict(name='A:CPU → 按 seed 的 CUDA', seed=61692151016558, seeds=65536,
x0=50941, z0=418996, width=257, height=257, modes='2',
cpu_in_table=True, comparisons=[('01', '02')]),
dict(name='B:大坐标范围的固定坐标策略', seed=61692151016558, seeds=65536,
x0=49021, z0=417076, width=4097, height=4097, modes='2,3',
cpu_in_table=False, cpu_subset=True, comparisons=[('02', '03')]),
dict(name='C:固定 seed 的坐标扫描与 outer sum', seed=61692151016558, seeds=1,
x0=46973, z0=415028, width=8193, height=8193, modes='3,4',
cpu_in_table=False, repeats=100, comparisons=[('03', '04')]),
]
data = []
for case in cases:
print(f"测量线程映射:{case['name']},{case['seeds']} seed × {case['width']}×{case['height']}", flush=True)
args = args_for(case, case.get('repeats', repeats))
# B's full rectangle is expensive on CPU; disclose its independent validation window.
cpu_repeats = repeats if case['cpu_in_table'] else 1
cpu_case = dict(case)
if case.get('cpu_subset'):
cpu_case.update(x0=50941, z0=418996, width=257, height=257)
cpu_raw = native('01_cpu', [*args_for(cpu_case, cpu_repeats), '--cpu-threads', '16'], timeout=600)
print('CPU 对照完成;开始 CUDA 预热与计时', flush=True)
gpu_raw = native('cuda_steps', [*args, '--modes', case['modes']], timeout=600)
cpu, gpu = json.loads(cpu_raw['stdout']), json.loads(gpu_raw['stdout'])
checked_hits = [hit for hit in gpu['hits']
if cpu_case['x0'] <= hit[1] < cpu_case['x0']+cpu_case['width']
and cpu_case['z0'] <= hit[2] < cpu_case['z0']+cpu_case['height']]
if cpu['hits'] != checked_hits:
raise RuntimeError('CPU/CUDA full tuple mismatch')
for seed, x, z in gpu['hits']:
if not all(slime(seed,x+dx,z+dz) for dx in range(case.get('n',5))
for dz in range(case.get('n',5))):
raise RuntimeError('independent Java oracle rejected GPU hit')
print('完成该组完整命中对照', flush=True)
data.append(dict(case=case, cpu=cpu, gpu=gpu, raw=[cpu_raw, gpu_raw], equality=True,
verification=dict(cpu_scope='subset' if case.get('cpu_subset') else 'full',
cpu_case=cpu_case, all_gpu_hits_exact=True)))
return data
def residue(repeats):
print('测量完整坐标余数空间(固定 seed)', flush=True)
# The implementation scans all 2^36 residues every invocation.
args = ['--seed', '61692151016558', '-n', '5']
warmup = native('07_residue', args)
runs = [native('07_residue', args) for _ in range(repeats)]
hits = lambda raw: sorted(line for line in raw['stdout'].splitlines() if line.startswith('hit '))
expected = ['hit 51069 419124']
if any(hits(raw) != expected for raw in [warmup, *runs]):
raise RuntimeError('residue known-sample results mismatch')
counts = [fields(raw['stdout']) for raw in runs]
for k in ('residue_survivors', 'expanded_candidates', 'world_positions'):
if len({row[k] for row in counts}) != 1:
raise RuntimeError('residue repeats have different '+k)
return dict(raw=runs, warmup=warmup, counts=counts[0],
kernel=summarize([r['residue_kernel_ms'] for r in counts]),
compute=summarize([r['prepare_ms']+r['residue_kernel_ms']+r['exact_ms'] for r in counts]),
process=summarize([r['process_ms'] for r in runs]))
def seeds(repeats):
print('测量固定坐标 seed 反查:暴力吞吐与完整求解', flush=True)
fixed = ['--x', '51069', '--z', '419124', '-n', '5']
commands = {
'brute_cpu': ('seed_brute_cpu', [*fixed, '--count', str(1 << 28), '-p', '16']),
'brute_cuda': ('seed_brute_cuda', [*fixed, '--count', str(1 << 34)]),
'solver_cpu': ('08_seed_cpu', ['search', *fixed, '-p', '16', '--max-results', '1000']),
'solver_cuda': ('08_seed_cuda', [*fixed, '--threads', '256', '--max-results', '1000']),
}
collected = {name: [] for name in commands}
# Each process runs its own kernel/algorithm; extra unrecorded passes warm machine clocks.
for name, (binary, args) in commands.items():
native(binary, args)
names = list(commands)
expected = None
for repeat in range(repeats):
for name in names[repeat % 4:] + names[:repeat % 4]:
binary, args = commands[name]
raw = native(binary, args)
raw['metrics'] = fields(raw['stdout'])
if name.startswith('solver'):
answers = seed_results(raw['stdout'])
if len(answers) != 24 or 61692151016558 not in answers:
raise RuntimeError('fixed-position solver did not recover 24 known seeds')
if expected is None:
expected = answers
elif expected != answers:
raise RuntimeError('CPU/CUDA solver seed tuples mismatch')
collected[name].append(raw)
result = {'raw': collected, 'seeds': expected, 'equality': True}
for name, rows in collected.items():
key = 'milliseconds' if 'cuda' in name else 'seconds'
scale = 1 if key == 'milliseconds' else 1000
result[name] = dict(compute=summarize([r['metrics'][key]*scale for r in rows]),
process=summarize([r['process_ms'] for r in rows]))
if name.startswith('brute'):
result[name]['projected_full48_ms'] = statistics.median(
r['metrics']['projected_full48_seconds']*1000 for r in rows)
return result
def upper(repeats):
print('隔离测量高 13 位:枚举与解析求解', flush=True)
raw = native('08_upper_compare', ['--planes', '65536', '--repeats', str(repeats)])
return dict(raw=raw, data=json.loads(raw['stdout']))
def report(data):
text = [
'# show:逐步实现与本次实测', '',
f"测量时间:{data['metadata']['date']}(Asia/Hong_Kong)。所有表格由 `benchmark.py` 自动生成。",
'CPU:AMD Ryzen 7 5800H,WSL,16 个逻辑线程。GPU 型号见下面逐项输出;编译器、驱动、源码 SHA256 和全部样本保存在 `results/latest.json`。',
'', '## 1. 分段对比:相邻版本在同一负载下测试', '',
'主线分成 A、B、C 三组,横线表示更换测试负载。分界两边重复测量衔接版本:A/B 都有 02,B/C 都有 03;只在组内计算速度比。展平实现已移到 experiments,不属于这个阶段。',
'原来只有 64 个 seed 的极端负载移到 `results/mapping_diagnostics.json`,用于解释并行度,不再作为主线 02→03 的优化倍数。',
'GPU 各对照版本核对完整 `(seed,x,z)` 命中列表;所有 GPU 命中另由独立 Python Java 算术核验。A/C 与完整 CPU 扫描核对,B 的独立 CPU 验证采用明确列出的窗口。每个版本先跑实际工作量预热,GPU 测量顺序轮换。',
'CPU 计时包含线程创建、搜索与结果收集;GPU kernel 时间用 CUDA event,scan 时间另含清零、同步与结果回传。两者均不含结果排序,GPU scan 不含一次性的上下文创建及内存分配。',
'A 的 01→02 用 CPU 搜索时间和 GPU scan 时间比较;B/C 的相邻 CUDA 版本用 kernel 时间比较。B/C 的 CPU 仅用于正确性验证,不列入性能表。', '',
]
if 'mapping_updated_at' in data['metadata']:
text += [f"线程映射分段改测时间:{data['metadata']['mapping_updated_at']}。后面的算法实验沿用上方日期的实测数据,分段实验测量时的源码 SHA256 记录在 metadata.mapping_source_sha256,C 单独复测的源码记录在 metadata.C_source_sha256。", '']
if 'C_updated_at' in data['metadata']:
text += [f"C 组复测时间:{data['metadata']['C_updated_at']},100 轮轮换测量。", '']
labels = {2:'02 CUDA/每个 thread 一个 seed', 3:'03 CUDA/固定坐标,复用多项式后遍历 seed',
4:'04 CUDA/tiled outer sum'}
ids = {2:'02', 3:'03', 4:'04'}
for index, case in enumerate(data['mapping']):
a, cpu, gpu = case['case'], case['cpu'], case['gpu']
if index:
text += ['---', '', '**测试负载分界:下一组重新测量衔接版本,不跨横线计算加速比。**', '']
text += [f"### {a['name']}", '',
f"`seed={a['seed']}` 起共 {a['seeds']:,} 个 seed,{a['width']}×{a['height']} 个锚点,实心 5×5。候选总数 {gpu['candidate_count']:,}。",
f"GPU:{gpu['gpu']}。结果完全一致,命中 {len(gpu['hits'])} 个元组;每版完整预热一次,计时 {len(gpu['rows'][0]['kernel_samples_ms'])} 轮。", '',
'| 版本 | CPU 搜索/GPU kernel 中位数 | GPU scan 中位数 |',
'| --- | ---: | ---: |']
if a['cpu_in_table']:
text.append(f"| 01 CPU | {cpu['median_ms']:.3f} ms | — |")
for row in gpu['rows']:
t = row['kernel_median_ms']
text.append(f"| {labels[row['mode']]} | {t:.3f} ms | {row['scan_median_ms']:.3f} ms |")
by_id = {ids[row['mode']]: row for row in gpu['rows']}
text += ['', '| 相邻/分支对照 | 同口径速度比(前项时间/后项时间) |', '| --- | ---: |']
for before, after in a['comparisons']:
if before == '01':
ratio = cpu['median_ms']/by_id[after]['scan_median_ms']
region = 'CPU 搜索/GPU scan'
else:
ratio = by_id[before]['kernel_median_ms']/by_id[after]['kernel_median_ms']
region = 'GPU kernel'
text.append(f'| {before} → {after}({region}) | {ratio:.3f}× |')
if index == 0:
text += ['', '65536 个 seed 为版本 02 提供足够的独立线程,在 257×257 的完整地图上测量 CPU→CUDA。']
elif index == 1:
text += ['', '本组使用 65536 个 seed,坐标范围扩大到 4097×4097(16785409 个坐标任务)。02/03 都完整扫描相同的笛卡尔积,GPU 命中列表完全一致。',
'独立 CPU 核对全部 65536 个 seed 在 `[50941,51197] × [418996,419252]` 的 257×257 窗口;这是窗口验证,不是完整大范围 CPU 重扫。所有 GPU 命中均另作独立精确算术验证。']
else:
text += ['', '本组改为固定 seed,重新测量 03 后再比较 04。03、04 均保持固定坐标后遍历 seed 的策略。']
text += ['']
text += ['随机数性质的探索独立成章,见 [RANDOM_PROPERTIES.md](RANDOM_PROPERTIES.md)。从状态空间、低位依赖和模运算约束推导搜索方法,不列为局部计时优化。', '',
'## 2. 固定 seed:全坐标范围余数过滤', '']
r = data['residue']
# Project only from the largest measured rectangle, not from a tiny launch.
rectangular = data['mapping'][-1]
direct = next(row for row in rectangular['gpu']['rows'] if row['mode']==3)
projected = direct['kernel_median_ms'] * r['counts']['world_positions']/rectangular['gpu']['candidate_count']
text += [f"固定 seed `61692151016558`;两轴锚点范围 `[-1875000,1874999]`,总计 {r['counts']['world_positions']:,} 个位置。",
f"完整扫描 2^36 个余数对,保留 {r['counts']['residue_survivors']:,} 个,展开 {r['counts']['expanded_candidates']:,} 个候选,得到 `(51069,419124)`。", '',
'| 时间区域 | 中位数 |', '| --- | ---: |',
f"| 余数 GPU kernel(实测) | {r['kernel']['median_ms']/1000:.3f} s |",
f"| 表准备+kernel+CPU 精查(实测) | {r['compute']['median_ms']/1000:.3f} s |",
f"| 独立进程全流程(实测) | {r['process']['median_ms']/1000:.3f} s |",
f"| 按 8193×8193 直接扫描吞吐外推全范围 | {projected/1000:.1f} s |", '',
f"外推直接 kernel 相对余数实测计算流程约 {projected/r['compute']['median_ms']:.1f}×。分母包含余数方法的准备和 CPU 精查;没有声称实际跑完直接全范围搜索。",
'07 沿用 research 的首次输出候选模型,尚未覆盖 Java 拒绝尾部例外。锚点范围允许图案向外延伸四格。', '',
'## 3. 固定坐标:seed 搜索', '']
s = data['seeds']
text += ['锚点 `(51069,419124)`、实心 5×5。CPU/CUDA 解析流程均恢复同一组 24 个已知 seed。', '',
'| 版本 | 实测规模 | 搜索/kernel 中位数 | 进程全流程中位数 | 完整 2^48 外推 |',
'| --- | --- | ---: | ---: | ---: |']
for name, scale in [('brute_cpu','2^28 seed'), ('brute_cuda','2^34 seed'),
('solver_cpu','全部 lower35 候选+upper13 解析'),
('solver_cuda','全部 lower35 候选+upper13 解析')]:
row = s[name]
projection = f"{row['projected_full48_ms']/1000:.1f} s" if 'projected_full48_ms' in row else '—'
text.append(f"| {name} | {scale} | {row['compute']['median_ms']:.3f} ms | {row['process']['median_ms']:.3f} ms | {projection} |")
text += ['', f"同 GPU 算法比值:暴力完整空间外推/解析 kernel 约 {s['brute_cuda']['projected_full48_ms']/s['solver_cuda']['compute']['median_ms']:,.0f}×。",
'solver_cpu 的程序内 seconds 计时从通过筛选的低 18 位取值 生成之后开始;进程全流程覆盖全部准备。CUDA 的 milliseconds 为 kernel 时间。',
'解析流程是首次输出候选模型,不是含全部拒绝尾部分支的严格穷尽。', '',
'### 高 13 位优化的受控拆解', '',
'相同的 65536 个连续 lower35 平面,从 `16420645998` 开始。枚举与解析都使用首次输出候选模型,并准确验证最终候选。',
'这里的 lower35 指世界种子的低 35 位。', '',
'| 高位策略 | kernel 中位数 |', '| --- | ---: |']
rows = data['upper']['data']['rows']
for row in rows:
text.append(f"| {row['mode']} | {row['kernel_median_ms']:.3f} ms |")
text += ['', f"完整候选结果相同。局部解析加速 {rows[0]['kernel_median_ms']/rows[1]['kernel_median_ms']:.3f}×,只适用于这组供给的 lower35 平面。", '']
text += ['## 复现与边界', '',
'```bash', 'make -C show -j4', 'python3 show/test_steps.py --gpu',
f"python3 show/benchmark.py --repeats {data['metadata']['repeats']}",
'```', '',
'GPU 运行需要宿主 GPU 可用。计时期间 GPU 同时承担桌面工作,时钟、温度和系统负载会影响数字;全部原始样本可检查波动。',
'01–04 精确覆盖被请求的有限输入范围。07/08 的生成器保留研究原型的拒绝尾部覆盖限制;全 seed × 全世界联合生成仍未实现。',
'本报告不把“潜在笛卡尔积大小”当作实际访问次数,也不把局部加速比连乘。', '']
return '\n'.join(text)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--repeats', type=int, default=5)
parser.add_argument('--only-mapping', action='store_true',
help='remeasure A/B/C and regenerate report, retaining unchanged algorithm phases')
args = parser.parse_args()
if args.repeats < 1:
parser.error('repeats must be positive')
if args.only_mapping:
path = ROOT/'results/latest.json'
data = json.loads(path.read_text())
mapping_files = {'01_cpu.cpp', 'cuda_steps.cu', '02_cuda_seed.cuh',
'03_cuda_position.cuh', '04_cuda_outer_sum.cuh'}
current_mapping_sources = {}
for name, digest in data['metadata']['source_sha256'].items():
current = hashlib.sha256((ROOT/name).read_bytes()).hexdigest()
if name in mapping_files:
current_mapping_sources[name] = current
elif current != data['metadata'].get('verification_source_sha256', {}).get(name, digest):
raise RuntimeError('shared/algorithm source changed; run the full benchmark: '+name)
diagnostics = ROOT/'results/mapping_diagnostics.json'
if not diagnostics.exists():
diagnostics.write_text(json.dumps(dict(metadata=data['metadata'], mapping=data['mapping'],
note='Original imbalance experiments; diagnostics only, not the main adjacent-version comparison.'),
ensure_ascii=False, indent=2)+'\n')
data['mapping'] = mapping(args.repeats)
data['metadata']['mapping_updated_at'] = datetime.now(ZoneInfo('Asia/Hong_Kong')).isoformat()
data['metadata']['mapping_repeats'] = args.repeats
data['metadata']['mapping_source_sha256'] = current_mapping_sources
data['metadata']['C_updated_at'] = data['metadata']['mapping_updated_at']
data['metadata']['C_source_sha256'] = {name: digest for name, digest in current_mapping_sources.items() if name != '01_cpu.cpp'}
data['metadata']['C_source_sha256']['common.hpp'] = hashlib.sha256((ROOT/'common.hpp').read_bytes()).hexdigest()
path.write_text(json.dumps(data, ensure_ascii=False, indent=2)+'\n')
(ROOT/'BENCHMARK.md').write_text(report(data))
print('完成分段改测:show/BENCHMARK.md 和 show/results/latest.json', flush=True)
return
cpu = next(line.split(':', 1)[1].strip() for line in Path('/proc/cpuinfo').read_text().splitlines()
if line.startswith('model name'))
paths = sorted(ROOT.glob('*.cpp'))+sorted(ROOT.glob('*.cu'))+sorted(ROOT.glob('*.cuh'))+[
ROOT/'common.hpp', ROOT/'../research/slime_solver_core.hpp']
data = {'metadata': dict(date=datetime.now(ZoneInfo('Asia/Hong_Kong')).isoformat(),
cpu=cpu, platform=platform.platform(), repeats=args.repeats,
gpu=capture(['nvidia-smi', '--query-gpu=name,driver_version,temperature.gpu,power.draw,utilization.gpu', '--format=csv']),
nvcc=capture(['nvcc', '--version']), gxx=capture(['g++', '--version']),
source_sha256={str(p.relative_to(ROOT)): hashlib.sha256(p.read_bytes()).hexdigest() for p in paths})}
# Checkpoint after each phase, so an interrupted long run preserves its completed evidence.
out = ROOT/'results'
out.mkdir(exist_ok=True)
for name, function in [('mapping', lambda: mapping(args.repeats)),
('residue', lambda: residue(args.repeats)),
('seeds', lambda: seeds(args.repeats)),
('upper', lambda: upper(args.repeats))]:
data[name] = function()
(out/'latest.json').write_text(json.dumps(data, ensure_ascii=False, indent=2)+'\n')
(ROOT/'BENCHMARK.md').write_text(report(data))
print('完成:show/BENCHMARK.md 和 show/results/latest.json', flush=True)
if __name__ == '__main__':
main()

172
show/common.hpp Normal file
View file

@ -0,0 +1,172 @@
#pragma once
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <string>
#include <tuple>
#include <vector>
#ifdef __CUDACC__
#define HD __host__ __device__
#else
#define HD
#endif
namespace show {
constexpr uint64_t A = 0x5DEECE66DULL;
constexpr uint64_t B = 11;
constexpr uint64_t MASK48 = (1ULL << 48) - 1;
constexpr uint64_t SLIME_XOR = 987234911;
constexpr uint32_t REJECTION_START = 2147483640U;
constexpr int MAX_N = 8;
// Explicit Java int wrapping and sign extension; no signed overflow in C++.
HD inline uint64_t javaLong(uint32_t v) {
return (v & 0x80000000U) ? uint64_t(v) | 0xFFFFFFFF00000000ULL : uint64_t(v);
}
HD inline uint64_t xTerm(int32_t x) {
const uint32_t u = uint32_t(x);
return javaLong(u*u*4987142U) + javaLong(u*5947611U);
}
HD inline uint64_t zTerm(int32_t z) {
const uint32_t u = uint32_t(z);
return javaLong(u*u)*4392871ULL + javaLong(u*389711U);
}
HD inline uint64_t term(int32_t x, int32_t z) { return xTerm(x) + zTerm(z); }
// Stage 01/02/03/04: the original two-XOR JavaRandom path, with defined arithmetic.
struct JavaRandom {
uint64_t state;
HD explicit JavaRandom(uint64_t input) : state((input ^ A) & MASK48) {}
HD uint32_t next31() {
state = (state*A+B) & MASK48;
return uint32_t(state >> 17);
}
HD bool slime() {
for (;;) {
const uint32_t q = next31();
if (q < REJECTION_START) return q % 10 == 0;
}
}
};
HD inline bool original(uint64_t seed, uint64_t t) {
JavaRandom random((seed+t) ^ SLIME_XOR);
return random.slime();
}
// State equations used by the Random-properties discussion and analytical demos.
HD inline uint64_t firstState(uint64_t seed, uint64_t t) {
const uint64_t u = ((seed+t) ^ (SLIME_XOR ^ A)) & MASK48;
return (u*A+B) & MASK48;
}
HD inline bool fromState(uint64_t state) {
for (;;) {
const uint32_t q = uint32_t(state >> 17);
if (q < REJECTION_START) return q % 10 == 0;
state = (state*A+B) & MASK48;
}
}
HD inline bool fused(uint64_t seed, uint64_t t) { return fromState(firstState(seed,t)); }
template<int stage> HD inline bool solid(uint64_t seed, int32_t x, int32_t z, int n) {
for (int dx=0; dx<n; ++dx) {
for (int dz=0; dz<n; ++dz) {
const uint64_t t = term(x+dx,z+dz);
if (!original(seed,t)) return false;
}
}
return true;
}
struct Args {
uint64_t seed = 61692151016558ULL, seeds = 1;
int32_t x0 = 50941, z0 = 418996;
uint64_t width = 257, height = 257;
int n = 5, repeats = 5, cpuThreads = 16;
uint64_t capacity = 100000;
std::string modes = "all";
uint64_t positions() const { return width*height; }
uint64_t work() const { return seeds*positions(); }
};
inline uint64_t positive(const std::string& s) {
if (s.empty() || s[0]=='-') throw std::runtime_error("expected unsigned integer");
size_t end=0;
const uint64_t v=std::stoull(s,&end);
if (end!=s.size()) throw std::runtime_error("invalid integer");
return v;
}
inline Args parse(int argc, char** argv) {
Args a;
for (int i=1;i<argc;++i) {
const std::string key=argv[i];
if(key=="--help") {
std::cout << "--seed N --seeds N --x0 X --z0 Z --width N --height N -n N "
"--repeats N --cpu-threads N --capacity N --modes all|2,3,4\n";
std::exit(0);
}
if(++i>=argc) throw std::runtime_error("missing value: "+key);
const std::string v=argv[i];
if(key=="--seed") a.seed=positive(v);
else if(key=="--seeds") a.seeds=positive(v);
else if(key=="--x0") a.x0=std::stoi(v);
else if(key=="--z0") a.z0=std::stoi(v);
else if(key=="--width") a.width=positive(v);
else if(key=="--height") a.height=positive(v);
else if(key=="-n") a.n=std::stoi(v);
else if(key=="--repeats") a.repeats=std::stoi(v);
else if(key=="--cpu-threads") a.cpuThreads=std::stoi(v);
else if(key=="--capacity") a.capacity=positive(v);
else if(key=="--modes") a.modes=v;
else throw std::runtime_error("unknown option: "+key);
}
if(a.n<1 || a.n>MAX_N || !a.seeds || !a.width || !a.height ||
a.repeats<1 || a.cpuThreads<1 || a.cpuThreads>256 || !a.capacity || a.capacity>10000000 ||
a.width>10000000 || a.height>10000000 || a.seeds>10000000)
throw std::runtime_error("invalid range/size/count");
if(a.seeds-1 > UINT64_MAX-a.seed || a.seeds > UINT64_MAX/a.positions())
throw std::runtime_error("seed/work range overflow");
// Include the expanded 16x16 tile's unused fringe so CUDA precomputation is safe.
if(int64_t(a.x0)+int64_t((a.width+15)/16*16)+a.n-2 > INT32_MAX ||
int64_t(a.z0)+int64_t((a.height+15)/16*16)+a.n-2 > INT32_MAX)
throw std::runtime_error("coordinate footprint/tile exceeds int32");
return a;
}
struct Hit {
uint64_t seed;
int32_t x,z;
bool operator<(const Hit& o) const { return std::tie(seed,x,z)<std::tie(o.seed,o.x,o.z); }
bool operator==(const Hit& o) const { return seed==o.seed && x==o.x && z==o.z; }
};
inline double ms(std::chrono::steady_clock::time_point t) {
return std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-t).count();
}
inline double median(std::vector<double> v) {
std::sort(v.begin(),v.end());
return v.size()%2 ? v[v.size()/2] : (v[v.size()/2-1]+v[v.size()/2])/2;
}
inline void array(const std::vector<double>& v) {
std::cout << '[';
for(size_t i=0;i<v.size();++i) { if(i) std::cout << ','; std::cout << v[i]; }
std::cout << ']';
}
inline void results(const std::vector<Hit>& v) {
std::cout << '[';
for(size_t i=0;i<v.size();++i) {
if(i) std::cout << ',';
std::cout << '[' << v[i].seed << ',' << v[i].x << ',' << v[i].z << ']';
}
std::cout << ']';
}
inline void workload(const Args& a) {
std::cout << "\"seed_start\":" << a.seed << ",\"seeds\":" << a.seeds
<< ",\"x0\":" << a.x0 << ",\"z0\":" << a.z0
<< ",\"width\":" << a.width << ",\"height\":" << a.height
<< ",\"n\":" << a.n << ",\"candidate_count\":" << a.work();
}
} // namespace show

131
show/cuda_steps.cu Normal file
View file

@ -0,0 +1,131 @@
#include "common.hpp"
#include <cuda_runtime.h>
#include <map>
void check(cudaError_t err) {
if(err!=cudaSuccess) throw std::runtime_error(cudaGetErrorString(err));
}
// std::string in host Args must never be copied into a device kernel.
struct DeviceArgs {
uint64_t seed,seeds,width,height,capacity;
int32_t x0,z0;
int n;
};
__device__ void emit(DeviceArgs a,show::Hit* hits,unsigned long long* count,
uint64_t seed,int32_t x,int32_t z) {
const auto slot=atomicAdd(count,1ULL);
if(slot<a.capacity) hits[slot]={seed,x,z};
}
#include "02_cuda_seed.cuh"
#include "03_cuda_position.cuh"
#include "04_cuda_outer_sum.cuh"
struct Buffers {
show::Hit* hits=nullptr;
unsigned long long* count=nullptr;
cudaEvent_t begin=nullptr,end=nullptr;
~Buffers() {
if(begin) cudaEventDestroy(begin);
if(end) cudaEventDestroy(end);
if(count) cudaFree(count);
if(hits) cudaFree(hits);
}
};
struct Run { double kernelMs,scanMs; std::vector<show::Hit> hits; };
Run run(int mode,const show::Args& a,int maxBlocks,Buffers& b) {
const auto wall=std::chrono::steady_clock::now();
check(cudaMemset(b.count,0,sizeof(unsigned long long)));
const DeviceArgs d{a.seed,a.seeds,a.width,a.height,a.capacity,a.x0,a.z0,a.n};
const int threads=256;
const uint64_t work=mode==2 ? a.seeds : a.positions();
const int blocks=int(std::min<uint64_t>((work+threads-1)/threads,maxBlocks));
check(cudaEventRecord(b.begin));
switch(mode) {
case 2: seedKernel<<<blocks,threads>>>(d,b.hits,b.count); break;
case 3: launchCoordinate(blocks,threads,d,b.hits,b.count); break;
case 4: {
const uint64_t tiles=(a.width+15)/16*((a.height+15)/16);
if((a.height+15)/16<=65535) {
const dim3 grid(unsigned((a.width+15)/16),unsigned((a.height+15)/16));
outerRectKernel<<<grid,dim3(16,16)>>>(d,b.hits,b.count);
} else {
outerKernel<<<int(std::min<uint64_t>(tiles,0x7FFFFFFFULL)),dim3(16,16)>>>(d,b.hits,b.count);
}
break;
}
default: throw std::runtime_error("mode must be 2..4");
}
check(cudaGetLastError()); check(cudaEventRecord(b.end));
check(cudaEventSynchronize(b.end));
float kernel=0; check(cudaEventElapsedTime(&kernel,b.begin,b.end));
unsigned long long count=0;
check(cudaMemcpy(&count,b.count,sizeof(count),cudaMemcpyDeviceToHost));
if(count>a.capacity) throw std::runtime_error("hit buffer overflow; raise --capacity");
std::vector<show::Hit> hits(count);
if(count) check(cudaMemcpy(hits.data(),b.hits,count*sizeof(show::Hit),cudaMemcpyDeviceToHost));
const double wallMs=show::ms(wall);
std::sort(hits.begin(),hits.end()); // equality checks are outside timed scan
return {kernel,wallMs,std::move(hits)};
}
int main(int argc,char** argv) {
try {
const auto start=std::chrono::steady_clock::now();
const auto a=show::parse(argc,argv);
std::vector<int> modes;
if(a.modes=="all") modes={2,3,4};
else {
for(size_t p=0;p<a.modes.size();) {
const size_t end=a.modes.find(',',p);
const std::string token=a.modes.substr(p,end==std::string::npos?end:end-p);
const auto m=show::positive(token);
if(m<2 || m>4 || std::find(modes.begin(),modes.end(),int(m))!=modes.end())
throw std::runtime_error("invalid/duplicate CUDA mode");
modes.push_back(int(m));
if(end==std::string::npos) break;
p=end+1;
}
}
if(modes.empty()) throw std::runtime_error("empty mode list");
int device=0; cudaDeviceProp prop{};
check(cudaGetDevice(&device)); check(cudaGetDeviceProperties(&prop,device));
const int maxBlocks=prop.multiProcessorCount*32;
Buffers b;
check(cudaMalloc(&b.hits,a.capacity*sizeof(show::Hit)));
check(cudaMalloc(&b.count,sizeof(unsigned long long)));
check(cudaEventCreate(&b.begin)); check(cudaEventCreate(&b.end));
const double setup=show::ms(start);
std::vector<show::Hit> expected;
bool first=true;
for(int mode:modes) {
const auto r=run(mode,a,maxBlocks,b); // warm every kernel on actual workload
if(first) { expected=r.hits; first=false; }
else if(r.hits!=expected) throw std::runtime_error("CUDA warmups disagree");
}
std::map<int,std::vector<double>> kernel,wall;
for(int repeat=0;repeat<a.repeats;++repeat) {
for(size_t j=0;j<modes.size();++j) {
const int mode=modes[(j+repeat)%modes.size()]; // rotate order to reduce clock/order bias
auto r=run(mode,a,maxBlocks,b);
if(r.hits!=expected) throw std::runtime_error("CUDA modes/repeats disagree");
kernel[mode].push_back(r.kernelMs); wall[mode].push_back(r.scanMs);
}
}
std::cout.precision(10);
std::cout << '{'; show::workload(a);
std::cout << ",\"gpu\":\"" << prop.name << "\",\"setup_ms\":" << setup
<< ",\"comparison_equal\":true,\"rows\":[";
for(size_t i=0;i<modes.size();++i) {
const int m=modes[i]; if(i) std::cout << ',';
std::cout << "{\"mode\":" << m << ",\"kernel_samples_ms\":";
show::array(kernel[m]);
std::cout << ",\"kernel_median_ms\":" << show::median(kernel[m])
<< ",\"scan_samples_ms\":"; show::array(wall[m]);
std::cout << ",\"scan_median_ms\":" << show::median(wall[m]) << '}';
}
std::cout << "],\"hits\":"; show::results(expected); std::cout << "}\n";
} catch(const std::exception& e) { std::cerr << e.what() << '\n'; return 1; }
}

View file

@ -0,0 +1,6 @@
# 主线之外的实验
`flattened_position.cuh` 保存此前把 `(seed,x,z)` 展平的实现。
它已从 `cuda_steps.cu` 和默认构建移除,不再作为 03 或 Random 优化的基础。
主线只讲按 seed → 固定坐标后遍历 seed → 复用轴项。
旧测速中的 mode 31 与 03b 标签属于历史实验,见 `../results/mapping_diagnostics.json`。

View file

@ -0,0 +1,16 @@
#pragma once
// Archived experiment; excluded from the teaching sequence and normal build.
// Stage 03b (mode 31): flatten the whole (seed,x,z) product into fine work items.
template<int stage> __global__ void positionKernel(
DeviceArgs a, show::Hit* hits, unsigned long long* count
) {
const uint64_t area=a.width*a.height, total=a.seeds*area;
const uint64_t stride=uint64_t(gridDim.x)*blockDim.x;
for(uint64_t i=uint64_t(blockIdx.x)*blockDim.x+threadIdx.x;i<total;i+=stride) {
const uint64_t s=i/area, p=i%area;
const int32_t x=int32_t(int64_t(a.x0)+int64_t(p/a.height));
const int32_t z=int32_t(int64_t(a.z0)+int64_t(p%a.height));
if(show::solid<stage>(a.seed+s,x,z,a.n)) emit(a,hits,count,a.seed+s,x,z);
}
}

View file

@ -0,0 +1 @@
{"seed_start":61692151016558,"seeds":1,"x0":46973,"z0":415028,"width":8193,"height":8193,"n":5,"candidate_count":67125249,"gpu":"NVIDIA GeForce RTX 3060 Laptop GPU","setup_ms":261.544059,"comparison_equal":true,"rows":[{"mode":3,"kernel_samples_ms":[2.719743967,4.548607826,2.719743967,2.71359992,3.421184063,2.719743967,2.721791983,4.546559811,2.71359992,2.714623928,2.717695951,2.719743967,2.716671944,5.061632156,2.717695951,2.718719959,2.71359992,2.715647936,2.71359992,4.462592125,2.715647936,2.719743967,2.718719959,2.718719959,2.714623928,3.991552114,4.080639839,2.715647936,2.716671944,2.718719959,2.714623928,3.131392002,4.841472149,2.718719959,2.717695951,2.71359992,2.718719959,2.718719959,5.118976116,2.717695951,2.718719959,3.597311974,2.717695951,2.714623928,4.768767834,2.353152037,2.357248068,4.742144108,2.61836791,2.350080013,2.343935966,2.343935966,2.352128029,2.34700799,2.619391918,2.369535923,2.367487907,2.363392115,4.854784012,2.362368107,2.362368107,4.595712185,2.362368107,2.361344099,2.367487907,2.361344099,2.365439892,2.360320091,3.052544117,2.360320091,2.360320091,2.371583939,4.817920208,2.376703978,2.370559931,4.264959812,2.379776001,2.385920048,2.380800009,2.371583939,2.370559931,2.370559931,2.872319937,2.38489604,2.379776001,4.811776161,2.378751993,2.367487907,2.360320091,2.362368107,3.404799938,3.628031969,6.178815842,2.3664639,2.361344099,2.349056005,2.349056005,2.349056005,4.595712185,2.374655962],"kernel_median_ms":2.714623928,"scan_samples_ms":[3.361799,5.427067,3.262892,3.256716,4.0131,3.125633,3.140041,5.201408,3.133867,3.08945,3.14145,3.247291,3.207966,5.791283,3.204174,3.179908,3.246966,3.235266,3.267225,5.118424,3.255958,3.1889,3.150117,3.118158,3.131592,4.524433,4.937291,3.1382,3.121191,3.147733,3.128991,3.577925,5.585233,3.1161,3.103642,3.121733,3.170592,3.147192,5.81685,3.104834,3.176983,4.392916,3.136574,3.098659,5.483508,2.83205,2.804316,5.437791,3.179041,2.733033,2.688617,2.784058,2.816017,2.7391,3.169725,2.728158,2.768133,2.930417,5.615133,2.786441,2.788933,5.239433,2.7404,2.761416,2.738233,2.7417,2.744516,2.758059,3.5204,2.721117,2.760225,2.75275,5.519799,2.753183,2.783083,4.988533,2.859675,2.929659,2.876033,2.840716,2.790558,2.781891,3.403725,3.063125,2.996175,5.555333,2.8769,2.736175,2.819375,2.763041,3.908559,4.207558,7.026717,2.722525,2.790125,2.696308,2.720684,2.699992,5.185483,2.718517],"scan_median_ms":3.127312},{"mode":4,"kernel_samples_ms":[1.602560043,1.604608059,1.602560043,1.598464012,3.100672007,1.602560043,1.602560043,1.597440004,1.598464012,1.597440004,3.857408047,1.598464012,1.597440004,1.602560043,1.602560043,1.603584051,3.83180809,2.126847982,1.598464012,1.598464012,1.602560043,1.602560043,3.572736025,1.602560043,1.602560043,1.602560043,1.602560043,1.597440004,3.282943964,1.602560043,1.605631948,1.602560043,1.603584051,1.602560043,2.043904066,2.509824038,1.602560043,1.602560043,1.606655955,1.602560043,1.597440004,2.599936008,1.598464012,1.602560043,1.598464012,1.469439983,1.392639995,1.388543963,1.389567971,1.384448051,1.619968057,2.686975956,1.387519956,1.387519956,2.667520046,1.398784041,1.397760034,1.39468801,1.393664002,1.393664002,1.397760034,1.946624041,1.395712018,1.39468801,2.056191921,2.760704041,1.395712018,1.395712018,1.894400001,1.39468801,1.391615987,1.397760034,1.396736026,1.400832057,1.400832057,1.396736026,1.401855946,1.401855946,1.401855946,2.65625596,1.398784041,1.397760034,2.949120045,1.401855946,1.404927969,1.406975985,1.404927969,1.404927969,2.736128092,2.593791962,1.398784041,1.395712018,1.402879953,1.402879953,2.193408012,2.944000006,1.39468801,1.39468801,1.401855946,1.404927969],"kernel_median_ms":1.598464012,"scan_samples_ms":[2.081192,2.017816,2.022583,1.983042,3.886783,1.923133,1.953575,1.913058,1.916525,1.901358,4.444266,1.985209,1.931908,1.913384,1.983909,1.960941,4.444699,2.614408,1.977083,1.954767,1.934833,1.912733,4.119917,1.919666,1.9136,1.901574,1.923675,2.015975,3.7778,1.9786,1.969716,1.907533,1.9669,1.9149,2.430566,3.039184,1.923458,1.904283,1.922158,1.914358,1.891283,3.0251,1.904284,1.930391,1.994199,1.850008,1.762042,1.787716,1.692925,1.673858,2.019116,3.393867,1.702241,1.683825,3.373283,1.712966,1.703217,1.828233,1.725641,1.709392,1.690867,2.282366,1.692491,1.671692,2.446708,3.304491,1.735825,1.695308,2.4349,1.689999,1.697909,1.716975,1.776342,1.6809,1.711992,1.761392,1.805592,1.847625,1.826934,3.585725,1.699317,1.695308,3.960775,1.921508,1.832675,1.792158,1.675484,1.686641,3.621367,3.119025,1.729975,1.696934,1.825742,1.665517,2.606825,3.577491,1.65945,1.658258,1.75695,1.668333],"scan_median_ms":1.913492}],"hits":[[61692151016558,51069,419124]]}

View file

@ -0,0 +1,324 @@
{
"note": "地图规模变化的历史诊断实验,已移出视频/博客主线,不作为新的优化步骤。",
"metadata": {
"date": "2026-10-08T00:28:41.318982+08:00",
"cpu": "AMD Ryzen 7 5800H with Radeon Graphics",
"platform": "Linux-6.18.40.1-microsoft-standard-WSL2-x86_64-with-glibc2.39",
"repeats": 5,
"gpu": {
"command": [
"nvidia-smi",
"--query-gpu=name,driver_version,temperature.gpu,power.draw,utilization.gpu",
"--format=csv"
],
"process_ms": 78.23648804333061,
"stdout": "name, driver_version, temperature.gpu, power.draw [W], utilization.gpu [%]\nNVIDIA GeForce RTX 3060 Laptop GPU, 596.49, 67, 19.65 W, 29 %\n",
"stderr": ""
},
"nvcc": {
"command": [
"nvcc",
"--version"
],
"process_ms": 31.78272402146831,
"stdout": "nvcc: NVIDIA (R) Cuda compiler driver\nCopyright (c) 2005-2023 NVIDIA Corporation\nBuilt on Fri_Jan__6_16:45:21_PST_2023\nCuda compilation tools, release 12.0, V12.0.140\nBuild cuda_12.0.r12.0/compiler.32267302_0\n",
"stderr": ""
},
"gxx": {
"command": [
"g++",
"--version"
],
"process_ms": 7.172641053330153,
"stdout": "g++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0\nCopyright (C) 2023 Free Software Foundation, Inc.\nThis is free software; see the source for copying conditions. There is NO\nwarranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n\n",
"stderr": ""
},
"source_sha256": {
"01_cpu.cpp": "b7fc5d25b32050bd3defdb3934e6bb4f1dd8b5d83d529bef8b8a90d719d17e8b",
"08_seed_cpu.cpp": "b9722cc52e46eb720fb78ea1d3f0531249cf656d274771fd8c2bad9f06f27800",
"07_residue.cu": "873bea97faec42ce87620ea8b6bea3ffdb329ba2cf7924ab2d11719a91347b38",
"08_seed_cuda.cu": "e4e95df388e572c21fed0e87b81299265ed66221f7d37b90d5c8feb7839a1ed7",
"08_upper_compare.cu": "f20dc272ebc821230c83d22ca5c9819e8b658b1b994025b2c31b1928f067335f",
"cuda_steps.cu": "b81d7c85f96c1e1f36fa6cb6907e17f62955606821e86959b038095e10f4957a",
"02_cuda_seed.cuh": "1f6a8bf1d780f569c1a685c67edd8242db9d658fb919247b0197c3b450854455",
"03_cuda_position.cuh": "69ceeb76a9954b073826a613fe9b343bc3cbb106f47546b3af2bd00df589e34c",
"04_cuda_outer_sum.cuh": "a316d3a4845351be996bee4930d817aa9d37edec05cbcf8e1869c086533a477d",
"common.hpp": "686d1964fe2fd5cb17709d9c5de7cbfbf0530a7a5622604a138a65194f100a13",
"../research/slime_solver_core.hpp": "feb1200518716b4d796fb60f60a83b6b798155bbd4e795531793181a5c7f9eba"
},
"mapping_updated_at": "2026-10-08T23:15:42.590910+08:00",
"mapping_repeats": 5,
"mapping_source_sha256": {
"01_cpu.cpp": "b7fc5d25b32050bd3defdb3934e6bb4f1dd8b5d83d529bef8b8a90d719d17e8b",
"cuda_steps.cu": "a5c639dafcdf581c35f4551195cf28e5ec82eac9b642631a6845fbfc6fa48cfb",
"02_cuda_seed.cuh": "1f6a8bf1d780f569c1a685c67edd8242db9d658fb919247b0197c3b450854455",
"03_cuda_position.cuh": "44f49a7db73e52ec87ac632a2cee2f7938a2241a356780e151b3ab0de5d9a506",
"04_cuda_outer_sum.cuh": "a316d3a4845351be996bee4930d817aa9d37edec05cbcf8e1869c086533a477d"
},
"C_updated_at": "2026-10-08T23:58:18.383398+08:00",
"verification_updated_at": "2026-10-08T23:58:18.383398+08:00",
"verification_source_sha256": {
"01_cpu.cpp": "b7fc5d25b32050bd3defdb3934e6bb4f1dd8b5d83d529bef8b8a90d719d17e8b",
"08_seed_cpu.cpp": "b9722cc52e46eb720fb78ea1d3f0531249cf656d274771fd8c2bad9f06f27800",
"07_residue.cu": "873bea97faec42ce87620ea8b6bea3ffdb329ba2cf7924ab2d11719a91347b38",
"08_seed_cuda.cu": "e4e95df388e572c21fed0e87b81299265ed66221f7d37b90d5c8feb7839a1ed7",
"08_upper_compare.cu": "f20dc272ebc821230c83d22ca5c9819e8b658b1b994025b2c31b1928f067335f",
"cuda_steps.cu": "1015892b920383399368527e3c8c5620a004276c9db8fcb5c67188a00a2d3e7a",
"02_cuda_seed.cuh": "1f6a8bf1d780f569c1a685c67edd8242db9d658fb919247b0197c3b450854455",
"03_cuda_position.cuh": "82ce3adb0bca11dab7a48f75fdccbbb207dbffa43a0c2ad8b19ec0f741f2738c",
"04_cuda_outer_sum.cuh": "a316d3a4845351be996bee4930d817aa9d37edec05cbcf8e1869c086533a477d",
"common.hpp": "9d0814927d6905964c3288d20f502a7797800e3382795e6e7c2cbbfccea6da33",
"../research/slime_solver_core.hpp": "feb1200518716b4d796fb60f60a83b6b798155bbd4e795531793181a5c7f9eba"
},
"C_source_sha256": {
"cuda_steps.cu": "1015892b920383399368527e3c8c5620a004276c9db8fcb5c67188a00a2d3e7a",
"02_cuda_seed.cuh": "1f6a8bf1d780f569c1a685c67edd8242db9d658fb919247b0197c3b450854455",
"03_cuda_position.cuh": "82ce3adb0bca11dab7a48f75fdccbbb207dbffa43a0c2ad8b19ec0f741f2738c",
"04_cuda_outer_sum.cuh": "a316d3a4845351be996bee4930d817aa9d37edec05cbcf8e1869c086533a477d",
"common.hpp": "9d0814927d6905964c3288d20f502a7797800e3382795e6e7c2cbbfccea6da33"
},
"verification_note": "Timing sequence ends at 04. Random properties moved into a theory chapter. All 13 correctness tests passed after removing the 05 timing branch; older phase measurements retain their original dates and hashes, C was remeasured with the current binary."
},
"range_sweep": [
{
"case": {
"seed": 61692151016558,
"seeds": 65536,
"x0": 50813,
"z0": 418868,
"width": 513,
"height": 513
},
"data": {
"seed_start": 61692151016558,
"seeds": 65536,
"x0": 50813,
"z0": 418868,
"width": 513,
"height": 513,
"n": 5,
"candidate_count": 17247043584,
"gpu": "NVIDIA GeForce RTX 3060 Laptop GPU",
"setup_ms": 214.74635,
"comparison_equal": true,
"rows": [
{
"mode": 2,
"kernel_samples_ms": [
566.0969238
],
"kernel_median_ms": 566.0969238,
"scan_samples_ms": [
613.641925
],
"scan_median_ms": 613.641925
},
{
"mode": 3,
"kernel_samples_ms": [
310.4040833
],
"kernel_median_ms": 310.4040833,
"scan_samples_ms": [
336.619617
],
"scan_median_ms": 336.619617
}
],
"hits": [
[
61692151016558,
51069,
419124
]
]
},
"raw": {
"command": [
"/mnt/d/dev/slime/show/bin/cuda_steps",
"--seed",
"61692151016558",
"--seeds",
"65536",
"--x0",
"50813",
"--z0",
"418868",
"--width",
"513",
"--height",
"513",
"-n",
"5",
"--repeats",
"1",
"--modes",
"2,3"
],
"process_ms": 2072.731592000082,
"stdout": "{\"seed_start\":61692151016558,\"seeds\":65536,\"x0\":50813,\"z0\":418868,\"width\":513,\"height\":513,\"n\":5,\"candidate_count\":17247043584,\"gpu\":\"NVIDIA GeForce RTX 3060 Laptop GPU\",\"setup_ms\":214.74635,\"comparison_equal\":true,\"rows\":[{\"mode\":2,\"kernel_samples_ms\":[566.0969238],\"kernel_median_ms\":566.0969238,\"scan_samples_ms\":[613.641925],\"scan_median_ms\":613.641925},{\"mode\":3,\"kernel_samples_ms\":[310.4040833],\"kernel_median_ms\":310.4040833,\"scan_samples_ms\":[336.619617],\"scan_median_ms\":336.619617}],\"hits\":[[61692151016558,51069,419124]]}\n",
"stderr": ""
}
},
{
"case": {
"seed": 61692151016558,
"seeds": 65536,
"x0": 50557,
"z0": 418612,
"width": 1025,
"height": 1025
},
"data": {
"seed_start": 61692151016558,
"seeds": 65536,
"x0": 50557,
"z0": 418612,
"width": 1025,
"height": 1025,
"n": 5,
"candidate_count": 68853760000,
"gpu": "NVIDIA GeForce RTX 3060 Laptop GPU",
"setup_ms": 229.882141,
"comparison_equal": true,
"rows": [
{
"mode": 2,
"kernel_samples_ms": [
2155.086914
],
"kernel_median_ms": 2155.086914,
"scan_samples_ms": [
2335.14515
],
"scan_median_ms": 2335.14515
},
{
"mode": 3,
"kernel_samples_ms": [
1197.010986
],
"kernel_median_ms": 1197.010986,
"scan_samples_ms": [
1297.136859
],
"scan_median_ms": 1297.136859
}
],
"hits": [
[
61692151016558,
51069,
419124
]
]
},
"raw": {
"command": [
"/mnt/d/dev/slime/show/bin/cuda_steps",
"--seed",
"61692151016558",
"--seeds",
"65536",
"--x0",
"50557",
"--z0",
"418612",
"--width",
"1025",
"--height",
"1025",
"-n",
"5",
"--repeats",
"1",
"--modes",
"2,3"
],
"process_ms": 7595.587693000198,
"stdout": "{\"seed_start\":61692151016558,\"seeds\":65536,\"x0\":50557,\"z0\":418612,\"width\":1025,\"height\":1025,\"n\":5,\"candidate_count\":68853760000,\"gpu\":\"NVIDIA GeForce RTX 3060 Laptop GPU\",\"setup_ms\":229.882141,\"comparison_equal\":true,\"rows\":[{\"mode\":2,\"kernel_samples_ms\":[2155.086914],\"kernel_median_ms\":2155.086914,\"scan_samples_ms\":[2335.14515],\"scan_median_ms\":2335.14515},{\"mode\":3,\"kernel_samples_ms\":[1197.010986],\"kernel_median_ms\":1197.010986,\"scan_samples_ms\":[1297.136859],\"scan_median_ms\":1297.136859}],\"hits\":[[61692151016558,51069,419124]]}\n",
"stderr": ""
}
},
{
"case": {
"seed": 61692151016558,
"seeds": 65536,
"x0": 50045,
"z0": 418100,
"width": 2049,
"height": 2049
},
"data": {
"seed_start": 61692151016558,
"seeds": 65536,
"x0": 50045,
"z0": 418100,
"width": 2049,
"height": 2049,
"n": 5,
"candidate_count": 275146407936,
"gpu": "NVIDIA GeForce RTX 3060 Laptop GPU",
"setup_ms": 218.339333,
"comparison_equal": true,
"rows": [
{
"mode": 2,
"kernel_samples_ms": [
8471.788086
],
"kernel_median_ms": 8471.788086,
"scan_samples_ms": [
9178.481975
],
"scan_median_ms": 9178.481975
},
{
"mode": 3,
"kernel_samples_ms": [
4668.789551
],
"kernel_median_ms": 4668.789551,
"scan_samples_ms": [
5058.365918
],
"scan_median_ms": 5058.365918
}
],
"hits": [
[
61692151016558,
51069,
419124
]
]
},
"raw": {
"command": [
"/mnt/d/dev/slime/show/bin/cuda_steps",
"--seed",
"61692151016558",
"--seeds",
"65536",
"--x0",
"50045",
"--z0",
"418100",
"--width",
"2049",
"--height",
"2049",
"-n",
"5",
"--repeats",
"1",
"--modes",
"2,3"
],
"process_ms": 28857.863240999905,
"stdout": "{\"seed_start\":61692151016558,\"seeds\":65536,\"x0\":50045,\"z0\":418100,\"width\":2049,\"height\":2049,\"n\":5,\"candidate_count\":275146407936,\"gpu\":\"NVIDIA GeForce RTX 3060 Laptop GPU\",\"setup_ms\":218.339333,\"comparison_equal\":true,\"rows\":[{\"mode\":2,\"kernel_samples_ms\":[8471.788086],\"kernel_median_ms\":8471.788086,\"scan_samples_ms\":[9178.481975],\"scan_median_ms\":9178.481975},{\"mode\":3,\"kernel_samples_ms\":[4668.789551],\"kernel_median_ms\":4668.789551,\"scan_samples_ms\":[5058.365918],\"scan_median_ms\":5058.365918}],\"hits\":[[61692151016558,51069,419124]]}\n",
"stderr": ""
}
}
]
}

1840
show/results/latest.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,232 @@
{
"date": "2026-10-08T01:12:22.661151+08:00",
"repeats": 5,
"note": "此前测量中保留下来的 02/03 样本;其他实验分支已删除。这些不是删除分支后的重新测量。",
"cases": [
{
"case": {
"name": "A:小地图、多 seed",
"seed": 61692151016558,
"seeds": 65536,
"x0": 51053,
"z0": 419108,
"width": 33,
"height": 33,
"n": 5
},
"gpu": "NVIDIA GeForce RTX 3060 Laptop GPU",
"rows": [
{
"mode": 2,
"kernel_samples_ms": [
1.719295979,
1.72339201,
1.474560022,
1.470463991,
1.47558403
],
"kernel_median_ms": 1.47558403,
"scan_samples_ms": [
2.067108,
2.053242,
1.774716,
1.760308,
1.777425
],
"scan_median_ms": 1.777425
},
{
"mode": 3,
"kernel_samples_ms": [
25.17913628,
27.14112091,
23.79264069,
21.79686356,
21.46816063
],
"kernel_median_ms": 23.79264069,
"scan_samples_ms": [
27.637457,
29.684524,
26.08049,
23.880349,
23.558274
],
"scan_median_ms": 26.08049
}
],
"hits": [
[
61692151016558,
51069,
419124
]
],
"comparison_equal": true,
"verification": {
"cpu_case": {
"name": "A:小地图、多 seed",
"seed": 61692151016558,
"seeds": 65536,
"x0": 51053,
"z0": 419108,
"width": 33,
"height": 33,
"n": 5
},
"cpu_window_equal": true,
"all_gpu_hits_exact": true
}
},
{
"case": {
"name": "B:大地图、多 seed",
"seed": 61692151016558,
"seeds": 65536,
"x0": 50557,
"z0": 418612,
"width": 1025,
"height": 1025,
"n": 5
},
"gpu": "NVIDIA GeForce RTX 3060 Laptop GPU",
"rows": [
{
"mode": 2,
"kernel_samples_ms": [
1587.146729,
1605.599243,
1620.303833,
1643.439087,
1693.452271
],
"kernel_median_ms": 1620.303833,
"scan_samples_ms": [
1720.192965,
1739.794472,
1755.845789,
1780.816188,
1835.03301
],
"scan_median_ms": 1755.845789
},
{
"mode": 3,
"kernel_samples_ms": [
1505.31897,
1512.71521,
1520.886841,
1538.606079,
1601.283081
],
"kernel_median_ms": 1520.886841,
"scan_samples_ms": [
1631.207428,
1639.15866,
1648.096918,
1667.257617,
1735.300914
],
"scan_median_ms": 1648.096918
}
],
"hits": [
[
61692151016558,
51069,
419124
]
],
"comparison_equal": true,
"verification": {
"cpu_case": {
"name": "B:大地图、多 seed",
"seed": 61692151016558,
"seeds": 65536,
"x0": 51053,
"z0": 419108,
"width": 33,
"height": 33,
"n": 5
},
"cpu_window_equal": true,
"all_gpu_hits_exact": true
}
},
{
"case": {
"name": "D:同一大地图、少 seed",
"seed": 61692151016558,
"seeds": 256,
"x0": 50557,
"z0": 418612,
"width": 1025,
"height": 1025,
"n": 5
},
"gpu": "NVIDIA GeForce RTX 3060 Laptop GPU",
"rows": [
{
"mode": 2,
"kernel_samples_ms": [
354.4719238,
357.1487122,
355.1600647,
355.5502625,
352.5959778
],
"kernel_median_ms": 355.1600647,
"scan_samples_ms": [
384.401641,
387.353291,
385.320633,
385.6751,
382.439508
],
"scan_median_ms": 385.320633
},
{
"mode": 3,
"kernel_samples_ms": [
6.311935902,
5.736447811,
5.041152,
5.049344063,
5.649407864
],
"kernel_median_ms": 5.649407864,
"scan_samples_ms": [
7.155417,
6.451358,
5.790092,
5.803958,
6.47205
],
"scan_median_ms": 6.451358
}
],
"hits": [
[
61692151016558,
51069,
419124
]
],
"comparison_equal": true,
"verification": {
"cpu_case": {
"name": "D:同一大地图、少 seed",
"seed": 61692151016558,
"seeds": 256,
"x0": 51053,
"z0": 419108,
"width": 33,
"height": 33,
"n": 5
},
"cpu_window_equal": true,
"all_gpu_hits_exact": true
}
}
]
}

View file

@ -0,0 +1,704 @@
{
"metadata": {
"date": "2026-10-07T22:59:59.732843+08:00",
"cpu": "AMD Ryzen 7 5800H with Radeon Graphics",
"platform": "Linux-6.18.40.1-microsoft-standard-WSL2-x86_64-with-glibc2.39",
"repeats": 5,
"gpu": {
"command": [
"nvidia-smi",
"--query-gpu=name,driver_version,temperature.gpu,power.draw,utilization.gpu",
"--format=csv"
],
"process_ms": 78.16004200140014,
"stdout": "name, driver_version, temperature.gpu, power.draw [W], utilization.gpu [%]\nNVIDIA GeForce RTX 3060 Laptop GPU, 596.49, 63, 27.45 W, 21 %\n",
"stderr": ""
},
"nvcc": {
"command": [
"nvcc",
"--version"
],
"process_ms": 3.6383759579621255,
"stdout": "nvcc: NVIDIA (R) Cuda compiler driver\nCopyright (c) 2005-2023 NVIDIA Corporation\nBuilt on Fri_Jan__6_16:45:21_PST_2023\nCuda compilation tools, release 12.0, V12.0.140\nBuild cuda_12.0.r12.0/compiler.32267302_0\n",
"stderr": ""
},
"gxx": {
"command": [
"g++",
"--version"
],
"process_ms": 1.5936919953674078,
"stdout": "g++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0\nCopyright (C) 2023 Free Software Foundation, Inc.\nThis is free software; see the source for copying conditions. There is NO\nwarranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n\n",
"stderr": ""
},
"source_sha256": {
"01_cpu.cpp": "b7fc5d25b32050bd3defdb3934e6bb4f1dd8b5d83d529bef8b8a90d719d17e8b",
"08_seed_cpu.cpp": "b9722cc52e46eb720fb78ea1d3f0531249cf656d274771fd8c2bad9f06f27800",
"07_residue.cu": "873bea97faec42ce87620ea8b6bea3ffdb329ba2cf7924ab2d11719a91347b38",
"08_seed_cuda.cu": "e4e95df388e572c21fed0e87b81299265ed66221f7d37b90d5c8feb7839a1ed7",
"08_upper_compare.cu": "f20dc272ebc821230c83d22ca5c9819e8b658b1b994025b2c31b1928f067335f",
"cuda_steps.cu": "99a224471d68bf5f1404c4c391eda054f50757194589d9ddedccded652f14f65",
"02_cuda_seed.cuh": "1f6a8bf1d780f569c1a685c67edd8242db9d658fb919247b0197c3b450854455",
"03_cuda_position.cuh": "de7adcf92ddc0354b809eb65a177099f6f512f2fa00d28ba9e92c28a276d7fa3",
"04_cuda_outer_sum.cuh": "6ef956bc59750bd4a900b1b38aedf7b188d869f3c51d13559b771529daec8255",
"common.hpp": "50cb3915578826b7ea4edb2ceb79c5253dea53ee2365d7c3d393aab625d236d0",
"../research/slime_solver_core.hpp": "feb1200518716b4d796fb60f60a83b6b798155bbd4e795531793181a5c7f9eba"
}
},
"mapping": [
{
"case": {
"name": "少 seed/大地图",
"seed": 61692151016558,
"seeds": 64,
"x0": 50557,
"z0": 418612,
"width": 1025,
"height": 1025,
"modes": "all"
},
"cpu": {
"seed_start": 61692151016558,
"seeds": 64,
"x0": 50557,
"z0": 418612,
"width": 1025,
"height": 1025,
"n": 5,
"candidate_count": 67240000,
"mode": "01_cpu",
"cpu_threads": 16,
"samples_ms": [
68.250787,
65.500419,
70.745813,
66.927279,
68.617713
],
"median_ms": 68.250787,
"hits": [
[
61692151016558,
51069,
419124
]
]
},
"gpu": {
"seed_start": 61692151016558,
"seeds": 64,
"x0": 50557,
"z0": 418612,
"width": 1025,
"height": 1025,
"n": 5,
"candidate_count": 67240000,
"gpu": "NVIDIA GeForce RTX 3060 Laptop GPU",
"setup_ms": 245.809413,
"comparison_equal": true,
"rows": [
{
"mode": 2,
"kernel_samples_ms": [
315.2680969,
314.944519,
314.2410278,
315.0305176,
330.1017456
],
"kernel_median_ms": 315.0305176,
"scan_samples_ms": [
341.894404,
341.50397,
340.940854,
341.613496,
358.166944
],
"scan_median_ms": 341.613496
},
{
"mode": 3,
"kernel_samples_ms": [
1.258496046,
1.264639974,
1.737728,
1.259520054,
3.163136005
],
"kernel_median_ms": 1.264639974,
"scan_samples_ms": [
1.557401,
1.578742,
2.176959,
1.588709,
3.73826
],
"scan_median_ms": 1.588709
},
{
"mode": 31,
"kernel_samples_ms": [
1.898496032,
2.692095995,
1.894400001,
1.893375993,
1.894400001
],
"kernel_median_ms": 1.894400001,
"scan_samples_ms": [
2.214659,
3.094326,
2.320609,
2.252793,
2.351051
],
"scan_median_ms": 2.320609
},
{
"mode": 4,
"kernel_samples_ms": [
1.445888042,
1.441792011,
1.444864035,
1.696768045,
1.445888042
],
"kernel_median_ms": 1.445888042,
"scan_samples_ms": [
1.714485,
1.733875,
1.761284,
2.12366,
1.765184
],
"scan_median_ms": 1.761284
},
{
"mode": 5,
"kernel_samples_ms": [
1.888255954,
1.883136034,
1.891327977,
1.888255954,
2.775039911
],
"kernel_median_ms": 1.888255954,
"scan_samples_ms": [
2.186709,
2.202851,
2.223868,
2.227226,
3.193885
],
"scan_median_ms": 2.223868
},
{
"mode": 6,
"kernel_samples_ms": [
3.779583931,
3.779583931,
4.055039883,
3.786751986,
4.895775795
],
"kernel_median_ms": 3.786751986,
"scan_samples_ms": [
4.419027,
4.305927,
4.690511,
4.285452,
5.657277
],
"scan_median_ms": 4.419027
}
],
"hits": [
[
61692151016558,
51069,
419124
]
]
},
"raw": [
{
"command": [
"/mnt/d/dev/slime/show/bin/01_cpu",
"--seed",
"61692151016558",
"--seeds",
"64",
"--x0",
"50557",
"--z0",
"418612",
"--width",
"1025",
"--height",
"1025",
"-n",
"5",
"--repeats",
"5",
"--cpu-threads",
"16"
],
"process_ms": 411.2323169829324,
"stdout": "{\"seed_start\":61692151016558,\"seeds\":64,\"x0\":50557,\"z0\":418612,\"width\":1025,\"height\":1025,\"n\":5,\"candidate_count\":67240000,\"mode\":\"01_cpu\",\"cpu_threads\":16,\"samples_ms\":[68.250787,65.500419,70.745813,66.927279,68.617713],\"median_ms\":68.250787,\"hits\":[[61692151016558,51069,419124]]}\n",
"stderr": ""
},
{
"command": [
"/mnt/d/dev/slime/show/bin/cuda_steps",
"--seed",
"61692151016558",
"--seeds",
"64",
"--x0",
"50557",
"--z0",
"418612",
"--width",
"1025",
"--height",
"1025",
"-n",
"5",
"--repeats",
"5",
"--modes",
"all"
],
"process_ms": 2509.6489019924775,
"stdout": "{\"seed_start\":61692151016558,\"seeds\":64,\"x0\":50557,\"z0\":418612,\"width\":1025,\"height\":1025,\"n\":5,\"candidate_count\":67240000,\"gpu\":\"NVIDIA GeForce RTX 3060 Laptop GPU\",\"setup_ms\":245.809413,\"comparison_equal\":true,\"rows\":[{\"mode\":2,\"kernel_samples_ms\":[315.2680969,314.944519,314.2410278,315.0305176,330.1017456],\"kernel_median_ms\":315.0305176,\"scan_samples_ms\":[341.894404,341.50397,340.940854,341.613496,358.166944],\"scan_median_ms\":341.613496},{\"mode\":3,\"kernel_samples_ms\":[1.258496046,1.264639974,1.737728,1.259520054,3.163136005],\"kernel_median_ms\":1.264639974,\"scan_samples_ms\":[1.557401,1.578742,2.176959,1.588709,3.73826],\"scan_median_ms\":1.588709},{\"mode\":31,\"kernel_samples_ms\":[1.898496032,2.692095995,1.894400001,1.893375993,1.894400001],\"kernel_median_ms\":1.894400001,\"scan_samples_ms\":[2.214659,3.094326,2.320609,2.252793,2.351051],\"scan_median_ms\":2.320609},{\"mode\":4,\"kernel_samples_ms\":[1.445888042,1.441792011,1.444864035,1.696768045,1.445888042],\"kernel_median_ms\":1.445888042,\"scan_samples_ms\":[1.714485,1.733875,1.761284,2.12366,1.765184],\"scan_median_ms\":1.761284},{\"mode\":5,\"kernel_samples_ms\":[1.888255954,1.883136034,1.891327977,1.888255954,2.775039911],\"kernel_median_ms\":1.888255954,\"scan_samples_ms\":[2.186709,2.202851,2.223868,2.227226,3.193885],\"scan_median_ms\":2.223868},{\"mode\":6,\"kernel_samples_ms\":[3.779583931,3.779583931,4.055039883,3.786751986,4.895775795],\"kernel_median_ms\":3.786751986,\"scan_samples_ms\":[4.419027,4.305927,4.690511,4.285452,5.657277],\"scan_median_ms\":4.419027}],\"hits\":[[61692151016558,51069,419124]]}\n",
"stderr": ""
}
],
"equality": true
},
{
"case": {
"name": "多 seed/小地图",
"seed": 61692151016558,
"seeds": 65536,
"x0": 51053,
"z0": 419108,
"width": 33,
"height": 33,
"modes": "all"
},
"cpu": {
"seed_start": 61692151016558,
"seeds": 65536,
"x0": 51053,
"z0": 419108,
"width": 33,
"height": 33,
"n": 5,
"candidate_count": 71368704,
"mode": "01_cpu",
"cpu_threads": 16,
"samples_ms": [
81.224901,
73.117448,
97.834792,
108.934088,
90.93493
],
"median_ms": 90.93493,
"hits": [
[
61692151016558,
51069,
419124
]
]
},
"gpu": {
"seed_start": 61692151016558,
"seeds": 65536,
"x0": 51053,
"z0": 419108,
"width": 33,
"height": 33,
"n": 5,
"candidate_count": 71368704,
"gpu": "NVIDIA GeForce RTX 3060 Laptop GPU",
"setup_ms": 240.589803,
"comparison_equal": true,
"rows": [
{
"mode": 2,
"kernel_samples_ms": [
2.157567978,
1.483775973,
1.488896012,
1.487872005,
1.484799981
],
"kernel_median_ms": 1.487872005,
"scan_samples_ms": [
2.543776,
1.868209,
1.8044,
1.923351,
1.781325
],
"scan_median_ms": 1.868209
},
{
"mode": 3,
"kernel_samples_ms": [
21.31967926,
22.01600075,
21.36166382,
21.35961533,
21.49785614
],
"kernel_median_ms": 21.36166382,
"scan_samples_ms": [
23.495235,
24.324635,
23.509535,
23.510402,
23.78871
],
"scan_median_ms": 23.510402
},
{
"mode": 31,
"kernel_samples_ms": [
2.029567957,
2.024447918,
2.024447918,
2.027519941,
2.030591965
],
"kernel_median_ms": 2.027519941,
"scan_samples_ms": [
2.534134,
2.450717,
2.456026,
2.422443,
2.575627
],
"scan_median_ms": 2.456026
},
{
"mode": 4,
"kernel_samples_ms": [
2.405375957,
2.40435195,
2.40435195,
2.40435195,
2.878463984
],
"kernel_median_ms": 2.40435195,
"scan_samples_ms": [
2.824468,
2.812334,
2.821326,
2.880801,
3.663077
],
"scan_median_ms": 2.824468
},
{
"mode": 5,
"kernel_samples_ms": [
2.010112047,
2.227231979,
2.014208078,
2.014208078,
2.885632038
],
"kernel_median_ms": 2.014208078,
"scan_samples_ms": [
2.359826,
2.74571,
2.370009,
2.579851,
3.366243
],
"scan_median_ms": 2.579851
},
{
"mode": 6,
"kernel_samples_ms": [
6.228991985,
5.210175991,
6.320127964,
4.903935909,
4.611072063
],
"kernel_median_ms": 5.210175991,
"scan_samples_ms": [
6.99162,
5.999286,
7.126278,
5.851953,
5.353186
],
"scan_median_ms": 5.999286
}
],
"hits": [
[
61692151016558,
51069,
419124
]
]
},
"raw": [
{
"command": [
"/mnt/d/dev/slime/show/bin/01_cpu",
"--seed",
"61692151016558",
"--seeds",
"65536",
"--x0",
"51053",
"--z0",
"419108",
"--width",
"33",
"--height",
"33",
"-n",
"5",
"--repeats",
"5",
"--cpu-threads",
"16"
],
"process_ms": 541.5888970019296,
"stdout": "{\"seed_start\":61692151016558,\"seeds\":65536,\"x0\":51053,\"z0\":419108,\"width\":33,\"height\":33,\"n\":5,\"candidate_count\":71368704,\"mode\":\"01_cpu\",\"cpu_threads\":16,\"samples_ms\":[81.224901,73.117448,97.834792,108.934088,90.93493],\"median_ms\":90.93493,\"hits\":[[61692151016558,51069,419124]]}\n",
"stderr": ""
},
{
"command": [
"/mnt/d/dev/slime/show/bin/cuda_steps",
"--seed",
"61692151016558",
"--seeds",
"65536",
"--x0",
"51053",
"--z0",
"419108",
"--width",
"33",
"--height",
"33",
"-n",
"5",
"--repeats",
"5",
"--modes",
"all"
],
"process_ms": 542.0670810271986,
"stdout": "{\"seed_start\":61692151016558,\"seeds\":65536,\"x0\":51053,\"z0\":419108,\"width\":33,\"height\":33,\"n\":5,\"candidate_count\":71368704,\"gpu\":\"NVIDIA GeForce RTX 3060 Laptop GPU\",\"setup_ms\":240.589803,\"comparison_equal\":true,\"rows\":[{\"mode\":2,\"kernel_samples_ms\":[2.157567978,1.483775973,1.488896012,1.487872005,1.484799981],\"kernel_median_ms\":1.487872005,\"scan_samples_ms\":[2.543776,1.868209,1.8044,1.923351,1.781325],\"scan_median_ms\":1.868209},{\"mode\":3,\"kernel_samples_ms\":[21.31967926,22.01600075,21.36166382,21.35961533,21.49785614],\"kernel_median_ms\":21.36166382,\"scan_samples_ms\":[23.495235,24.324635,23.509535,23.510402,23.78871],\"scan_median_ms\":23.510402},{\"mode\":31,\"kernel_samples_ms\":[2.029567957,2.024447918,2.024447918,2.027519941,2.030591965],\"kernel_median_ms\":2.027519941,\"scan_samples_ms\":[2.534134,2.450717,2.456026,2.422443,2.575627],\"scan_median_ms\":2.456026},{\"mode\":4,\"kernel_samples_ms\":[2.405375957,2.40435195,2.40435195,2.40435195,2.878463984],\"kernel_median_ms\":2.40435195,\"scan_samples_ms\":[2.824468,2.812334,2.821326,2.880801,3.663077],\"scan_median_ms\":2.824468},{\"mode\":5,\"kernel_samples_ms\":[2.010112047,2.227231979,2.014208078,2.014208078,2.885632038],\"kernel_median_ms\":2.014208078,\"scan_samples_ms\":[2.359826,2.74571,2.370009,2.579851,3.366243],\"scan_median_ms\":2.579851},{\"mode\":6,\"kernel_samples_ms\":[6.228991985,5.210175991,6.320127964,4.903935909,4.611072063],\"kernel_median_ms\":5.210175991,\"scan_samples_ms\":[6.99162,5.999286,7.126278,5.851953,5.353186],\"scan_median_ms\":5.999286}],\"hits\":[[61692151016558,51069,419124]]}\n",
"stderr": ""
}
],
"equality": true
},
{
"case": {
"name": "固定 seed/大矩形",
"seed": 61692151016558,
"seeds": 1,
"x0": 46973,
"z0": 415028,
"width": 8193,
"height": 8193,
"modes": "31,4,5,6"
},
"cpu": {
"seed_start": 61692151016558,
"seeds": 1,
"x0": 46973,
"z0": 415028,
"width": 8193,
"height": 8193,
"n": 5,
"candidate_count": 67125249,
"mode": "01_cpu",
"cpu_threads": 16,
"samples_ms": [
611.078369,
917.563899,
433.067884,
427.50984,
419.277371
],
"median_ms": 433.067884,
"hits": [
[
61692151016558,
51069,
419124
]
]
},
"gpu": {
"seed_start": 61692151016558,
"seeds": 1,
"x0": 46973,
"z0": 415028,
"width": 8193,
"height": 8193,
"n": 5,
"candidate_count": 67125249,
"gpu": "NVIDIA GeForce RTX 3060 Laptop GPU",
"setup_ms": 224.834879,
"comparison_equal": true,
"rows": [
{
"mode": 31,
"kernel_samples_ms": [
2.184191942,
2.183167934,
2.189311981,
2.189311981,
2.188287973
],
"kernel_median_ms": 2.188287973,
"scan_samples_ms": [
2.579201,
2.564684,
2.605201,
2.653518,
2.550926
],
"scan_median_ms": 2.579201
},
{
"mode": 4,
"kernel_samples_ms": [
1.633280039,
1.633280039,
1.629184008,
2.458623886,
1.633280039
],
"kernel_median_ms": 1.633280039,
"scan_samples_ms": [
1.936459,
1.978817,
1.943284,
2.986426,
1.947401
],
"scan_median_ms": 1.947401
},
{
"mode": 5,
"kernel_samples_ms": [
2.425856113,
2.171904087,
2.168832064,
2.169856071,
2.176000118
],
"kernel_median_ms": 2.171904087,
"scan_samples_ms": [
2.821001,
2.525468,
2.639868,
2.538793,
2.597401
],
"scan_median_ms": 2.597401
},
{
"mode": 6,
"kernel_samples_ms": [
5.011456013,
4.367360115,
5.564415932,
4.3704319,
4.3704319
],
"kernel_median_ms": 4.3704319,
"scan_samples_ms": [
5.73907,
4.964918,
6.278786,
5.042594,
4.929169
],
"scan_median_ms": 5.042594
}
],
"hits": [
[
61692151016558,
51069,
419124
]
]
},
"raw": [
{
"command": [
"/mnt/d/dev/slime/show/bin/01_cpu",
"--seed",
"61692151016558",
"--seeds",
"1",
"--x0",
"46973",
"--z0",
"415028",
"--width",
"8193",
"--height",
"8193",
"-n",
"5",
"--repeats",
"5",
"--cpu-threads",
"16"
],
"process_ms": 3234.2790690017864,
"stdout": "{\"seed_start\":61692151016558,\"seeds\":1,\"x0\":46973,\"z0\":415028,\"width\":8193,\"height\":8193,\"n\":5,\"candidate_count\":67125249,\"mode\":\"01_cpu\",\"cpu_threads\":16,\"samples_ms\":[611.078369,917.563899,433.067884,427.50984,419.277371],\"median_ms\":433.067884,\"hits\":[[61692151016558,51069,419124]]}\n",
"stderr": ""
},
{
"command": [
"/mnt/d/dev/slime/show/bin/cuda_steps",
"--seed",
"61692151016558",
"--seeds",
"1",
"--x0",
"46973",
"--z0",
"415028",
"--width",
"8193",
"--height",
"8193",
"-n",
"5",
"--repeats",
"5",
"--modes",
"31,4,5,6"
],
"process_ms": 360.2725120144896,
"stdout": "{\"seed_start\":61692151016558,\"seeds\":1,\"x0\":46973,\"z0\":415028,\"width\":8193,\"height\":8193,\"n\":5,\"candidate_count\":67125249,\"gpu\":\"NVIDIA GeForce RTX 3060 Laptop GPU\",\"setup_ms\":224.834879,\"comparison_equal\":true,\"rows\":[{\"mode\":31,\"kernel_samples_ms\":[2.184191942,2.183167934,2.189311981,2.189311981,2.188287973],\"kernel_median_ms\":2.188287973,\"scan_samples_ms\":[2.579201,2.564684,2.605201,2.653518,2.550926],\"scan_median_ms\":2.579201},{\"mode\":4,\"kernel_samples_ms\":[1.633280039,1.633280039,1.629184008,2.458623886,1.633280039],\"kernel_median_ms\":1.633280039,\"scan_samples_ms\":[1.936459,1.978817,1.943284,2.986426,1.947401],\"scan_median_ms\":1.947401},{\"mode\":5,\"kernel_samples_ms\":[2.425856113,2.171904087,2.168832064,2.169856071,2.176000118],\"kernel_median_ms\":2.171904087,\"scan_samples_ms\":[2.821001,2.525468,2.639868,2.538793,2.597401],\"scan_median_ms\":2.597401},{\"mode\":6,\"kernel_samples_ms\":[5.011456013,4.367360115,5.564415932,4.3704319,4.3704319],\"kernel_median_ms\":4.3704319,\"scan_samples_ms\":[5.73907,4.964918,6.278786,5.042594,4.929169],\"scan_median_ms\":5.042594}],\"hits\":[[61692151016558,51069,419124]]}\n",
"stderr": ""
}
],
"equality": true
}
],
"note": "Original imbalance experiments; diagnostics only, not the main adjacent-version comparison."
}

View file

@ -0,0 +1,13 @@
测量线程映射:A:CPU → 按 seed 的 CUDA,65536 seed × 257×257
CPU 对照完成;开始 CUDA 预热与计时
完成该组完整命中对照
测量线程映射:B:大坐标范围的固定坐标策略,65536 seed × 4097×4097
CPU 对照完成;开始 CUDA 预热与计时
完成该组完整命中对照
测量线程映射:C:固定 seed 的坐标扫描与后续优化,1 seed × 8193×8193
CPU 对照完成;开始 CUDA 预热与计时
完成该组完整命中对照
坐标范围趋势:65536 seed × 513×513
坐标范围趋势:65536 seed × 1025×1025
坐标范围趋势:65536 seed × 2049×2049
完成分段改测:show/BENCHMARK.md 和 show/results/latest.json

View file

@ -0,0 +1,175 @@
{
"date": "2026-10-08T23:09:51.361807+08:00",
"case": {
"seed": 61692151016558,
"seeds": 65536,
"x0": 50557,
"z0": 418612,
"width": 1025,
"height": 1025,
"n": 5
},
"comparison_equal": true,
"before_source_sha256": {
"01_cpu.cpp": "b7fc5d25b32050bd3defdb3934e6bb4f1dd8b5d83d529bef8b8a90d719d17e8b",
"08_seed_cpu.cpp": "b9722cc52e46eb720fb78ea1d3f0531249cf656d274771fd8c2bad9f06f27800",
"07_residue.cu": "873bea97faec42ce87620ea8b6bea3ffdb329ba2cf7924ab2d11719a91347b38",
"08_seed_cuda.cu": "e4e95df388e572c21fed0e87b81299265ed66221f7d37b90d5c8feb7839a1ed7",
"08_upper_compare.cu": "f20dc272ebc821230c83d22ca5c9819e8b658b1b994025b2c31b1928f067335f",
"cuda_steps.cu": "b81d7c85f96c1e1f36fa6cb6907e17f62955606821e86959b038095e10f4957a",
"02_cuda_seed.cuh": "1f6a8bf1d780f569c1a685c67edd8242db9d658fb919247b0197c3b450854455",
"03_cuda_position.cuh": "69ceeb76a9954b073826a613fe9b343bc3cbb106f47546b3af2bd00df589e34c",
"04_cuda_outer_sum.cuh": "a316d3a4845351be996bee4930d817aa9d37edec05cbcf8e1869c086533a477d",
"common.hpp": "686d1964fe2fd5cb17709d9c5de7cbfbf0530a7a5622604a138a65194f100a13",
"../research/slime_solver_core.hpp": "feb1200518716b4d796fb60f60a83b6b798155bbd4e795531793181a5c7f9eba"
},
"after_source_sha256": {
"03_cuda_position.cuh": "44f49a7db73e52ec87ac632a2cee2f7938a2241a356780e151b3ab0de5d9a506",
"cuda_steps.cu": "a5c639dafcdf581c35f4551195cf28e5ec82eac9b642631a6845fbfc6fa48cfb"
},
"note": "Both versions use fixed-coordinate threads; new version precomputes private axis terms and uses compile-time pattern sizes. Each version has one full warmup and five samples. Versions run sequentially, unlike the rotating main 02/03 comparison.",
"results": [
{
"label": "before",
"binary_sha256": "95f417172b7de49271edc1dd20c791fe2834cdcbb0ad7367204e0431dd29a896",
"raw": {
"command": [
"/tmp/slime_show_before_stage3",
"--seed",
"61692151016558",
"--seeds",
"65536",
"--x0",
"50557",
"--z0",
"418612",
"--width",
"1025",
"--height",
"1025",
"-n",
"5",
"--repeats",
"5",
"--modes",
"3"
],
"process_ms": 10323.46587500001,
"stdout": "{\"seed_start\":61692151016558,\"seeds\":65536,\"x0\":50557,\"z0\":418612,\"width\":1025,\"height\":1025,\"n\":5,\"candidate_count\":68853760000,\"gpu\":\"NVIDIA GeForce RTX 3060 Laptop GPU\",\"setup_ms\":227.442475,\"comparison_equal\":true,\"rows\":[{\"mode\":3,\"kernel_samples_ms\":[1532.477417,1534.586914,1555.032104,1549.421509,1537.888306],\"kernel_median_ms\":1537.888306,\"scan_samples_ms\":[1660.816641,1662.997808,1685.21925,1678.91165,1666.479642],\"scan_median_ms\":1666.479642}],\"hits\":[[61692151016558,51069,419124]]}\n",
"stderr": ""
},
"data": {
"seed_start": 61692151016558,
"seeds": 65536,
"x0": 50557,
"z0": 418612,
"width": 1025,
"height": 1025,
"n": 5,
"candidate_count": 68853760000,
"gpu": "NVIDIA GeForce RTX 3060 Laptop GPU",
"setup_ms": 227.442475,
"comparison_equal": true,
"rows": [
{
"mode": 3,
"kernel_samples_ms": [
1532.477417,
1534.586914,
1555.032104,
1549.421509,
1537.888306
],
"kernel_median_ms": 1537.888306,
"scan_samples_ms": [
1660.816641,
1662.997808,
1685.21925,
1678.91165,
1666.479642
],
"scan_median_ms": 1666.479642
}
],
"hits": [
[
61692151016558,
51069,
419124
]
]
}
},
{
"label": "after",
"binary_sha256": "8668056a28c672a2fc12c1f2046a4be3d72201c8da17d2de30c27462372e64ce",
"raw": {
"command": [
"/mnt/d/dev/slime/show/bin/cuda_steps",
"--seed",
"61692151016558",
"--seeds",
"65536",
"--x0",
"50557",
"--z0",
"418612",
"--width",
"1025",
"--height",
"1025",
"-n",
"5",
"--repeats",
"5",
"--modes",
"3"
],
"process_ms": 6253.339395000012,
"stdout": "{\"seed_start\":61692151016558,\"seeds\":65536,\"x0\":50557,\"z0\":418612,\"width\":1025,\"height\":1025,\"n\":5,\"candidate_count\":68853760000,\"gpu\":\"NVIDIA GeForce RTX 3060 Laptop GPU\",\"setup_ms\":254.046325,\"comparison_equal\":true,\"rows\":[{\"mode\":3,\"kernel_samples_ms\":[912.5765381,920.5626831,915.3710327,905.3746948,917.3391113],\"kernel_median_ms\":915.3710327,\"scan_samples_ms\":[989.005658,997.631591,992.011583,981.183558,994.174567],\"scan_median_ms\":992.011583}],\"hits\":[[61692151016558,51069,419124]]}\n",
"stderr": ""
},
"data": {
"seed_start": 61692151016558,
"seeds": 65536,
"x0": 50557,
"z0": 418612,
"width": 1025,
"height": 1025,
"n": 5,
"candidate_count": 68853760000,
"gpu": "NVIDIA GeForce RTX 3060 Laptop GPU",
"setup_ms": 254.046325,
"comparison_equal": true,
"rows": [
{
"mode": 3,
"kernel_samples_ms": [
912.5765381,
920.5626831,
915.3710327,
905.3746948,
917.3391113
],
"kernel_median_ms": 915.3710327,
"scan_samples_ms": [
989.005658,
997.631591,
992.011583,
981.183558,
994.174567
],
"scan_median_ms": 992.011583
}
],
"hits": [
[
61692151016558,
51069,
419124
]
]
}
}
]
}

19
show/results/tests.txt Normal file
View file

@ -0,0 +1,19 @@
历史 GPU 验证日志;不表示此次清理重新运行 GPU 测试。
test_all_pattern_sizes (__main__.Steps.test_all_pattern_sizes) ... ok
test_complete_fixed_position_pipeline (__main__.Steps.test_complete_fixed_position_pipeline) ... ok
test_high16_equivalence_and_seed48_wrap (__main__.Steps.test_high16_equivalence_and_seed48_wrap) ... ok
test_int32_edges (__main__.Steps.test_int32_edges) ... ok
test_invalid_ranges_and_truncation (__main__.Steps.test_invalid_ranges_and_truncation) ... ok
test_known_hit_and_partial_tiles (__main__.Steps.test_known_hit_and_partial_tiles) ... ok
test_negative_and_overflow_coordinates (__main__.Steps.test_negative_and_overflow_coordinates) ... ok
test_nonzero_seed_and_nonblock_dimensions (__main__.Steps.test_nonzero_seed_and_nonblock_dimensions) ... ok
test_outer_tall_rectangle_fallback (__main__.Steps.test_outer_tall_rectangle_fallback) ... ok
test_rejection_tail (__main__.Steps.test_rejection_tail) ... ok
test_residue_expansion_against_direct_window (__main__.Steps.test_residue_expansion_against_direct_window) ... ok
test_upper13_against_enumeration (__main__.Steps.test_upper13_against_enumeration) ... ok
----------------------------------------------------------------------
Historical suite completed in 10.792s; retired X/Z test entry removed on 2026-10-11.
OK

181
show/test_steps.py Normal file
View file

@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""Compare complete tuples to an independent Python Java-arithmetic oracle."""
import argparse
import json
from pathlib import Path
import subprocess
import unittest
ROOT = Path(__file__).resolve().parent
GPU = False
A = 0x5DEECE66D
MASK = (1 << 48) - 1
XOR = 987234911
def i32(v):
v &= (1 << 32) - 1
return v - (1 << 32) if v >> 31 else v
def term(x, z):
return i32(x*x*4987142) + i32(x*5947611) + i32(z*z)*4392871 + i32(z*389711)
def first(seed, x, z):
state = ((seed + term(x, z)) ^ XOR ^ A) & MASK
return (state*A + 11) & MASK
def slime(seed, x, z):
state = first(seed, x, z)
while True:
q = state >> 17
if q < 2147483640:
return q % 10 == 0
state = (state*A + 11) & MASK
def oracle(seed, seeds, x0, z0, width, height, n):
return [[s, x, z] for s in range(seed, seed+seeds)
for x in range(x0, x0+width) for z in range(z0, z0+height)
if all(slime(s, x+dx, z+dz) for dx in range(n) for dz in range(n))]
def execute(binary, args, json_output=True):
result = subprocess.run([str(ROOT/'bin'/binary), *map(str, args)],
capture_output=True, text=True, timeout=60, check=True)
return json.loads(result.stdout) if json_output else result.stdout
def cli(case):
seed, seeds, x, z, width, height, n = case
return ['--seed', seed, '--seeds', seeds, '--x0', x, '--z0', z,
'--width', width, '--height', height, '-n', n, '--repeats', 1]
class Steps(unittest.TestCase):
def compare(self, case):
expected = oracle(*case)
self.assertEqual(execute('01_cpu', cli(case))['hits'], expected)
if GPU:
result = execute('cuda_steps', cli(case))
self.assertTrue(result['comparison_equal'])
self.assertEqual(result['hits'], expected)
def test_nonzero_seed_and_nonblock_dimensions(self):
self.compare((1337, 7, -23, 17, 19, 21, 1))
def test_negative_and_overflow_coordinates(self):
self.compare((98765, 3, -1875000, 1874995, 17, 19, 2))
def test_int32_edges(self):
self.compare((42, 2, -2147483648, 2147483600, 17, 17, 2))
def test_known_hit_and_partial_tiles(self):
case = (61692151016558, 3, 51050, 419107, 37, 35, 5)
self.compare(case)
self.assertIn([61692151016558, 51069, 419124], oracle(*case))
def test_all_pattern_sizes(self):
for n in range(1, 9):
with self.subTest(n=n):
self.compare((61692151016558, 2, 51069, 419124, 3, 3, n))
def test_high16_equivalence_and_seed48_wrap(self):
base = (61692151016558, 2, 51060, 419115, 17, 19, 5)
high = (base[0]+(34567 << 48), *base[1:])
self.compare(high)
# Compare normalized seed labels, not raw world seed IDs.
normalized = [[s & MASK, x, z] for s, x, z in oracle(*high)]
self.assertEqual(normalized, oracle(*base))
self.compare((MASK-2, 7, -7, 5, 17, 19, 1))
def test_rejection_tail(self):
# Engineer each of Java's eight rejected first outputs and a successful redraw.
inv = pow(A, -1, 1 << 48)
for q in range(2147483640, 2147483648):
for suffix in range(10000):
v = (q << 17) | suffix
u = ((v-11)*inv) & MASK
seed = ((u ^ (XOR ^ A))-term(-31, 29)) & MASK
if slime(seed, -31, 29):
break
else:
self.fail('could not construct successful redraw')
self.assertEqual(first(seed, -31, 29) >> 17, q)
self.compare((seed, 1, -31, 29, 1, 1, 1))
def test_invalid_ranges_and_truncation(self):
invalid = [
['--width', 0], ['--repeats', 0], ['--seeds', 0],
['--seed', (1 << 64)-1, '--seeds', 2],
['--x0', 2147483647, '--width', 2],
['--seed', 0, '--seeds', 5, '-n', 1, '--capacity', 1],
]
for binary in ['01_cpu'] + (['cuda_steps'] if GPU else []):
for args in invalid:
p = subprocess.run([str(ROOT/'bin'/binary), *map(str, args)],
capture_output=True, text=True, timeout=60)
self.assertNotEqual(p.returncode, 0, (binary, args))
def test_upper13_against_enumeration(self):
if not GPU:
self.skipTest('requires --gpu')
result = execute('08_upper_compare', ['--planes', 3, '--repeats', 1])
expected = []
for lower in range(16420645998, 16420645998+3):
for m in range(8192):
seed = lower+(m << 35)
if all((first(seed, 51069+dx, 419124+dz) >> 17) % 10 == 0
for dx in range(5) for dz in range(5)):
self.assertTrue(all(slime(seed, 51069+dx, 419124+dz)
for dx in range(5) for dz in range(5)))
expected.append([lower, seed])
self.assertTrue(result['comparison_equal'])
self.assertEqual(result['solutions'], expected)
self.assertEqual(len(expected), 12)
def test_complete_fixed_position_pipeline(self):
if not GPU:
self.skipTest('requires --gpu')
args = ['--x', 51069, '--z', 419124, '-n', 5, '--max-results', 1000]
cpu = execute('08_seed_cpu', ['search', *args, '-p', 16], False)
gpu = execute('08_seed_cuda', [*args, '--threads', 256], False)
seeds = lambda text: sorted(int(line) for line in text.splitlines() if line.isdigit())
self.assertEqual(seeds(cpu), seeds(gpu))
self.assertEqual(len(seeds(cpu)), 24)
self.assertIn(61692151016558, seeds(cpu))
for s in seeds(cpu):
self.assertTrue(all(slime(s, 51069+dx, 419124+dz)
for dx in range(5) for dz in range(5)))
def test_residue_expansion_against_direct_window(self):
if not GPU:
self.skipTest('requires --gpu')
case = (61692151016558, 1, 51050, 419107, 37, 35, 5)
output = execute('07_residue', ['--seed', case[0], '--x0', case[2],
'--x1', case[2]+case[4]-1, '--z0', case[3],
'--z1', case[3]+case[5]-1, '-n', 5], False)
hits = [[case[0], *map(int, line.split()[1:])] for line in output.splitlines()
if line.startswith('hit ')]
self.assertEqual(hits, oracle(*case))
def test_outer_tall_rectangle_fallback(self):
if not GPU:
self.skipTest('requires --gpu')
# More than 65535 tiles on Z: exercise the safe linear-grid fallback.
args = cli((61692151016558, 1, 51069, 0, 1, 1048561, 5))
cpu = execute('01_cpu', args)
gpu = execute('cuda_steps', [*args, '--modes', '3,4'])
self.assertEqual(cpu['hits'], gpu['hits'])
self.assertIn([61692151016558, 51069, 419124], cpu['hits'])
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--gpu', action='store_true')
args, rest = parser.parse_known_args()
GPU = args.gpu
unittest.main(argv=[__file__, *rest], verbosity=2)

6
show/video/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
.venv/
__pycache__/
media/
output/
.native/
.tools/

View file

@ -0,0 +1 @@
3.12

63
show/video/OUTER_SUM.md Normal file
View file

@ -0,0 +1,63 @@
# Outer sum 为什么成立
这里分离的是送入 Random 之前的坐标项。把有符号 32 位回绕写成
$$
J_{32}(a)=((a+2^{31})\bmod 2^{32})-2^{31}.
$$
按原式中 int 乘法、long 转换的实际位置,坐标项是
$$
T(x,z)=J_{32}(4987142x^2)+J_{32}(5947611x)
+4392871J_{32}(z^2)+J_{32}(389711z).
$$
尤其要注意 Z 的平方在乘 4392871 **之前**就转换成 long;X 的平方与 4987142 相乘则仍在 int 中进行。不能统一改成无限精度的二次多项式,也不能把各个 $J_{32}$ 随意移到整个和的外面。
定义
$$
\begin{aligned}
F(x)&=J_{32}(4987142x^2)+J_{32}(5947611x),\\
G(z)&=4392871J_{32}(z^2)+J_{32}(389711z).
\end{aligned}
$$
前两项只依赖 X,后两项只依赖 Z;没有混合项。只是对原来的加和项分组,便得到精确恒等式
$$T(x,z)=F(x)+G(z).$$
这对应 `common.hpp` 的 `xTerm`、`zTerm` 和 `term`;C++ 实现用无符号运算显式模拟回绕与符号扩展。
选取 $m$ 个 X 坐标、$n$ 个 Z 坐标,记列向量
$$
\mathbf X=(F(x_0),\ldots,F(x_{m-1}))^{\mathsf T},\qquad
\mathbf Z=(G(z_0),\ldots,G(z_{n-1}))^{\mathsf T}.
$$
每个格子的坐标输入是 $T_{ij}=X_i+Z_j$。用全 1 向量表达“复制”,就得到
$$
\mathbf T=\mathbf X\mathbf 1_n^{\mathsf T}
+\mathbf 1_m\mathbf Z^{\mathsf T}.
$$
第一个矩阵的每列都是 X,第二个矩阵的每行都是 Z 的转置。它们形状均为 $m\times n$,逐项相加就是 outer sum。式中的两个复制操作可以各写成一次外积;最终坐标表不是 $\mathbf X\mathbf Z^{\mathsf T}$ 那种相乘的表。
这是数学表示,GPU 不需要真的分配两个复制矩阵或完整的 T 矩阵;实现只保存两条轴,在检查某格时取出对应的两个轴项相加。
例如,X 为 2、5,Z 为 10、20、30:
$$
\begin{pmatrix}2&2&2\\5&5&5\end{pmatrix}
+\begin{pmatrix}10&20&30\\10&20&30\end{pmatrix}
=\begin{pmatrix}12&22&32\\15&25&35\end{pmatrix}.
$$
这些数只用于演示,不是实测坐标项。计算轴函数需要 $m+n$ 次,而不是在每个格子分别重复算 F、G;构造全部格子仍需 $mn$ 次相加,检查全部格子仍需相应的 Random 判断。
实际 16×16 锚点 tile 检查 5×5 图案,需要沿每条轴包含 halo 的 20 个项,共 40 个轴项。40 个轴项只解决预计算,不能说 40 次计算就完成了全部搜索。
最后,`seed + T` 之后还有 XOR、LCG、移位、取模及可能的拒绝重抽。不能把横轴的随机数结果和纵轴的随机数结果直接相加得到二维结果,也不能据此断言最终 slime 布尔图案可分离。

65
show/video/README.md Normal file
View file

@ -0,0 +1,65 @@
# 史莱姆搜索科普视频 · Manim / uv
以 `show/` 的教学实现与实测为基础。中文字幕、深色背景、16:9;绘图使用 Manim Community 的 Cairo 渲染器。
当前版本是带字幕的动画初稿,没有配音或背景音乐;渲染同时输出口播稿和可用于剪辑的 SRT 字幕。
## 环境与运行
需要 Python 3.12、uv,以及 Cairo/Pango 开发库。Ubuntu 常规安装:
```bash
sudo apt install build-essential python3-dev pkg-config libcairo2-dev libpango1.0-dev fonts-noto-cjk texlive-latex-extra dvisvgm
cd show/video
uv sync --locked
uv run python render.py --quality preview
uv run python render.py --quality final
uv run python verify.py --quality final
```
FFmpeg 由 `imageio-ffmpeg` 提供,渲染脚本会自动配置。数学公式使用 `MathTex` / LaTeX,中文说明使用 `Text` / Pango。
公式采用 Computer Modern 数学字体,指数、下标、取整分式与集合符号均由 LaTeX 排版。
渲染优先使用系统的 `latex`、`dvisvgm`;本环境的本地 TeX Live 已解包到 `.native/texlive`,随项目目录保留。
也可通过 `SLIME_TEX_ROOT` 指向相同目录结构的本地 TeX Live,`tex_environment.py` 会配置路径并构建 `latex.fmt`。
换一台机器时,请按上面的系统安装命令补齐工具链。
中文字体优先使用 Microsoft YaHei / Noto Sans CJK SC / WenQuanYi Zen Hei。
WSL 会尝试使用 `/mnt/c/Windows/Fonts/msyh.ttc`,也可设置 `SLIME_FONT_PATH` 为已有中文字体路径。
工程不复制或分发 Windows 字体。
安装方法依据 [Manim 官方安装文档](https://docs.manim.community/en/stable/installation/linux.html);
中文排版采用 [Text / Pango](https://docs.manim.community/en/stable/reference/manim.mobject.text.text_mobject.Text.html)。
`uv.lock` 锁定 Python 依赖,系统开发库不由 uv 管理。
## 场景与输出
- `SlimeFilm`:七个章节的完整动画。
- `ParallelEpisode`:问题、CPU、CUDA、固定坐标、outer sum。
- `RandomEpisode`:Random 性质、高位解析、结尾。
```bash
uv run python render.py --scene ParallelEpisode --quality preview
uv run python render.py --scene RandomEpisode --quality final
```
预览为 854×480 / 15 fps,正式画质为 1920×1080 / 30 fps。
`output/` 中会生成 MP4、SRT、口播稿、章节时间轴、视频规格记录和章节缩略图。
`verify.py` 会逐帧解码视频,并核对分辨率、字幕时间轴和源码/测速数据哈希。
字幕已显示在视频画面中,SRT 另外供剪辑软件使用,导入时无需重复叠加。
`output/`、`.venv/` 与 Manim 中间产物已忽略,不应提交大量渲染缓存。
## 编辑入口与数据口径
`scenes.py` 定义动画、字幕与停留时间。每个 `say()` 同时记录字幕时间轴,修改口播时请在这里修改。
字幕支持中文与 LaTeX 行内公式混排:`say(caption, seconds, display=r"中文 $2^{48}$")`。中文使用 Pango,`$...$` 内的公式使用 `MathTex`;`caption` 保留口播文字,供 SRT 与配音稿使用。时间轴同时保存实际画面用的 `display`,便于核对。
固定坐标与 outer sum 两章包含原始坐标公式分组、Java 回绕语义、广播相加的矩阵恒等式及 2×3 数值示例;完整推导见 [OUTER_SUM.md](OUTER_SUM.md)。
Random 章节逐步推导 XOR 常量的 35 位边界、输出高位平移、模 5 条件与回绕区间,并用数值例子解释 upper13 求解。完整数学与 effective seed 的区别见 [RANDOM_PROPERTIES.md](../RANDOM_PROPERTIES.md)。公式独立核验:`uv run python check_lower35.py`(逐个核对全部 8192 个高位,另核对完整 25 格区间求交)。
`data/benchmarks.json` 是渲染所用的固定数据快照,保留来源文件与 SHA256,避免一次视频渲染混入不同批次数据。
A/B/C 来自 `../results/latest.json`。
- A:65536 seed ×257²,CPU 搜索时间 / GPU scan。
- B:65536 seed ×4097²,只比较相同工作量的 02/03 kernel。
- C:1 seed ×8193²,重新测量 03,再比较 04 kernel。
不同组不连乘加速比。GPU diagram 简化线程数量;outer sum 图示为 5×5,实际 block 为 16×16。
Random 的高 16 位等价性质覆盖重抽路径,但后续首次输出候选模型仍可能漏掉依赖拒绝重抽的解。
独立筛轴章节已删除,历史结果见 [试错报告](../../research/XZ_FILTER_TRIAL_REPORT.md)。含旧章节的渲染视频、字幕与口播稿已删除;需用当前源码重新渲染。
最终视频须在配音、节奏审核后再作为发布版本。

14
show/video/STORYBOARD.md Normal file
View file

@ -0,0 +1,14 @@
# 分镜
| 章节 | 画面 | 讲解目的 | 对应材料 |
| --- | --- | --- | --- |
| 00 问题开场 | 网格中点亮 5×5 区域 | 明确 seed、坐标、图案三个维度 | POPULAR_SCIENCE_OUTLINE.md |
| 01 CPU | 逐格检查与失败退出,配合循环结构 | 建立正确基线;CPU 已使用多线程 | 01_cpu.cpp |
| 02 CUDA | seed 分给线程,线程各扫坐标图 | 解释独立任务并行;A 组图表 | 02_cuda_seed.cuh |
| 03 固定坐标 | 从原式按 X/Z 分组,标记 Java 回绕,再对照两种循环 | 证明 T=F+G;seed 循环外复用轴项;B 组图表 | 03_cuda_position.cuh、common.hpp |
| 04 outer sum | 列向量/行向量广播的矩阵恒等式、2×3 数值例子,再点亮行列 | 解释 outer sum 的数学依据与计算量边界;halo;C 组图表 | 04_cuda_outer_sum.cuh、OUTER_SUM.md |
| 05 Random | 64 位拆成 16+48;XOR 位宽→高位平移→35−17→模 5→回绕区间及数值例子 | 推导 lower35/upper13,解释 18+17+13 与首次输出模型边界 | RANDOM_PROPERTIES.md |
| 06 结尾 | 两条优化路线和实验原则 | 把实现优化与结构化搜索串起来 | BENCHMARK.md |
画面以字幕节拍编排,口播稿与 SRT 在渲染时生成,避免维护三份不同步的时间轴。
没有把历史外推数据、删除的 05/06 性能分支或坐标范围趋势实验重新带入视频。

View file

@ -0,0 +1,85 @@
"""Check the narrated identities against direct first Java Random outputs.
This checks first-output arithmetic, not exhaustive rejection/redraw coverage.
Run with: uv run python check_lower35.py
"""
import random
A = 0x5DEECE66D
C = A ^ 987234911
MASK48 = (1 << 48) - 1
M = 8192
def first_output(seed, term):
# Keep the two original initialization XORs separate from the derivation.
raw_seed = (seed + term) & MASK48
state = ((raw_seed ^ 987234911) ^ A) & MASK48
return ((A * state + 11) & MASK48) >> 17
def j32(value):
return ((value + (1 << 31)) % (1 << 32)) - (1 << 31)
def coordinate_term(x, z):
return j32(4987142*x*x) + j32(5947611*x) + 4392871*j32(z*z) + j32(389711*z)
def check_case(lower, term):
q0 = first_output(lower, term)
r, b = q0 % (1 << 18), q0 >> 18
for h in range(M):
t = A*h % M
direct = first_output(lower + (h << 35), term)
derived = r + (1 << 18)*((b+t) % M)
assert direct == derived, (lower, term, h)
residue = (r-b + (2 if t >= M-b else 0)) % 5
allowed = (r % 2 == 0 and t % 5 == residue)
assert allowed == (direct % 10 == 0), (lower, term, h)
assert 4965*t % M == h
return r, b
def main():
assert C.bit_length() == 35
assert A*4965 % M == 1
# At a 34-bit boundary C still flips an unknown bit: simple translation fails.
assert (((1 << 34) ^ C) & MASK48) != ((C+(1 << 34)) & MASK48)
rng = random.Random(35013)
cases = [(0, 0), ((1 << 35)-1, -1), (0, MASK48),
((1 << 35)-1, (1 << 35)+1)]
# Real Java-wrapped terms, including signed int limits and negative offsets.
for x, z in [(51069, 419124), (-1, -1), (2**31-1, -2**31), (0, 0)]:
cases.append((rng.randrange(1 << 35), coordinate_term(x, z)))
relative = coordinate_term(x+1 if x < 2**31-1 else x-1, z)-coordinate_term(x, z)
cases.append((rng.randrange(1 << 35), relative))
cases += [(rng.randrange(1 << 35), rng.randrange(-2**63, 2**63)) for _ in range(20)]
for lower, term in cases:
check_case(lower, term)
# Find and verify a whole 25-cell intersection, with an actual planted seed.
lower = 10565146255
terms = [coordinate_term(51069+dx, 419124+dz)-coordinate_term(51069, 419124)
for dx in range(5) for dz in range(5)]
profiles = [(first_output(lower, d) % (1 << 18), first_output(lower, d) >> 18)
for d in terms]
points = sorted({0, M, *(M-b for _, b in profiles)})
ts = []
if all(r % 2 == 0 for r, _ in profiles):
for start, stop in zip(points, points[1:]):
residues = {(r-b+(2 if start >= M-b else 0)) % 5 for r, b in profiles}
if len(residues) == 1:
residue = residues.pop()
ts.extend(range(start+(residue-start) % 5, stop, 5))
analytic = {4965*t % M for t in ts}
direct = {h for h in range(M)
if all(first_output(lower+(h << 35), d) % 10 == 0 for d in terms)}
assert analytic == direct and direct
assert 2 + (1 << 18)*((8190+4) % M) == 524290
assert 4965*4 % M == 3476
print(f'OK: {len(cases)*M} single-cell identities; '
f'25-cell interval intersection equals all {M} high-bit trials ({len(direct)} candidates).')
if __name__ == '__main__':
main()

View file

@ -0,0 +1,23 @@
{
"source": "../results/latest.json",
"source_sha256": "044fed3620ae3e6b421948126c6a05614f60bead24997996ca68c76666431232",
"hardware": "Ryzen 7 5800H · RTX 3060 Laptop",
"A": {
"workload": "65,536 seeds × 257 × 257",
"metric": "CPU search / GPU scan",
"before_ms": 3550.398367,
"after_ms": 111.109917
},
"B": {
"workload": "65,536 seeds × 4097 × 4097",
"metric": "GPU kernel",
"before_ms": 31598.53906,
"after_ms": 17600.38672
},
"C": {
"workload": "1 seed × 8193 × 8193",
"metric": "GPU kernel",
"before_ms": 2.714623928,
"after_ms": 1.598464012
}
}

8
show/video/manim.cfg Normal file
View file

@ -0,0 +1,8 @@
[CLI]
renderer = cairo
background_color = #0B1220
pixel_width = 1920
pixel_height = 1080
frame_rate = 30
media_dir = media
verbosity = WARNING

10
show/video/pyproject.toml Normal file
View file

@ -0,0 +1,10 @@
[project]
name = "slime-research-video"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"av>=19.0.1",
"imageio-ffmpeg>=0.6.0",
"manim>=0.22.0",
"pillow>=12.3.0",
]

73
show/video/render.py Normal file
View file

@ -0,0 +1,73 @@
"""Render a film/episode, export subtitles, and create a chapter contact sheet."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import av
import imageio_ffmpeg
from PIL import Image, ImageDraw
from tex_environment import prepare_tex
HERE = Path(__file__).resolve().parent
parser = argparse.ArgumentParser()
parser.add_argument('--quality', choices=['preview', 'final'], default='preview')
parser.add_argument('--scene', choices=['SlimeFilm', 'ParallelEpisode', 'RandomEpisode'], default='SlimeFilm')
args = parser.parse_args()
os.chdir(HERE)
output = HERE / 'output'
output.mkdir(exist_ok=True)
tools = output / '.tools'
tools.mkdir(exist_ok=True)
ffmpeg = tools / 'ffmpeg'
if not ffmpeg.exists():
ffmpeg.symlink_to(imageio_ffmpeg.get_ffmpeg_exe())
env = dict(os.environ, PATH=str(tools)+os.pathsep+os.environ['PATH'])
env = prepare_tex(env)
quality = '-ql' if args.quality == 'preview' else '-qh'
fps = '15' if args.quality == 'preview' else '30'
subprocess.run([sys.executable, '-m', 'manim', quality, '--fps', fps, '--disable_caching',
'--format', 'mp4', '-o', args.scene, 'scenes.py', args.scene],
env=env, check=True)
source = next(p for p in (HERE / 'media/videos/scenes').rglob(f'{args.scene}.mp4')
if ('480p15' if args.quality == 'preview' else '1080p30') in str(p))
# Final resolution is 1080p; use explicit 30 fps to keep authoring/rendering practical.
target = output / f'{args.scene}-{args.quality}.mp4'
shutil.copy2(source, target)
timeline = json.loads((output / f'{args.scene}.timeline.json').read_text())
with av.open(str(target)) as video:
stream = video.streams.video[0]
report = {'file': target.name, 'width': stream.width, 'height': stream.height,
'fps': float(stream.average_rate), 'duration': float(stream.duration*stream.time_base),
'audio_streams': len(video.streams.audio), 'chapters': timeline['chapters'],
'caption_count': len(timeline['cues'])}
report['source_sha256'] = {p: hashlib.sha256((HERE / p).read_bytes()).hexdigest()
for p in ('scenes.py', 'render.py', 'tex_environment.py', 'manim.cfg', 'uv.lock', 'data/benchmarks.json')}
samples = [chapter['start']+3 for chapter in timeline['chapters']]
samples += [cue['start']+1 for cue in timeline['cues']
if cue['text'].startswith(('A 组', 'B 组', 'C 组', '同一 4096'))]
shots = []
for t in samples:
video.seek(int(t/stream.time_base), stream=stream, backward=True)
for frame in video.decode(stream):
if float(frame.pts*stream.time_base) >= t:
im = frame.to_image()
im.thumbnail((640, 360))
shots.append(im)
break
sheet = Image.new('RGB', (1280, ((len(shots)+1)//2)*390), '#0B1220')
draw = ImageDraw.Draw(sheet)
for i, im in enumerate(shots):
x, y = i%2*640, i//2*390
sheet.paste(im, (x,y))
draw.text((x+12,y+365), f'Frame {i+1} | {samples[i]:.1f}s', fill='white')
sheet.save(output / f'{args.scene}-{args.quality}-contact.png')
(output / f'{args.scene}-{args.quality}.render.json').write_text(json.dumps(report, ensure_ascii=False, indent=2)+'\n')
# Keep the subtitle/script matching this exact quality's frame-aligned timeline.
for ext in ('srt', 'timeline.json', 'narration.md'):
shutil.copy2(output / f'{args.scene}.{ext}', output / f'{args.scene}-{args.quality}.{ext}')
print(json.dumps(report, ensure_ascii=False, indent=2))

415
show/video/scenes.py Normal file
View file

@ -0,0 +1,415 @@
"""Chinese Manim film; all timings shown come from data/benchmarks.json."""
from pathlib import Path
import json
import os
import re
import manimpango
from manim import *
HERE = Path(__file__).resolve().parent
DATA = json.loads((HERE / 'data/benchmarks.json').read_text())
BG, INK, MUTED = '#0B1220', '#E8EEF7', '#94A3B8'
CYAN, MINT, AMBER, PANEL = '#38BDF8', '#34D399', '#FBBF24', '#172235'
# WSL may use an installed Windows font without copying/distributing it.
font_path = os.environ.get('SLIME_FONT_PATH', '/mnt/c/Windows/Fonts/msyh.ttc')
if Path(font_path).is_file():
manimpango.register_font(font_path)
fonts = manimpango.list_fonts()
FONT = next((f for f in ('Microsoft YaHei', 'Noto Sans CJK SC', 'WenQuanYi Zen Hei') if f in fonts), None)
if FONT is None:
raise RuntimeError('需要中文字体:安装 Noto Sans CJK,或设置 SLIME_FONT_PATH 指向中文字体。')
def text(value, size=28, color=INK, max_width=12):
obj = Text(str(value), font=FONT, font_size=size, color=color)
if obj.width > max_width:
obj.scale_to_fit_width(max_width)
return obj
MATH_TEMPLATE = TexTemplate(
documentclass=r"\documentclass[preview]{standalone}",
preamble=r"\usepackage{amsmath}\usepackage{amssymb}",
)
def formula_tex(value, size=32, color=INK, max_width=12):
obj = MathTex(value, tex_template=MATH_TEMPLATE, font_size=size, color=color)
if obj.width > max_width:
obj.scale_to_fit_width(max_width)
return obj
def subtitle_text(value, max_width=12.4):
"""Mix Pango Chinese prose and actual MathTex for $...$ inline formulas."""
if value.count('$') % 2:
raise ValueError(f'Unbalanced inline math: {value}')
parts = VGroup()
for segment in re.split(r'(\$[^$]+\$)', value):
if not segment.strip():
continue
if segment.startswith('$') and segment.endswith('$'):
parts.add(formula_tex(segment[1:-1], 30))
else:
parts.add(text(segment.strip(), 24))
parts.arrange(RIGHT, buff=.055, aligned_edge=DOWN)
if parts.width > max_width:
parts.scale_to_fit_width(max_width)
return parts
def grid(n=5, side=.48, color=CYAN):
return VGroup(*[
Square(side_length=side, stroke_color=color, stroke_width=1.3,
fill_color=PANEL, fill_opacity=1).move_to(
[(j-(n-1)/2)*side, ((n-1)/2-i)*side, 0])
for i in range(n) for j in range(n)
])
def box(label, width=2.5, height=.65, color=CYAN):
frame = RoundedRectangle(width=width, height=height, corner_radius=.10,
stroke_color=color, fill_color=PANEL, fill_opacity=1)
return VGroup(frame, text(label, 22, color, width-.2))
class FilmBase(Scene):
def setup(self):
self.camera.background_color = BG
self.cues = []
self.chapters = []
self.subtitle = None
def chapter(self, number, title, tag):
if self.mobjects:
self.play(*[FadeOut(m) for m in list(self.mobjects)], run_time=.45)
self.subtitle = None
self.chapters.append({'title': title, 'start': float(self.time)})
heading = text(title, 34).to_edge(UP, buff=.35).align_to(LEFT*6.5, LEFT)
badge = text(number, 20, MINT).to_corner(UR, buff=.4)
footer = text(tag, 17, MUTED).move_to([0, -2.93, 0])
line = Line([-6.5, -3.16, 0], [6.5, -3.16, 0], color=PANEL)
self.play(FadeIn(heading), FadeIn(badge), FadeIn(footer), Create(line), run_time=.6)
def say(self, caption, seconds=5, *, display=None):
if self.subtitle is not None:
self.remove(self.subtitle)
self.subtitle = subtitle_text(display if display is not None else caption).move_to([0, -3.58, 0])
self.add(self.subtitle)
self.cues.append({'start': float(self.time), 'end': float(self.time + seconds), 'text': caption, 'display': display if display is not None else caption})
self.wait(seconds)
def chart(self, key, labels):
d = DATA[key]
before, after = d['before_ms'], d['after_ms']
max_width = 6.4
# Exact linear scale, including very small optimized bars.
baseline = -3.4
items = VGroup()
for i, (label, value, color) in enumerate(zip(labels, (before, after), (AMBER, MINT))):
y = .5-i*1.0
name = text(label, 24, max_width=2.5).move_to([-5, y, 0])
bar = Rectangle(width=max_width*value/before, height=.4,
stroke_width=0, fill_color=color, fill_opacity=1)
bar.move_to([baseline+bar.width/2, y, 0])
value_text = text(f'{value:.3f} ms', 23, color).next_to(bar, RIGHT, buff=.18)
items.add(name, bar, value_text)
ratio = formula_tex(rf'{before/after:.2f}\times', 46, MINT).move_to([0, -1.45, 0])
condition = text(d['workload'], 25, CYAN).move_to([0, 1.55, 0])
metric_label = {
'A': 'CPU 搜索 / GPU scan · 中位数 / 5 轮 · CPU 16 逻辑线程',
'B': 'GPU kernel · 中位数 / 5 轮 · 同组完整命中列表一致',
'C': 'GPU kernel · 中位数 / 100 轮 · 同组完整命中列表一致',
}[key]
metric = text(metric_label, 19, MUTED).move_to([0, -2.2, 0])
self.play(FadeIn(condition), LaggedStart(*[FadeIn(m) for m in items], lag_ratio=.08),
FadeIn(ratio), FadeIn(metric), run_time=1.8)
return VGroup(condition, items, ratio, metric)
def opening(self):
self.chapter('00', '寻找史莱姆区块:从暴力搜索到结构化计算', '示意图用于解释工作分配;测速来自实际程序')
g = grid(9, .40).move_to([-3.3, -.15, 0])
title = VGroup(text('同一个问题', 38, CYAN), text('换一种计算方式', 38, MINT),
text('CPU → CUDA → 复用 → 数学性质', 24, MUTED)).arrange(DOWN, buff=.4).move_to([2.3, 0, 0])
self.play(LaggedStart(*[FadeIn(c) for c in g], lag_ratio=.008), FadeIn(title), run_time=1.5)
target = [g[i*9+j] for i in range(2, 7) for j in range(2, 7)]
self.play(*[c.animate.set_fill(MINT, opacity=.75) for c in target], run_time=.8)
self.say('我们想找一个 seed,以及一片连续 5×5 的史莱姆区块。', 6, display='我们想找一个 seed,以及一片连续 $5\\times5$ 的史莱姆区块。')
self.say('先让程序算得更快,再研究随机数本身的结构。', 6)
def problem(self):
self.chapter('01', 'CPU:先把正确答案算出来', '源码:01_cpu.cpp · 原始 Java Random 语义 · 不通过便立即退出')
g = grid(5, .62).move_to([-3.2, .15, 0])
code = VGroup(*[text(s, 25, c, 6.1) for s,c in [
('for seed in seeds:', CYAN), (' for (x, z) in anchors:', INK),
(' check 5 × 5 cells', INK), (' stop on first failure', AMBER),
(' collect complete hits', MINT)]]).arrange(DOWN, aligned_edge=LEFT, buff=.26).move_to([2.5, .25, 0])
self.play(FadeIn(g), FadeIn(code), run_time=1)
self.play(g[0].animate.set_fill(MINT, opacity=.6), g[1].animate.set_fill(AMBER, opacity=.8), run_time=.8)
self.say('对每个 seed,遍历锚点,再判断锚点周围的 25 格。', 6)
self.say('一格不满足就停止,只有全部通过才记录结果。', 6)
note = text('CPU 基线也使用多线程:实测为 16 个逻辑线程', 23, MUTED).move_to([0, -2.1, 0])
self.play(FadeIn(note))
self.say('保留 CPU 实现,后面的优化都要核对相同搜索结果。', 6)
def cuda(self):
self.chapter('02', 'CUDA:每个 thread 负责一个 seed', '源码:02_cuda_seed.cuh · 图中只展示少量线程,实际启动更多线程')
workers = VGroup(*[box(f'thread {i}', 1.45, .65) for i in range(6)]).arrange(RIGHT, buff=.3).move_to([0, 1.3, 0])
seeds = VGroup(*[text(f'seed {i}', 23, MINT) for i in range(6)]).arrange(RIGHT, buff=.75).move_to([0, .2, 0])
maps = VGroup(*[grid(3, .24) for _ in range(6)]).arrange(RIGHT, buff=1.0).move_to([0, -1.1, 0])
arrows = VGroup(*[Arrow(w.get_bottom(), s.get_top(), buff=.1, color=MUTED) for w,s in zip(workers,seeds)])
self.play(FadeIn(workers), FadeIn(seeds), FadeIn(maps), Create(arrows), run_time=1.5)
for cell in range(3):
self.play(*[g[cell].animate.set_fill(MINT, opacity=.65) for g in maps], run_time=.35)
self.say('把 seed 分给 GPU 线程;每个线程仍然遍历整个坐标窗口。', 7)
self.say('相同的检查逻辑,被更多线程同时执行。', 5)
self.play(FadeOut(VGroup(workers, seeds, maps, arrows)), run_time=.6)
self.chart('A', ('CPU', 'CUDA / seed'))
self.say('A 组:CPU 搜索 3550 毫秒,GPU scan 111 毫秒,约快 32 倍。', 8)
self.say('这里比较的是搜索时间和 GPU scan,不能改拿 kernel 时间计算这个倍数。', 7)
def mapping(self):
self.chapter('03', '改变分工:固定坐标,遍历 seed', '源码:03_cuda_position.cuh · 坐标项存在线程私有数组中 · 不引入全局坐标表')
self.coordinate_identity()
left = VGroup(box('thread → seed', 3.2, color=AMBER),
text('for each (x, z)', 25), text('compute F(x+dx), G(z+dz)', 22, AMBER)).arrange(DOWN, buff=.35).move_to([-3.4, .6, 0])
right = VGroup(box('thread → (x, z)', 3.2, color=MINT),
text('precompute 5 X + 5 Z terms', 22, MINT),
text('for each seed: reuse terms', 23)).arrange(DOWN, buff=.35).move_to([3.1, .6, 0])
arrow = Arrow([-1.2, .5, 0], [1.0, .5, 0], color=CYAN)
self.play(FadeIn(left), FadeIn(right), GrowArrow(arrow), run_time=1.2)
formula = formula_tex(r'T(x+\Delta x,z+\Delta z)=F(x+\Delta x)+G(z+\Delta z)', 34, CYAN).move_to([0, -1.7, 0])
self.play(FadeIn(formula))
self.say('坐标固定之后,X 和 Z 的多项式在 seed 循环外计算。', 7, display='坐标固定之后,$X$ 和 $Z$ 的多项式在 seed 循环外计算。')
self.say('同一份坐标项,可以在许多 seed 上重复使用。', 6)
self.play(FadeOut(VGroup(left, right, arrow, formula)))
self.chart('B', ('CUDA / seed', 'CUDA / coordinate'))
self.say('B 组扩大到 4097×4097;相同任务下,从 31.599 秒降到 17.600 秒。', 8, display='B 组扩大到 $4097\\times4097$;相同任务下,从 31.599 秒降到 17.600 秒。')
self.say('这一组重新测量两个版本,只在组内比较:约快 1.80 倍。', 7)
def outer(self):
self.chapter('04', 'Outer sum:邻居也可以共享坐标项', '源码:04_cuda_outer_sum.cuh · 实际 block 为 16×16 · 示意图简化为 5×5')
self.outer_identity()
g = grid(5, .54).move_to([0, .1, 0])
xs = VGroup(*[formula_tex(rf'F_{{{i}}}', 28, CYAN).move_to([g[i*5].get_x()-.75, g[i*5].get_y(), 0]) for i in range(5)])
zs = VGroup(*[formula_tex(rf'G_{{{i}}}', 28, MINT).move_to([g[i].get_x(), g[i].get_y()+.65, 0]) for i in range(5)])
self.play(FadeIn(g), FadeIn(xs), FadeIn(zs), run_time=1)
self.play(*[g[2*5+j].animate.set_fill(CYAN, opacity=.4) for j in range(5)], run_time=.7)
self.say('固定 X 的坐标项沿一整行复用;固定 Z 的项沿一整列复用。', 7, display='固定 $X$ 的坐标项沿一整行复用;固定 $Z$ 的项沿一整列复用。')
self.play(*[g[i*5+3].animate.set_fill(MINT, opacity=.4) for i in range(5)], run_time=.7)
formula = formula_tex(r'T_{ij}=X_i+Z_j', 36, MINT, 4.5).move_to([3.9, -.1, 0])
self.play(FadeIn(formula))
self.say('把两条轴放进 shared memory,再用 outer sum 组合每格的输入。', 7)
self.play(FadeOut(VGroup(g, xs, zs, formula)))
note = text('16×16 锚点 + 5×5 图案 → 每条轴需要 20 项(包含 halo)', 22, MUTED).move_to([0, 2.1, 0])
self.add(note)
self.chart('C', ('fixed coordinate', 'tiled outer sum'))
self.say('C 组固定一个 seed;kernel 从 2.715 毫秒降到 1.598 毫秒。', 8)
self.say('重新测量 03 再比较 04,约快 1.70 倍;不同负载的倍数不能连乘。', 7)
def coordinate_identity(self):
intro = text('先看送入 Random 之前的坐标加和项', 25, MUTED).move_to([0,2.05,0])
original = formula_tex(
r'T(x,z)=\operatorname{J}_{32}(4987142x^2)+\operatorname{J}_{32}(5947611x)'
r'+4392871\operatorname{J}_{32}(z^2)+\operatorname{J}_{32}(389711z)',
34).move_to([0,1.25,0])
wrap = VGroup(
formula_tex(r'\operatorname{J}_{32}(a)=((a+2^{31})\bmod 2^{32})-2^{31}', 30, MUTED),
text('32 位回绕 → 有符号解释 → 提升到 64 位', 21, MUTED),
).arrange(DOWN, buff=.15).move_to([0,-1.9,0])
self.play(FadeIn(intro), FadeIn(original), FadeIn(wrap), run_time=1.1)
self.say('把原始坐标公式写完整,并保留 Java 的 32 位回绕位置。', 7)
x_part = formula_tex(
r'F(x)=\operatorname{J}_{32}(4987142x^2)+\operatorname{J}_{32}(5947611x)',
36, CYAN).move_to([0,.35,0])
z_part = formula_tex(
r'G(z)=4392871\operatorname{J}_{32}(z^2)+\operatorname{J}_{32}(389711z)',
36, MINT).move_to([0,-.55,0])
self.play(FadeIn(x_part), FadeIn(z_part), run_time=1)
self.say('前两项只依赖 X,后两项只依赖 Z,把它们分别命名为 F 和 G。', 8, display='前两项只依赖 $X$,后两项只依赖 $Z$,把它们分别命名为 $F$ 和 $G$。')
separated = formula_tex(r'T(x,z)=F(x)+G(z)', 44, AMBER).move_to(original)
self.play(Transform(original, separated), run_time=1)
self.say('没有 X 与 Z 相乘的混合项,所以坐标输入能精确拆成两条独立的轴。', 8, display='没有 $X$ 与 $Z$ 相乘的混合项,所以坐标输入能精确拆成两条独立的轴。')
self.say('这只是按变量重新分组,不是近似;每一项的整数回绕规则都保持原样。', 7)
self.play(FadeOut(VGroup(intro,original,x_part,z_part,wrap)), run_time=.6)
def outer_identity(self):
entry = formula_tex(r'T_{ij}=F(x_i)+G(z_j)=X_i+Z_j', 42, CYAN).move_to([0,1.8,0])
vectors = formula_tex(
r'\mathbf X=(X_0,\ldots,X_{m-1})^{\mathsf T},\qquad'
r'\mathbf Z=(Z_0,\ldots,Z_{n-1})^{\mathsf T}', 32, MUTED).move_to([0,.9,0])
identity = formula_tex(
r'\mathbf T=\underbrace{\mathbf X\mathbf 1_n^{\mathsf T}}_{\text{repeat X across columns}}'
r'+\underbrace{\mathbf 1_m\mathbf Z^{\mathsf T}}_{\text{repeat Z across rows}}',
40, MINT).move_to([0,-.2,0])
self.play(FadeIn(entry), FadeIn(vectors), FadeIn(identity), run_time=1.3)
self.say('对所有 X 和所有 Z 配对,第 i 行、第 j 列就是 X 的第 i 项加 Z 的第 j 项。', 8, display='对所有 $X$ 和所有 $Z$ 配对,第 $i$ 行、第 $j$ 列就是 $X_i+Z_j$。')
self.say('矩阵写法是:X 列向量复制到每列,Z 行向量复制到每行,再逐项相加。', 8, display='矩阵写法是:$X$ 列向量复制到每列,$Z$ 行向量复制到每行,再逐项相加。')
self.play(FadeOut(vectors), FadeOut(identity), run_time=.5)
example = formula_tex(
r'\begin{pmatrix}2&2&2\\5&5&5\end{pmatrix}'
r'+\begin{pmatrix}10&20&30\\10&20&30\end{pmatrix}'
r'=\begin{pmatrix}12&22&32\\15&25&35\end{pmatrix}',
40).move_to([0,.1,0])
note = text('数值仅用于演示广播相加;不是实测坐标项', 21, MUTED).move_to([0,-1.25,0])
contrast = text('outer sum:两条轴相加 · outer product:两条轴相乘', 24, AMBER).move_to([0,-2.0,0])
self.play(FadeIn(example), FadeIn(note), FadeIn(contrast), run_time=1)
self.say('例如 X 是二和五,Z 是十、二十、三十,就得到这张二乘三的加法表。', 8, display='例如 $X=(2,5)^{\\mathsf T}$,$Z=(10,20,30)^{\\mathsf T}$,就得到这张 $2\\times3$ 的加法表。')
self.say('两项 X 加三项 Z,只需五次轴函数计算;六个格子仍需六次相加。', 8, display='两项 $X$ 加三项 $Z$,只需五次轴函数计算;六个格子仍需六次相加。')
self.say('这就是 outer sum:所有交叉组合都相加。外积则是相乘,运算不同。', 7)
self.say('我们复用的是 Random 之前的坐标项,仍要逐格执行随机数判断。', 8)
self.play(FadeOut(VGroup(entry,example,note,contrast)), run_time=.6)
def random(self):
self.chapter('05', '研究 Random:从计算速度转向状态空间', '性质章节独立于 01–04 的测速 · seed 均指有效的 48-bit 状态等价类')
bits = VGroup(*[Rectangle(width=.19, height=.55, stroke_width=.7,
stroke_color=BG, fill_color=AMBER if i<16 else CYAN,
fill_opacity=1) for i in range(64)]).arrange(RIGHT, buff=.015).move_to([0, 1.25, 0])
labels = VGroup(text('高 16 位:不影响 Random 结果', 24, AMBER).move_to([-4.0,.3,0]),
text('低 48 位:决定内部状态', 24, CYAN).move_to([2,.3,0]))
self.play(FadeIn(bits), FadeIn(labels), run_time=1)
formula = VGroup(formula_tex(r'u=\bigl((\mathrm{seed}+T)\mathbin{\mathtt{xor}}C\bigr)\bmod 2^{48}', 34, CYAN),
formula_tex(r'v=(Au+11)\bmod 2^{48}', 34),
formula_tex(r'q=\left\lfloor\frac{v}{2^{17}}\right\rfloor', 34, MINT)).arrange(DOWN, buff=.25).move_to([0, -1.4, 0])
self.play(FadeIn(formula))
self.say('输入是 64 位,但 Java Random 的状态只有 48 位。', 7)
self.say('相差 2^48 的 seed 给出相同结果,包括拒绝重抽的路径。', 7, display='相差 $2^{48}$ 的 seed 给出相同结果,包括拒绝重抽的路径。')
self.say('这不是一个微小的计时优化,而是搜索空间里的等价关系。', 7)
self.play(FadeOut(VGroup(bits,labels,formula)))
self.lower35_proof()
def lower35_proof(self):
def page(title, equations, note=None):
heading = text(title, 27, CYAN).move_to([0, 2.15, 0])
rows = VGroup(*[formula_tex(eq, 32, color) for eq, color in equations])
rows.arrange(DOWN, buff=.32).move_to([0, .25, 0])
objects = VGroup(heading, rows)
if note:
objects.add(text(note, 21, MUTED).move_to([0, -2.25, 0]))
self.play(FadeIn(objects), run_time=.7)
return objects
first = page('先明确未知量:不是把高 13 位忽略', [
(r's=\ell+2^{35}h,\quad 0\le\ell<2^{35},\quad0\le h<8192', CYAN),
(r'u_i(h)=\bigl((s+d_i)\mathbin{\mathtt{xor}}C\bigr)\bmod2^{48}', INK),
(r'C=\mathtt{0x5E434E432},\qquad 2^{34}\le C<2^{35}', MINT),
], '固定坐标求解:s 是世界种子')
self.say('低 35 位是待求输入的一部分;固定它之后,还要求出高 13 位。', 8, display='$\\ell=s\\bmod2^{35}$ 是待求输入的低 35 位;固定它之后,还要求出高 13 位。')
self.say('为什么选 35?合并后的 XOR 常量恰好占 35 位,不会翻转更高位。', 9)
self.play(FadeOut(first))
carry = page('保留坐标进位,再分离 XOR', [
(r'(\ell+d_i)\bmod2^{48}=L_i+2^{35}H_i', INK),
(r'(s+d_i)\bmod2^{48}=L_i+2^{35}\bigl((H_i+h)\bmod8192\bigr)', CYAN),
(r'u_i(h)=\bigl(u_i(0)+2^{35}h\bigr)\bmod2^{48}', MINT),
], 'Hᵢ 包含坐标加法的进位;C 只 XOR 低位 Lᵢ')
self.say('坐标项加到低位上可能进位,我们把进位保留在 H 里面。', 8, display='坐标项加到低位上可能进位,我们把进位保留在 $H_i$ 里面。')
self.say('XOR 只改变低位,所以改变 h,相当于让初始化状态增加 h 乘 2^35。', 10, display='XOR 只改变低位,所以改变 $h$,相当于让初始化状态增加 $h\\,2^{35}$。')
self.say('这个平移来自位域分离,不能把一般的 XOR 当成线性运算。', 8)
self.play(FadeOut(carry))
output = page('一次状态推进:35 − 17 = 18', [
(r'v_i(h)=\bigl(v_i(0)+Ah\,2^{35}\bigr)\bmod2^{48}', INK),
(r'q_i(h)=\bigl(q_i(0)+Ah\,2^{18}\bigr)\bmod2^{31}', CYAN),
(r'q_i(0)=r_i+2^{18}b_i', INK),
(r'q_i(h)=r_i+2^{18}\bigl((b_i+Ah)\bmod8192\bigr)', MINT),
], '0 ≤ rᵢ < 2¹⁸;0 ≤ bᵢ < 8192;所有格子共用同一个 h')
self.say('LCG 先乘 A。next(31) 再去掉状态低 17 位,因此 2^35 变成 2^18。', 11, display='LCG 先乘 $A$。next(31) 再去掉状态低 17 位,因此 $2^{35}$ 变成 $2^{18}$。')
self.say('把首次输出拆成 r 和 b:高位 h 只改变 b,输出的低 18 位 r 保持不变。', 10, display='把首次输出拆成 $r_i$ 和 $b_i$:高位 $h$ 只改变高位部分,低 18 位 $r_i$ 保持不变。')
self.play(FadeOut(output))
modular = page('模 10 条件 = 奇偶条件 + 模 5 条件', [
(r'y_i=(b_i+Ah)\bmod8192,\qquad q_i=r_i+2^{18}y_i', INK),
(r'2^{18}\equiv0\pmod2,\qquad2^{18}\equiv-1\pmod5', CYAN),
(r'q_i\equiv0\pmod{10}\iff r_i\equiv0\pmod2\ \land\ y_i\equiv r_i\pmod5', MINT),
], '这里先求 q mod 10 = 0 的候选;Java 拒绝重抽在后面单独说明')
self.say('如果 r 是奇数,所有 h 都无法让首次输出被 10 整除。', 8, display='如果 $r_i$ 是奇数,所有 $h$ 都无法让首次输出被 10 整除。')
self.say('r 为偶数时,因为 2^18 模 5 等于负一,剩下的要求就是 y 与 r 模 5 相同。', 11, display='$r_i$ 为偶数时,因为 $2^{18}\\equiv-1\\pmod5$,剩下的要求就是 $y_i\\equiv r_i\\pmod5$。')
self.play(FadeOut(modular))
intervals = page('换变量:每个格子最多回绕一次', [
(r't=Ah\bmod8192,\qquad h=4965t\bmod8192', CYAN),
(r'y_i=(b_i+t)\bmod8192', INK),
(r'\begin{cases}t\equiv r_i-b_i\pmod5,&t<8192-b_i,\\'
r't\equiv r_i-b_i+2\pmod5,&t\ge8192-b_i.\end{cases}', MINT),
], 'A 是奇数,模 8192 可逆;回绕后 +2 来自 8192 mod 5 = 2')
self.say('把 A 乘 h 换成 t。A 是奇数,这个换元一一对应,不会丢掉高位解。', 10, display='令 $t=Ah\\bmod8192$。$A$ 是奇数,这个换元一一对应,不会丢掉高位解。')
self.say('b 加 t 最多回绕一次,所以每格只有两段固定的模 5 要求。', 9, display='$b_i+t$ 最多回绕一次,所以每格只有两段固定的模 5 要求。')
self.play(FadeOut(intervals))
example = page('数值例子:看见回绕怎样改变要求', [
(r'r=2,\quad b=8190,\quad8192-b=2', CYAN),
(r'0\le t<2:\ t\equiv2\pmod5\quad\Longrightarrow\quad\varnothing', INK),
(r'2\le t<8192:\ t\equiv4\pmod5\quad\Longrightarrow\quad t=4,9,14,\ldots', MINT),
(r't=4:\quad y=2,\quad q=2+2^{18}\cdot2=524290', AMBER),
], 'r、b 为示意数值;取 t=4 时恢复 h=3476')
self.say('例如 r 等于二、b 等于 8190,t 到二就回绕,允许的余数从二变成四。', 11, display='例如 $r_i=2$、$b_i=8190$,$t=2$ 时回绕,允许的余数从二变成四。')
self.say('取 t 等于四,输出为 524290,确实能被十整除。', 8, display='取 $t=4$,输出为 $q=524290$,确实能被十整除。')
self.play(FadeOut(example))
joint = page('多个格子:按回绕点分段,再求交', [
(r't\in[a_j,a_{j+1}),\qquad t\equiv c_i\pmod5\ \ (\forall i)', INK),
(r'c_i\ne c_k\ \Longrightarrow\ \varnothing', AMBER),
(r'c_i=c\ \ (\forall i)\ \Longrightarrow\ t=c+5k\in[a_j,a_{j+1})', MINT),
(r'\underbrace{18}_{\mathrm{parity}}+\underbrace{17}_{\mathrm{enumerate}}'
r'+\underbrace{13}_{\mathrm{solve}}=48', CYAN),
], '输入低 18 位决定首次输出奇偶;再补上中间 17 位得到低 35 位;最后求高 13 位')
self.say('排序所有格子的回绕点。每段内,只要两格要求不同余数,就能丢掉整段。', 10)
self.say('要求一致时,直接按间隔五生成 t,再恢复 h,不必逐个检查 8192 个高位。', 11, display='要求一致时,直接按间隔五生成 $t$,再恢复 $h$,不必逐个检查 8192 个高位。')
self.say('低 18 位先筛奇偶,中间 17 位目前仍需扩展;低 35 位候选的生成成本没有消失。', 10, display='低 18 位先筛奇偶,中间 17 位仍需扩展;$\\ell=s\\bmod2^{35}$ 的供给成本没有消失。')
self.play(FadeOut(joint))
boundary = page('公式精确,但候选条件有适用边界', [
(r'q<2^{31}-8:\quad\mathrm{nextInt}(10)=q\bmod10', MINT),
(r'q\ge2^{31}-8:\quad\mathrm{reject\ and\ redraw}', AMBER),
(r'2147483640\bmod10=0\quad\text{but this output is rejected}', INK),
], '精确验证能去掉假阳性,不能补回首次输出筛选漏掉的重抽成功解')
self.say('Java 会拒绝最高八个首次输出并重抽;此时不能只看首次输出模十。', 10)
self.say('所以这里是首次输出候选模型,还不能宣称穷尽了所有 Java Random 解。', 9)
def ending(self):
self.chapter('06', '优化的两条线:复用计算,利用结构', DATA['hardware'])
rows = VGroup(*[text(s, 29, c) for s,c in [
('CPU → CUDA:把独立任务同时做', CYAN),
('固定坐标 → outer sum:减少重复的坐标计算', MINT),
('Random → 高位解析:利用输出的模运算结构', AMBER),
('每个加速比,都要有同任务、同输入的对照', INK)]]).arrange(DOWN, aligned_edge=LEFT, buff=.55).move_to([0,.2,0])
self.play(LaggedStart(*[FadeIn(r) for r in rows], lag_ratio=.2), run_time=1.8)
self.say('先理解重复工作在哪里,再决定并行、复用,还是改变搜索方法。', 7)
self.say('源码、完整测试口径和原始测速记录,都放在 show 目录里。', 6)
def tear_down(self):
output = HERE / 'output'
output.mkdir(exist_ok=True)
name = self.__class__.__name__
(output / f'{name}.timeline.json').write_text(json.dumps(
{'duration': float(self.time), 'chapters': self.chapters, 'cues': self.cues}, ensure_ascii=False, indent=2)+'\n')
def stamp(t):
ms = round(t*1000)
return f'{ms//3600000:02}:{ms//60000%60:02}:{ms//1000%60:02},{ms%1000:03}'
(output / f'{name}.srt').write_text('\n\n'.join(
f'{i}\n{stamp(c["start"])} --> {stamp(c["end"])}\n{c["text"]}' for i,c in enumerate(self.cues,1))+'\n')
(output / f'{name}.narration.md').write_text('# 口播稿(与当前渲染时间轴一致)\n\n'+
'\n\n'.join(f'[{stamp(c["start"])}] {c["text"]}' for c in self.cues)+'\n')
super().tear_down()
class SlimeFilm(FilmBase):
def construct(self):
for part in (self.opening, self.problem, self.cuda, self.mapping,
self.outer, self.random, self.ending):
part()
class ParallelEpisode(FilmBase):
def construct(self):
for part in (self.opening, self.problem, self.cuda, self.mapping, self.outer):
part()
class RandomEpisode(FilmBase):
def construct(self):
for part in (self.random, self.ending):
part()

View file

@ -0,0 +1,43 @@
"""Use system TeX Live, or an explicitly configured local unpacked installation."""
from pathlib import Path
import os
import shutil
import subprocess
HERE = Path(__file__).resolve().parent
def prepare_tex(env):
env = dict(env)
if shutil.which('latex', path=env['PATH']) and shutil.which('dvisvgm', path=env['PATH']):
return env
root = Path(env.get('SLIME_TEX_ROOT', HERE / '.native/texlive')).resolve()
if not (root/'usr/bin/pdftex').is_file():
raise RuntimeError('需要 LaTeX / dvisvgm。Ubuntu: sudo apt install texlive-latex-extra dvisvgm')
dist = root/'usr/share/texlive/texmf-dist'
formats = root/'formats'
formats.mkdir(exist_ok=True)
env.update({
'PATH': str(root/'usr/bin')+os.pathsep+env['PATH'],
'LD_LIBRARY_PATH': str(root/'usr/lib/x86_64-linux-gnu')+os.pathsep+env.get('LD_LIBRARY_PATH', ''),
'TEXMFCNF': str(dist/'web2c'),
'TEXMFROOT': str(root/'usr/share/texlive'),
'TEXMFDIST': str(dist),
'TEXMF': '{'+str(dist)+','+str(root/'usr/share/texmf')+'}',
'TEXMFVAR': str(root/'var'),
'TEXMFCONFIG': str(root/'config'),
'TEXMFHOME': str(root/'texmf-home'),
'TEXFORMATS': str(formats)+'//:',
})
gs_roots = sorted((root/'usr/share/ghostscript').glob('*'))
if gs_roots:
gs = gs_roots[-1]
env['GS_LIB'] = os.pathsep.join(str(p) for p in (
gs/'Resource/Init', gs/'lib', gs/'Resource/Font',
root/'usr/share/fonts/type1/urw-base35'))
if not (formats/'latex.fmt').is_file():
subprocess.run(['mktexlsr', str(dist), str(root/'usr/share/texmf')], env=env, check=True)
subprocess.run(['pdftex', '-ini', '-etex', '-interaction=nonstopmode', '-halt-on-error',
'-jobname=latex', '-progname=latex', 'latex.ini'],
cwd=formats, env=env, check=True)
return env

904
show/video/uv.lock generated Normal file
View file

@ -0,0 +1,904 @@
version = 1
revision = 3
requires-python = ">=3.12"
resolution-markers = [
"python_full_version >= '3.13'",
"python_full_version < '3.13'",
]
[[package]]
name = "audioop-lts"
version = "0.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/38/53/946db57842a50b2da2e0c1e34bd37f36f5aadba1a929a3971c5d7841dbca/audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", size = 30686, upload-time = "2025-08-05T16:43:17.409Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/d4/94d277ca941de5a507b07f0b592f199c22454eeaec8f008a286b3fbbacd6/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800", size = 46523, upload-time = "2025-08-05T16:42:20.836Z" },
{ url = "https://files.pythonhosted.org/packages/f8/5a/656d1c2da4b555920ce4177167bfeb8623d98765594af59702c8873f60ec/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", size = 27455, upload-time = "2025-08-05T16:42:22.283Z" },
{ url = "https://files.pythonhosted.org/packages/1b/83/ea581e364ce7b0d41456fb79d6ee0ad482beda61faf0cab20cbd4c63a541/audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", size = 26997, upload-time = "2025-08-05T16:42:23.849Z" },
{ url = "https://files.pythonhosted.org/packages/b8/3b/e8964210b5e216e5041593b7d33e97ee65967f17c282e8510d19c666dab4/audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", size = 85844, upload-time = "2025-08-05T16:42:25.208Z" },
{ url = "https://files.pythonhosted.org/packages/c7/2e/0a1c52faf10d51def20531a59ce4c706cb7952323b11709e10de324d6493/audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", size = 85056, upload-time = "2025-08-05T16:42:26.559Z" },
{ url = "https://files.pythonhosted.org/packages/75/e8/cd95eef479656cb75ab05dfece8c1f8c395d17a7c651d88f8e6e291a63ab/audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", size = 93892, upload-time = "2025-08-05T16:42:27.902Z" },
{ url = "https://files.pythonhosted.org/packages/5c/1e/a0c42570b74f83efa5cca34905b3eef03f7ab09fe5637015df538a7f3345/audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", size = 96660, upload-time = "2025-08-05T16:42:28.9Z" },
{ url = "https://files.pythonhosted.org/packages/50/d5/8a0ae607ca07dbb34027bac8db805498ee7bfecc05fd2c148cc1ed7646e7/audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", size = 79143, upload-time = "2025-08-05T16:42:29.929Z" },
{ url = "https://files.pythonhosted.org/packages/12/17/0d28c46179e7910bfb0bb62760ccb33edb5de973052cb2230b662c14ca2e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", size = 84313, upload-time = "2025-08-05T16:42:30.949Z" },
{ url = "https://files.pythonhosted.org/packages/84/ba/bd5d3806641564f2024e97ca98ea8f8811d4e01d9b9f9831474bc9e14f9e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", size = 93044, upload-time = "2025-08-05T16:42:31.959Z" },
{ url = "https://files.pythonhosted.org/packages/f9/5e/435ce8d5642f1f7679540d1e73c1c42d933331c0976eb397d1717d7f01a3/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", size = 78766, upload-time = "2025-08-05T16:42:33.302Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3b/b909e76b606cbfd53875693ec8c156e93e15a1366a012f0b7e4fb52d3c34/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", size = 87640, upload-time = "2025-08-05T16:42:34.854Z" },
{ url = "https://files.pythonhosted.org/packages/30/e7/8f1603b4572d79b775f2140d7952f200f5e6c62904585d08a01f0a70393a/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", size = 86052, upload-time = "2025-08-05T16:42:35.839Z" },
{ url = "https://files.pythonhosted.org/packages/b5/96/c37846df657ccdda62ba1ae2b6534fa90e2e1b1742ca8dcf8ebd38c53801/audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", size = 26185, upload-time = "2025-08-05T16:42:37.04Z" },
{ url = "https://files.pythonhosted.org/packages/34/a5/9d78fdb5b844a83da8a71226c7bdae7cc638861085fff7a1d707cb4823fa/audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", size = 30503, upload-time = "2025-08-05T16:42:38.427Z" },
{ url = "https://files.pythonhosted.org/packages/34/25/20d8fde083123e90c61b51afb547bb0ea7e77bab50d98c0ab243d02a0e43/audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", size = 24173, upload-time = "2025-08-05T16:42:39.704Z" },
{ url = "https://files.pythonhosted.org/packages/58/a7/0a764f77b5c4ac58dc13c01a580f5d32ae8c74c92020b961556a43e26d02/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", size = 47096, upload-time = "2025-08-05T16:42:40.684Z" },
{ url = "https://files.pythonhosted.org/packages/aa/ed/ebebedde1a18848b085ad0fa54b66ceb95f1f94a3fc04f1cd1b5ccb0ed42/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", size = 27748, upload-time = "2025-08-05T16:42:41.992Z" },
{ url = "https://files.pythonhosted.org/packages/cb/6e/11ca8c21af79f15dbb1c7f8017952ee8c810c438ce4e2b25638dfef2b02c/audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", size = 27329, upload-time = "2025-08-05T16:42:42.987Z" },
{ url = "https://files.pythonhosted.org/packages/84/52/0022f93d56d85eec5da6b9da6a958a1ef09e80c39f2cc0a590c6af81dcbb/audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", size = 92407, upload-time = "2025-08-05T16:42:44.336Z" },
{ url = "https://files.pythonhosted.org/packages/87/1d/48a889855e67be8718adbc7a01f3c01d5743c325453a5e81cf3717664aad/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", size = 91811, upload-time = "2025-08-05T16:42:45.325Z" },
{ url = "https://files.pythonhosted.org/packages/98/a6/94b7213190e8077547ffae75e13ed05edc488653c85aa5c41472c297d295/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", size = 100470, upload-time = "2025-08-05T16:42:46.468Z" },
{ url = "https://files.pythonhosted.org/packages/e9/e9/78450d7cb921ede0cfc33426d3a8023a3bda755883c95c868ee36db8d48d/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", size = 103878, upload-time = "2025-08-05T16:42:47.576Z" },
{ url = "https://files.pythonhosted.org/packages/4f/e2/cd5439aad4f3e34ae1ee852025dc6aa8f67a82b97641e390bf7bd9891d3e/audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", size = 84867, upload-time = "2025-08-05T16:42:49.003Z" },
{ url = "https://files.pythonhosted.org/packages/68/4b/9d853e9076c43ebba0d411e8d2aa19061083349ac695a7d082540bad64d0/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", size = 90001, upload-time = "2025-08-05T16:42:50.038Z" },
{ url = "https://files.pythonhosted.org/packages/58/26/4bae7f9d2f116ed5593989d0e521d679b0d583973d203384679323d8fa85/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", size = 99046, upload-time = "2025-08-05T16:42:51.111Z" },
{ url = "https://files.pythonhosted.org/packages/b2/67/a9f4fb3e250dda9e9046f8866e9fa7d52664f8985e445c6b4ad6dfb55641/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", size = 84788, upload-time = "2025-08-05T16:42:52.198Z" },
{ url = "https://files.pythonhosted.org/packages/70/f7/3de86562db0121956148bcb0fe5b506615e3bcf6e63c4357a612b910765a/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", size = 94472, upload-time = "2025-08-05T16:42:53.59Z" },
{ url = "https://files.pythonhosted.org/packages/f1/32/fd772bf9078ae1001207d2df1eef3da05bea611a87dd0e8217989b2848fa/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", size = 92279, upload-time = "2025-08-05T16:42:54.632Z" },
{ url = "https://files.pythonhosted.org/packages/4f/41/affea7181592ab0ab560044632571a38edaf9130b84928177823fbf3176a/audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", size = 26568, upload-time = "2025-08-05T16:42:55.627Z" },
{ url = "https://files.pythonhosted.org/packages/28/2b/0372842877016641db8fc54d5c88596b542eec2f8f6c20a36fb6612bf9ee/audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", size = 30942, upload-time = "2025-08-05T16:42:56.674Z" },
{ url = "https://files.pythonhosted.org/packages/ee/ca/baf2b9cc7e96c179bb4a54f30fcd83e6ecb340031bde68f486403f943768/audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", size = 24603, upload-time = "2025-08-05T16:42:57.571Z" },
{ url = "https://files.pythonhosted.org/packages/5c/73/413b5a2804091e2c7d5def1d618e4837f1cb82464e230f827226278556b7/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", size = 47104, upload-time = "2025-08-05T16:42:58.518Z" },
{ url = "https://files.pythonhosted.org/packages/ae/8c/daa3308dc6593944410c2c68306a5e217f5c05b70a12e70228e7dd42dc5c/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", size = 27754, upload-time = "2025-08-05T16:43:00.132Z" },
{ url = "https://files.pythonhosted.org/packages/4e/86/c2e0f627168fcf61781a8f72cab06b228fe1da4b9fa4ab39cfb791b5836b/audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", size = 27332, upload-time = "2025-08-05T16:43:01.666Z" },
{ url = "https://files.pythonhosted.org/packages/c7/bd/35dce665255434f54e5307de39e31912a6f902d4572da7c37582809de14f/audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", size = 92396, upload-time = "2025-08-05T16:43:02.991Z" },
{ url = "https://files.pythonhosted.org/packages/2d/d2/deeb9f51def1437b3afa35aeb729d577c04bcd89394cb56f9239a9f50b6f/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", size = 91811, upload-time = "2025-08-05T16:43:04.096Z" },
{ url = "https://files.pythonhosted.org/packages/76/3b/09f8b35b227cee28cc8231e296a82759ed80c1a08e349811d69773c48426/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", size = 100483, upload-time = "2025-08-05T16:43:05.085Z" },
{ url = "https://files.pythonhosted.org/packages/0b/15/05b48a935cf3b130c248bfdbdea71ce6437f5394ee8533e0edd7cfd93d5e/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", size = 103885, upload-time = "2025-08-05T16:43:06.197Z" },
{ url = "https://files.pythonhosted.org/packages/83/80/186b7fce6d35b68d3d739f228dc31d60b3412105854edb975aa155a58339/audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", size = 84899, upload-time = "2025-08-05T16:43:07.291Z" },
{ url = "https://files.pythonhosted.org/packages/49/89/c78cc5ac6cb5828f17514fb12966e299c850bc885e80f8ad94e38d450886/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", size = 89998, upload-time = "2025-08-05T16:43:08.335Z" },
{ url = "https://files.pythonhosted.org/packages/4c/4b/6401888d0c010e586c2ca50fce4c903d70a6bb55928b16cfbdfd957a13da/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", size = 99046, upload-time = "2025-08-05T16:43:09.367Z" },
{ url = "https://files.pythonhosted.org/packages/de/f8/c874ca9bb447dae0e2ef2e231f6c4c2b0c39e31ae684d2420b0f9e97ee68/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", size = 84843, upload-time = "2025-08-05T16:43:10.749Z" },
{ url = "https://files.pythonhosted.org/packages/3e/c0/0323e66f3daebc13fd46b36b30c3be47e3fc4257eae44f1e77eb828c703f/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", size = 94490, upload-time = "2025-08-05T16:43:12.131Z" },
{ url = "https://files.pythonhosted.org/packages/98/6b/acc7734ac02d95ab791c10c3f17ffa3584ccb9ac5c18fd771c638ed6d1f5/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", size = 92297, upload-time = "2025-08-05T16:43:13.139Z" },
{ url = "https://files.pythonhosted.org/packages/13/c3/c3dc3f564ce6877ecd2a05f8d751b9b27a8c320c2533a98b0c86349778d0/audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", size = 27331, upload-time = "2025-08-05T16:43:14.19Z" },
{ url = "https://files.pythonhosted.org/packages/72/bb/b4608537e9ffcb86449091939d52d24a055216a36a8bf66b936af8c3e7ac/audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", size = 31697, upload-time = "2025-08-05T16:43:15.193Z" },
{ url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206, upload-time = "2025-08-05T16:43:16.444Z" },
]
[[package]]
name = "av"
version = "19.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/90/bc/a2a40e503250fe5d4174471911828f31658864eb69a8a7cb960c715e17b7/av-19.0.1.tar.gz", hash = "sha256:08674930eaf1af78a3ed8f93d3ba49383323b3a867e84349d9c399e36f7497da", size = 4274648, upload-time = "2026-10-03T01:48:28.575Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/2f/f4d219b2c72fea88bcbaea23de5b7f864ebecd348586fd2fe69f7f657147/av-19.0.1-cp312-abi3-macosx_11_0_x86_64.whl", hash = "sha256:2bd44ef4c09bb04aa6100d4c6191ddedaffef6af757ac55d5b4dc90915859299", size = 22625494, upload-time = "2026-10-03T01:47:21.866Z" },
{ url = "https://files.pythonhosted.org/packages/ff/75/db37bb43a12a317cc0c0b96ddabc7896f582503b377e0803d4d721969522/av-19.0.1-cp312-abi3-macosx_14_0_arm64.whl", hash = "sha256:29d85e4ee36bf8f475dad07d4f4417c07bba62535f6a7179429c357e0ca8fb0f", size = 18439188, upload-time = "2026-10-03T01:47:25.541Z" },
{ url = "https://files.pythonhosted.org/packages/10/4b/61f138fcf21e7bb50655ed21dd7fdc7a296baf72ea3c7ad8e89cb00b69c1/av-19.0.1-cp312-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:437d4c0d5a7d771f2c3af84cd28e6aac6e173851116c60b53e81dbf1eebe4eab", size = 32676941, upload-time = "2026-10-03T01:47:29.237Z" },
{ url = "https://files.pythonhosted.org/packages/c8/97/5fb45934ac64e8afc2c6869a7dcb8cb2af1ddab09a725367548856cbb59f/av-19.0.1-cp312-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1bea5b6134209305199bce7627ac3d33964de2cf2b09c77d08e7f67cf8bd4170", size = 34983451, upload-time = "2026-10-03T01:47:32.895Z" },
{ url = "https://files.pythonhosted.org/packages/66/f2/6eee1b99ac492fa1965d6fd466ef8b644ca296b4f1dfa8c8225ab340b139/av-19.0.1-cp312-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1de938ec0134ad88f795dfe0a2dfc2d59e9ecea39a20158d37961279a3483612", size = 41660680, upload-time = "2026-10-03T01:47:36.903Z" },
{ url = "https://files.pythonhosted.org/packages/11/be/e4ddd0197d02a3114402f3ffde541f6c4edecd24d670bea0da1eb6f15fb2/av-19.0.1-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:bcd0af218ecbeddbb1b0c56c4278043a3d97b87f3b8e33f6f92d452c744b1b08", size = 33748455, upload-time = "2026-10-03T01:47:40.541Z" },
{ url = "https://files.pythonhosted.org/packages/7a/41/b9af863f635f64abaf5eb734521306487fc79447f5d55d792339a81c8a4d/av-19.0.1-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:935a6b6386a6994964e324eb02af4dab01eedbcbbde23b4b21bf1dc59b004244", size = 36008899, upload-time = "2026-10-03T01:47:44.13Z" },
{ url = "https://files.pythonhosted.org/packages/e6/dc/a87a5a5e3ac462734f9befd8bad1447301e5802d8c111e22bf708fba7af3/av-19.0.1-cp312-abi3-win_amd64.whl", hash = "sha256:906fc3db09288319a75ea23ffefb59961c7dbe0d1c074601507a89de7d8593d8", size = 28149519, upload-time = "2026-10-03T01:47:47.372Z" },
{ url = "https://files.pythonhosted.org/packages/a5/78/16864f1aa2c3ac5017f15132b85c6d3c74bb85caca8c45ce836ad30dfe20/av-19.0.1-cp312-abi3-win_arm64.whl", hash = "sha256:e9e1b0cae6cebd2adc2c5c6691fc890112f8f6c846b76a9135307617db1e32e9", size = 20706822, upload-time = "2026-10-03T01:47:50.72Z" },
{ url = "https://files.pythonhosted.org/packages/78/4a/b5d7614856af72d7c18b926dda43bd227844b0b42d64e7c478b080f8d9c1/av-19.0.1-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:3ef376ab828730f50b635e3541f305503adad713cb4c3eadb5ad0e4c6a6f4a72", size = 22909764, upload-time = "2026-10-03T01:47:54.032Z" },
{ url = "https://files.pythonhosted.org/packages/b6/c9/50b2dedd4314a0ba0d78d7a7a52f7b073bc3377e5152e51d9d5627c5bcf4/av-19.0.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:17f2e42a1c969c78c616fe58bc69641a9df404c1ac2f01b50c1ddc22e5c31f69", size = 18718945, upload-time = "2026-10-03T01:47:58.396Z" },
{ url = "https://files.pythonhosted.org/packages/ef/a5/eb2b6aadbda16ee676c76e43012709f0cdfe09c35bc9ad4ffb5099827e72/av-19.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aafd294abd0e5c23e6c813b10fb4792cf1dd1002c1aead0292d195cda2ca154e", size = 36470355, upload-time = "2026-10-03T01:48:01.686Z" },
{ url = "https://files.pythonhosted.org/packages/c1/f0/25e7d21cc29e949118bdac6efe0ef5c5020fc4273a3ea237989728ebe816/av-19.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:400ba5234865dc370c442658efff0672c64dcad2de26a2a7c900abf16ffd9f68", size = 38457564, upload-time = "2026-10-03T01:48:05.61Z" },
{ url = "https://files.pythonhosted.org/packages/3f/09/77fec7c8de49fb815d55de1dfac21b39fb9e6915cbd8dcd945538ebb6f44/av-19.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:5e527b9d2d23c096d2b488e19a40ceba3654ea84a3cecee1c1b46c70ceaceae2", size = 43462245, upload-time = "2026-10-03T01:48:10.674Z" },
{ url = "https://files.pythonhosted.org/packages/8c/1d/bb0281ada4203c5d85f7e8b045de2cadc89c3b5d0ed5705298f7a9288b1f/av-19.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79136e62d4bc93db81fb63d6dd0060e86259426c071ca5157b1abe8c815c40b7", size = 37339005, upload-time = "2026-10-03T01:48:14.805Z" },
{ url = "https://files.pythonhosted.org/packages/0a/84/19a9d37d7546a3879d759a8957b2513a029cafb81f60218c496b1ce9d5a8/av-19.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:330f91c704aa822b96d9aa21382c0eb41a68531d388078d724d334faa460cbcc", size = 39466754, upload-time = "2026-10-03T01:48:18.988Z" },
{ url = "https://files.pythonhosted.org/packages/30/c4/39d4e2b778f1e86672671e25c3fd38e8d59d59b6f65c5cd13d7fae3d88a3/av-19.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8289295bfd2a438f2cf83c3ab426964055e441f1500410a842e7a767bdc8e51e", size = 29063526, upload-time = "2026-10-03T01:48:22.724Z" },
{ url = "https://files.pythonhosted.org/packages/f4/7d/a20ff44c1445c09a93985418f6997e5823635848e955a7953339636a9829/av-19.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:e1f70b1bda35588aff5fc526500376afe143e33cfce5d7e30d368170c38717db", size = 21915698, upload-time = "2026-10-03T01:48:26.386Z" },
]
[[package]]
name = "beautifulsoup4"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "soupsieve" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" },
]
[[package]]
name = "click"
version = "8.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" },
]
[[package]]
name = "cloup"
version = "4.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
]
sdist = { url = "https://files.pythonhosted.org/packages/05/e2/d41446c6195eff0db3b671ddb202e39f42f9ea7c0dd15cd43fbf5cf0d7f7/cloup-4.0.0.tar.gz", hash = "sha256:83b0870ee863bcc85129e40e1b208bcfdebe4cd2142e9ce1d0daf7d276cab038", size = 252135, upload-time = "2026-09-12T03:23:39.698Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/34/20224fd9d52e8ce7e3b319b5d6b7126da25e89d00cd1a5692b4b46ed2d26/cloup-4.0.0-py3-none-any.whl", hash = "sha256:0678744a3a2753e1d78dd3ea5c6e4cbbc3b41561fd7a7a7d075dbc8dad74f74f", size = 55776, upload-time = "2026-09-12T03:23:38.564Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "cython"
version = "3.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a9/d8/4981ef716ad0e3ff0d3ef383aefc6b03c4a88dee33b272bf8e0d833001ca/cython-3.3.0.tar.gz", hash = "sha256:eed0d93fbca7087f143b42c34b05a825849bdf17f101572c2105acfa49aa88b8", size = 3727515, upload-time = "2026-08-22T05:16:39.493Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/da/0f/95bad838a80ac52c9e982dad00bd9a0b2bad57fb4c688e5f53ac3ef65ff0/cython-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03bc5333932f5dda3ba9315298ecdd21daa1b58410bb1f8ce04c78ec8337130a", size = 3143472, upload-time = "2026-08-22T05:17:07.956Z" },
{ url = "https://files.pythonhosted.org/packages/2f/cc/abc977cf683140e372714acea42164ecfc5cd3d3984ed025860e6d830ee4/cython-3.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:03056533fe4fdbc4f1d34a39178f9a4937ff35196f8bcdde2a67b5b5809c61fe", size = 3134892, upload-time = "2026-08-22T05:17:16.675Z" },
{ url = "https://files.pythonhosted.org/packages/8a/7f/e409f76bb955ecdcb746b80350b945fbb808846d797346d647a37e1790ca/cython-3.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0deedc2e9a5a664e1adfa4c2d310aa7b54903e1a647c274b6c9213f77a02d637", size = 3186700, upload-time = "2026-08-22T05:17:24.288Z" },
{ url = "https://files.pythonhosted.org/packages/ea/45/7f6988070013e16918e39b1b3dab9c5f2c8e404253a7fd10ee685bbd6902/cython-3.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:596e8df019372a2cd417805015022d42cb8ee4e1803ccdc11ed00e451625fb66", size = 3177681, upload-time = "2026-08-22T05:17:32.523Z" },
{ url = "https://files.pythonhosted.org/packages/14/59/bc1a84b434cb5bebb0cd6f50da8f239d35a5c141b20fdeafc2817fd87778/cython-3.3.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:e0d2713d2b292c826bc21dc8732bd9e47628103aa3764180c881e04b3fef95dc", size = 3063660, upload-time = "2026-08-22T05:17:40.923Z" },
{ url = "https://files.pythonhosted.org/packages/bf/77/67b0b24e45073a699610e50f00c18474ff9b09ea29ecc95083bdf5e60acd/cython-3.3.0-py3-none-any.whl", hash = "sha256:9b24b5c8cd536946b62086fcafee6d5509d3f549f72d553d2336af87ffbe0da1", size = 1349151, upload-time = "2026-08-22T05:16:36.741Z" },
]
[[package]]
name = "decorator"
version = "5.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" },
]
[[package]]
name = "glcontext"
version = "3.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5f/99/e877a962bfa5783f6c01116483471f0666b53d5f805ea0ebdbb0c66d159b/glcontext-3.1.0.tar.gz", hash = "sha256:754d33f73aaf07e016e503b722e44c6ef914d9f101643be524cb185e1d55717f", size = 16536, upload-time = "2026-10-07T10:22:51.302Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/ed/d8494c981c9f84736d887a64440e205f51aaa528e286dd6c94fc608ef913/glcontext-3.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68b37321eee368ba878f2dcef14222e9383e94f3454493ad70ac0b98201a1cb4", size = 9933, upload-time = "2026-10-07T10:23:12.31Z" },
{ url = "https://files.pythonhosted.org/packages/81/24/7bdeab738393d80adbadc5df8d04737e5c0e1b86dab38200d1173dabf456/glcontext-3.1.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1f5678b1dc331f13b50f81a9fa09d5abc52f4be736920e12abe3f8078f2213dd", size = 47463, upload-time = "2026-10-07T10:23:13.944Z" },
{ url = "https://files.pythonhosted.org/packages/c2/47/1dd230080356f5d648bcba45c007854bfc40f2066d64185b9dd018aa619b/glcontext-3.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:efab9360c45e2e31b97a492a3e71e9f666cacbb4c90f0db3004c8a28c633cefa", size = 46562, upload-time = "2026-10-07T10:23:15.379Z" },
{ url = "https://files.pythonhosted.org/packages/6e/86/80ca4fb5165a5a0ec1440db9fb214571493ece173065b2ef7cb717d4af5b/glcontext-3.1.0-cp312-cp312-win32.whl", hash = "sha256:d41e942ff263e5c0dab5447094b0342af23c2b8e5f9c03a8c4153dc1dde55ea4", size = 11799, upload-time = "2026-10-07T10:23:16.606Z" },
{ url = "https://files.pythonhosted.org/packages/61/f0/09054b47c443da4425932aea76167a89ee0b0b321f36bc46c182630403a0/glcontext-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ce2efe49e41b11aa4c621e636adf493a3fa19157b2b0be1d2d73312f4a396f25", size = 12604, upload-time = "2026-10-07T10:23:17.737Z" },
{ url = "https://files.pythonhosted.org/packages/e7/7a/da27321e7929cc5a803c570a238850a00e8d41fd7a9b635ed5185d6c68c6/glcontext-3.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:575b4c91093d07af0a95e0fa1432c3f3e5e5a6b023e3515db391314ebd9fea07", size = 9936, upload-time = "2026-10-07T10:23:18.86Z" },
{ url = "https://files.pythonhosted.org/packages/81/be/1beaf74335b509f450e721b9df7a1e6d9c772672f5ff5cba3ccfae2d9f10/glcontext-3.1.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eae7b6e07d11220672514cc0cd4ab4067685614796b459e4047838384c84c1fd", size = 53008, upload-time = "2026-10-07T10:23:20.19Z" },
{ url = "https://files.pythonhosted.org/packages/45/63/15808833a732ed210cb4a8cd37de8c4363aac1ca2ea93fec8d7ecee456f5/glcontext-3.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a4bbf6b785c111ddec97ef17ee829e6beabd27d9aca0478e33d2baf99dc3b2d", size = 46398, upload-time = "2026-10-07T10:23:21.829Z" },
{ url = "https://files.pythonhosted.org/packages/d6/b4/8d8ae1e2bb8236d664d337615bb7db1e90fa3d2fa221d020d970ee07c438/glcontext-3.1.0-cp313-cp313-win32.whl", hash = "sha256:26972317f03270d278a98bd7ad756e8950e72f28ab854e9ccdc79553c3f0063e", size = 11800, upload-time = "2026-10-07T10:23:23.058Z" },
{ url = "https://files.pythonhosted.org/packages/ec/ea/9cb78c8ea5171c1af8c4a9e39a354e6ebbe775068a37644e3e6d6ccedee0/glcontext-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:6e8817e66221a9c2a7a22147307a0b1305bfe1ce860832464a06a8c08d0d1f61", size = 12606, upload-time = "2026-10-07T10:23:24.184Z" },
{ url = "https://files.pythonhosted.org/packages/92/16/9bfbb8ff8c2c905442299f967cf09e9b806f7abaf2add6f3ade2b89e677c/glcontext-3.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:88633a25a6b709fe48c4dd5f89953732a77f89e3576e4eb33b81405849f6e429", size = 9931, upload-time = "2026-10-07T10:23:25.313Z" },
{ url = "https://files.pythonhosted.org/packages/14/39/f726f1ea2927175f9fbb5f6fae8d3f487e2af824f6e167c03ff1f4734e56/glcontext-3.1.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3a96f4424212e79358a105b4869faa0818bd18f36a55155cf67f01e9f37341d5", size = 53107, upload-time = "2026-10-07T10:23:27.051Z" },
{ url = "https://files.pythonhosted.org/packages/d1/55/01767c0f9bd107a17af5a64c6f067aabfa9c49b3e1d8e4eadfeaa29af1e1/glcontext-3.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c0de0b3bb4d39945dcc6d38b1032a23c81803f29a0633a2b4aea953829efd8d4", size = 46533, upload-time = "2026-10-07T10:23:28.487Z" },
{ url = "https://files.pythonhosted.org/packages/0c/b4/99706c0eddd0cd9fa46c8e8dae97e89f8f458f714976221651fa2e913ce1/glcontext-3.1.0-cp314-cp314-win32.whl", hash = "sha256:e1562a50303e9eb6acebec058dc23d72a67d1c2888760a55603d220ce9bab511", size = 11990, upload-time = "2026-10-07T10:23:35.756Z" },
{ url = "https://files.pythonhosted.org/packages/79/13/7b5fa7f75e9b05b6d15614bf4424211233b2274435e0adda265daab78058/glcontext-3.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:ec2f1678fe91b788569b02fdd58e9309e372f176b1acd606ba0bc383c1043f40", size = 12848, upload-time = "2026-10-07T10:23:36.882Z" },
{ url = "https://files.pythonhosted.org/packages/e5/2f/ee748973abaab8cb140cec5f25ad7879d798e4d9aca30709cf942cbb4b83/glcontext-3.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:77bcdf2037981d485b00cc25667545b5766c1841d358da0ee6697572a376a9c1", size = 10032, upload-time = "2026-10-07T10:23:30.021Z" },
{ url = "https://files.pythonhosted.org/packages/d5/1c/2cb3997751767a6dd73642b4f57901b8fbfdf07b9d9da797df5d6724199b/glcontext-3.1.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a2898591b8e848fc307ce197c22a1ea4c135144831fd555a74dc8ff0b88d6b8b", size = 53226, upload-time = "2026-10-07T10:23:31.252Z" },
{ url = "https://files.pythonhosted.org/packages/47/ec/f47263f7ff49409dd829afd6a223e289258169a9eea9dfe5411b5ce97d27/glcontext-3.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:047c4ebfb75f468e52eb043230dc41195aa91264f9aeb117e5aa7e8fb452824e", size = 46624, upload-time = "2026-10-07T10:23:32.581Z" },
{ url = "https://files.pythonhosted.org/packages/ae/70/bc48d64200c307f3b7d00804728d5d904e20f9a00dd0fc6472668ebe1976/glcontext-3.1.0-cp314-cp314t-win32.whl", hash = "sha256:f16c99dd7a3c39650c526e1910c724288e54ef95770074383df0644f87f5a386", size = 11995, upload-time = "2026-10-07T10:23:33.709Z" },
{ url = "https://files.pythonhosted.org/packages/20/b2/6a168a80209a24e6b1b763d1d8be84218ac9c3110a5a80b0a0792751d512/glcontext-3.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:fb27b52bf8f317c0447f955105884aeea5532356e924e66f9b52cbba233cd804", size = 12830, upload-time = "2026-10-07T10:23:34.731Z" },
{ url = "https://files.pythonhosted.org/packages/c2/b6/5d6d4387c35864eeb49d97ff7297233673354cb5af00d9b1a34d9d68addd/glcontext-3.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6b01e5b2482eac21fbd674e4ebf96262ea059545ad602aa154fcec3b4aa39641", size = 9938, upload-time = "2026-10-07T10:23:38.008Z" },
{ url = "https://files.pythonhosted.org/packages/c6/e9/c9525daf111e8dbe2a4b7e56ed8270178aee402081aff5648c1bb2acc077/glcontext-3.1.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:589d3e50d370e24c2c731dab6ce0d42d4dfb4de303cc37151a52c3df7bf8b5b0", size = 53452, upload-time = "2026-10-07T10:23:39.342Z" },
{ url = "https://files.pythonhosted.org/packages/12/f9/5bbe4b0ab156a0d9db3d00c8627bef5a7188be1a4faebf87f5cfbc272a57/glcontext-3.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:03bdfdecb4abd74cba4d0c6595e0de200a507a1179e63df39460bc27dd681ff6", size = 46819, upload-time = "2026-10-07T10:23:40.775Z" },
{ url = "https://files.pythonhosted.org/packages/79/b5/4f5ffdf84876dc9c85f909e6f8950b97edd1b54b676321c88102d38fde5a/glcontext-3.1.0-cp315-cp315-win32.whl", hash = "sha256:4c82d05ca71647d0c036e97e0cb16daeb9154622e30e2c873538641aff73aa77", size = 11984, upload-time = "2026-10-07T10:23:48.454Z" },
{ url = "https://files.pythonhosted.org/packages/d3/7b/4ed407507b0d68345812c10697650d942cb81d8607856491a1b6bafafb5a/glcontext-3.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:f3d8ba603673a7c42d59a732877fa3ecadb57fb210d2e87a24b6f2e9fce2b943", size = 12847, upload-time = "2026-10-07T10:23:49.477Z" },
{ url = "https://files.pythonhosted.org/packages/fc/f4/b224b17d958fc6030c10598d9e9475e33bdfbead151a9c1aea7a2683a816/glcontext-3.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:623627f2fe95241a8f581adc28650025d5f46354a2b95880d7b2f9fd9b5de77a", size = 10034, upload-time = "2026-10-07T10:23:41.9Z" },
{ url = "https://files.pythonhosted.org/packages/f1/a0/7b407e54024ccd271a6e4d9537d51d016d745d404ca952f340471265b8e8/glcontext-3.1.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4fd997bba95d908a01ddc62aed1f45360832d641783c2001391ddb2ea44b3d3", size = 53519, upload-time = "2026-10-07T10:23:43.232Z" },
{ url = "https://files.pythonhosted.org/packages/3f/51/9609adff8384d9711452fb84ef5f66cd1721c5110411c24fd06f916a0da8/glcontext-3.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f3eefb3a05d7ae1a1fe3a7fac5856f69a5eb72f1cce501a11d5edcdb52cb38d5", size = 46908, upload-time = "2026-10-07T10:23:44.872Z" },
{ url = "https://files.pythonhosted.org/packages/93/c0/d9acfa3a6a003ad1bf7d05e502672be592a76ed4c856fa7ca7b08e09fe19/glcontext-3.1.0-cp315-cp315t-win32.whl", hash = "sha256:394176ea4c3c1ff56882f13b886e5ea22add3843abf7fad6e8ad42a3df053424", size = 11990, upload-time = "2026-10-07T10:23:46.304Z" },
{ url = "https://files.pythonhosted.org/packages/50/2a/55ab96fda4ca184ef0b9c5cbef5ee75be119be8a1eb87c735c98de27bbe7/glcontext-3.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f50646ea59fb3868db41ae377fb5ad2d0fcb98c97a4b3af97d2ed6f9ff8bf639", size = 12827, upload-time = "2026-10-07T10:23:47.329Z" },
]
[[package]]
name = "imageio-ffmpeg"
version = "0.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/44/bd/c3343c721f2a1b0c9fc71c1aebf1966a3b7f08c2eea8ed5437a2865611d6/imageio_ffmpeg-0.6.0.tar.gz", hash = "sha256:e2556bed8e005564a9f925bb7afa4002d82770d6b08825078b7697ab88ba1755", size = 25210, upload-time = "2025-01-16T21:34:32.747Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/da/58/87ef68ac83f4c7690961bce288fd8e382bc5f1513860fc7f90a9c1c1c6bf/imageio_ffmpeg-0.6.0-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.whl", hash = "sha256:9d2baaf867088508d4a3458e61eeb30e945c4ad8016025545f66c4b5aaef0a61", size = 24932969, upload-time = "2025-01-16T21:34:20.464Z" },
{ url = "https://files.pythonhosted.org/packages/40/5c/f3d8a657d362cc93b81aab8feda487317da5b5d31c0e1fdfd5e986e55d17/imageio_ffmpeg-0.6.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b1ae3173414b5fc5f538a726c4e48ea97edc0d2cdc11f103afee655c463fa742", size = 21113891, upload-time = "2025-01-16T21:34:00.277Z" },
{ url = "https://files.pythonhosted.org/packages/33/e7/1925bfbc563c39c1d2e82501d8372734a5c725e53ac3b31b4c2d081e895b/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1d47bebd83d2c5fc770720d211855f208af8a596c82d17730aa51e815cdee6dc", size = 25632706, upload-time = "2025-01-16T21:33:53.475Z" },
{ url = "https://files.pythonhosted.org/packages/a0/2d/43c8522a2038e9d0e7dbdf3a61195ecc31ca576fb1527a528c877e87d973/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c7e46fcec401dd990405049d2e2f475e2b397779df2519b544b8aab515195282", size = 29498237, upload-time = "2025-01-16T21:34:13.726Z" },
{ url = "https://files.pythonhosted.org/packages/a0/13/59da54728351883c3c1d9fca1710ab8eee82c7beba585df8f25ca925f08f/imageio_ffmpeg-0.6.0-py3-none-win32.whl", hash = "sha256:196faa79366b4a82f95c0f4053191d2013f4714a715780f0ad2a68ff37483cc2", size = 19652251, upload-time = "2025-01-16T21:34:06.812Z" },
{ url = "https://files.pythonhosted.org/packages/2c/c6/fa760e12a2483469e2bf5058c5faff664acf66cadb4df2ad6205b016a73d/imageio_ffmpeg-0.6.0-py3-none-win_amd64.whl", hash = "sha256:02fa47c83703c37df6bfe4896aab339013f62bf02c5ebf2dce6da56af04ffc0a", size = 31246824, upload-time = "2025-01-16T21:34:28.6Z" },
]
[[package]]
name = "isosurfaces"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/da/cf/bd7e70bb7b8dfd77afdc79aba8d83afd4a9263f045861cd4ddd34b7f6a12/isosurfaces-0.1.2.tar.gz", hash = "sha256:fa51ebe864ea9355b26830e27fdd6a41d5a58b419fa8d4b47e3b8b80718d6e21", size = 11348, upload-time = "2024-02-26T00:20:52.066Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/68/d5e9e6e0d6e43107d8393d2ee3d231dbb597bf93052c6f3117b313724980/isosurfaces-0.1.2-py3-none-any.whl", hash = "sha256:525a49ba93f4dbc35303cd2faf30976af0f99d9274cfa2787aec016b8ef96c64", size = 11649, upload-time = "2024-02-26T00:20:41.308Z" },
]
[[package]]
name = "manim"
version = "0.22.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "audioop-lts", marker = "python_full_version >= '3.13'" },
{ name = "av" },
{ name = "beautifulsoup4" },
{ name = "click" },
{ name = "cloup" },
{ name = "decorator" },
{ name = "isosurfaces" },
{ name = "manimpango" },
{ name = "mapbox-earcut" },
{ name = "moderngl" },
{ name = "moderngl-window" },
{ name = "networkx" },
{ name = "numpy" },
{ name = "pillow" },
{ name = "pycairo" },
{ name = "pydub" },
{ name = "pygments" },
{ name = "rich" },
{ name = "scipy" },
{ name = "screeninfo" },
{ name = "skia-pathops" },
{ name = "srt" },
{ name = "svgelements" },
{ name = "tqdm" },
{ name = "typing-extensions" },
{ name = "watchdog" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/fa/cef49e8c5c5617232ae8324a8ad14d8d724eb308dc898687240daca2800b/manim-0.22.0.tar.gz", hash = "sha256:e227aef6dab218c7fe68fa31ed1ff418e1c86a450f43509ed2630f3ffd06602d", size = 599668, upload-time = "2026-10-08T13:27:34.513Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/05/35fc67b65a8ecbe38de39b9747adabc18de696683e0e27b5e121ad6d05ae/manim-0.22.0-py3-none-any.whl", hash = "sha256:bce56dbe420ce002cb77076c0b4e041637a0bd41ce5749e837e96375937e935f", size = 700688, upload-time = "2026-10-08T13:27:32.887Z" },
]
[[package]]
name = "manimpango"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/88/35/30bfde8a5fbb82392755d078e41d3f43b686fa2feb4f8c25c12435e23892/manimpango-0.7.0.tar.gz", hash = "sha256:64330c360f5040ed356b9eb289d52e8ae8cbc3d9c39297c5e77522e1b417f1c7", size = 4081389, upload-time = "2026-10-01T17:23:56.764Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/ed/5e969f9089527df2d4ae8417cd9d2c2bab75b5a0695cd768d8dab29a5fac/manimpango-0.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3312eea20bfdec642ea7f5fe4dd0fb858ffacd79b068d962064be111c9653028", size = 8892699, upload-time = "2026-10-01T17:23:09.582Z" },
{ url = "https://files.pythonhosted.org/packages/c8/ef/a1c72c324c7955ca08cb1dfa30b2b4be4f1da44d860f26345d4a78eb7a80/manimpango-0.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38c41068eff3468b23fd85857debc932f775adb721ed78a7f7cb5ea91ee49842", size = 8383946, upload-time = "2026-10-01T17:23:12.7Z" },
{ url = "https://files.pythonhosted.org/packages/5b/75/5738da821d36d75277d07c0d549ab662f6b2a057ee622d7dc024f540fcbd/manimpango-0.7.0-cp312-cp312-win32.whl", hash = "sha256:f3883f92a43754adcfe6af4aa7ca64e051db57c2026e0683dce39549a67a11a2", size = 5653300, upload-time = "2026-10-01T17:23:15.389Z" },
{ url = "https://files.pythonhosted.org/packages/b2/f9/e926d644eb320ff0f697188f3b660909a177648869506617878690f6fb1d/manimpango-0.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:7bf6b1c468f1910ae8f2d68c62674b14c40e059c337b167147cd3fa38e2b1e17", size = 6384848, upload-time = "2026-10-01T17:23:17.887Z" },
{ url = "https://files.pythonhosted.org/packages/19/d5/2212ba7c1ca9c1b918080603db538d0d5c055951cbd21f9215c827a75597/manimpango-0.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8ba455701a45e77b7f77ebe188d15364584970075b224ccff9a43098e43c4fb", size = 8890810, upload-time = "2026-10-01T17:23:20.662Z" },
{ url = "https://files.pythonhosted.org/packages/d4/de/b909715c166f14d2a1f6b7f514c6cc50013b6be703cf303458609c7dd167/manimpango-0.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:58a9295461b2940d8769f3df5200870cd397caa7cf5f9a6f3774ef5313be0b80", size = 8381553, upload-time = "2026-10-01T17:23:24.56Z" },
{ url = "https://files.pythonhosted.org/packages/a2/9b/c8b0d0b9f14bdc4947b84bd63dfd0535026b407637153796f5def86f3a6e/manimpango-0.7.0-cp313-cp313-win32.whl", hash = "sha256:467de4337ab051063f2422ceac5234df3c0bddd0e8bd5efb8907f380ddeb2f77", size = 5659242, upload-time = "2026-10-01T17:23:27.371Z" },
{ url = "https://files.pythonhosted.org/packages/b2/6d/259612e345058d3c7183fc9190ed9f64f2a6f80fffda3806fb56338d6894/manimpango-0.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:698324da00edb76fe918f66097a5c93eab54fcb63573d19b3229f9237f5a91aa", size = 6393717, upload-time = "2026-10-01T17:23:30.289Z" },
{ url = "https://files.pythonhosted.org/packages/a7/48/579caa5114e3da3b0de3fcbeb4baf30a939e75486b3a50b49de7d93bd1ad/manimpango-0.7.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2b07ce20c4f24a81d4f00c1f5c9b6c551cf0e0b6ebb9c816e86c432242fc99a7", size = 8890750, upload-time = "2026-10-01T17:23:33.415Z" },
{ url = "https://files.pythonhosted.org/packages/b4/6e/17fe330181de9da3051276c79034a2f3c0154e1ce9a110eea9dd7cec608c/manimpango-0.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6163074c608ea5c7a19b7ad1ad44aeab7c649bfeef6de3c57ad4669baf06846f", size = 8381733, upload-time = "2026-10-01T17:23:36.674Z" },
{ url = "https://files.pythonhosted.org/packages/7b/02/fc0839dc0bd03f7e2b5760b0cc3d7adb5817ea1e7e3a58bc2a3b66ea121c/manimpango-0.7.0-cp314-cp314-win32.whl", hash = "sha256:be79a9c255103f63c5d405b96b041ff626b1e8b56e2f0321a0ce0b68415d3626", size = 5851076, upload-time = "2026-10-01T17:23:39.665Z" },
{ url = "https://files.pythonhosted.org/packages/25/2c/b1e41adc60a96f4625d090c8481b164b1beea8f358b23244624bbb419cb6/manimpango-0.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:9332f2fc2da291c396391e4eec3614f7ae53b217b8410798de60f5bfaf654f13", size = 6591563, upload-time = "2026-10-01T17:23:42.393Z" },
{ url = "https://files.pythonhosted.org/packages/93/ac/841d1bb8230e27ce5c8c53517aaf0b2c49eed5c2ce329c7bcf47e233679a/manimpango-0.7.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:3bf7b818af739ad5d8d81ee486ae4446bffb94e2f8a4d128f1285f82854d5a9e", size = 8890414, upload-time = "2026-10-01T17:23:45.569Z" },
{ url = "https://files.pythonhosted.org/packages/00/7f/98b36de8b476e015df79cdb053d175dcfaab1cc921d00b2ee84286bc7c96/manimpango-0.7.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:8bd86ecb0d2c30ffa683036c98872e879bb5df4817e7c7e1bf076b43f1855d8b", size = 8381244, upload-time = "2026-10-01T17:23:48.94Z" },
{ url = "https://files.pythonhosted.org/packages/48/99/9549c07e9d38458ff8bf7ff3dfedef1e77bdb78ec0caafda7f467bfc3ed4/manimpango-0.7.0-cp315-cp315-win32.whl", hash = "sha256:26a1d5cde87b619d43c01ce17d620b3d0d85b227c6ccb5ccb6a5d160200ed9ba", size = 5850127, upload-time = "2026-10-01T17:23:51.722Z" },
{ url = "https://files.pythonhosted.org/packages/01/f6/b163d85db3fcbb4d4016013cd5c29dda992e0437f998b25b87be39b24540/manimpango-0.7.0-cp315-cp315-win_amd64.whl", hash = "sha256:887afc616ecadf77936e9975a923e220e22fa2100a880170bdb98f9c0bf3c49a", size = 6595290, upload-time = "2026-10-01T17:23:54.262Z" },
]
[[package]]
name = "mapbox-earcut"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/aa/14/8505f246191c41da54a64a4d6c30049ed47b6047930d8f0de328dadb06ee/mapbox_earcut-2.1.0.tar.gz", hash = "sha256:fb0c1e43581a635bbaabf4a5149009014d370c515816c4d5a7d8118477f28d48", size = 46744, upload-time = "2026-09-08T19:01:06.749Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/d0/74534b0a7df17340cb2ab2c959909ec7005eaaf8422da646565205b04664/mapbox_earcut-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba3e72cbe58c564acc4e07458da0da0cbf3e034705a093cdf6213815793cf566", size = 69401, upload-time = "2026-09-08T18:59:41.448Z" },
{ url = "https://files.pythonhosted.org/packages/67/3e/d93e98c76afad5075151d6e19a8446092a53fd42823f4734138dbc74fd5b/mapbox_earcut-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4e392807f3e25f268826ece0f32739f0410ac46eebf70f06e701950297292d7c", size = 66308, upload-time = "2026-09-08T18:59:42.656Z" },
{ url = "https://files.pythonhosted.org/packages/52/4f/b886e77d14f9807abccb2eb0e4752400746fa16cc6a2c9197e88385332ca/mapbox_earcut-2.1.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f6181c097afb5de2a7160424cdb0660510bf8689ed4585a3f61f3f52596c18b", size = 73034, upload-time = "2026-09-08T18:59:43.941Z" },
{ url = "https://files.pythonhosted.org/packages/9d/d0/9557f5d60e21c434cf87d70fe056899eb6b7ee8cf7755c556e06c77c8dab/mapbox_earcut-2.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5592b56bf6ba66c1d360edf019c7708c8ec9ab6b71b336e457f4660a077676ed", size = 76846, upload-time = "2026-09-08T18:59:45.248Z" },
{ url = "https://files.pythonhosted.org/packages/29/71/6ea9a0bf249782684b241c58a7fdf58c07aa760233a5c0d4366d10f2c25e/mapbox_earcut-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c0875192aed99a58e438abbcd5c096517c7947002a5f442440e2a7673f20cc0d", size = 168787, upload-time = "2026-09-08T18:59:46.708Z" },
{ url = "https://files.pythonhosted.org/packages/be/d5/8cbe09f35d6dedccfc90d7823bbdf7782e0665134dd5abd53d18e2ad1bc7/mapbox_earcut-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ace5e2f5f53d742fffdb795c66e8387b0cca04ecf11588fbb8ab14150f30f214", size = 174324, upload-time = "2026-09-08T18:59:48.087Z" },
{ url = "https://files.pythonhosted.org/packages/9d/c1/7c9453cb4e0cf19184da13324d96f349e5a180dc78da12dda886dce894a6/mapbox_earcut-2.1.0-cp312-cp312-win32.whl", hash = "sha256:c87bf3755011fde95713c5b655c67b144e23e218e65efed2b7ec44f0188b3af9", size = 272880, upload-time = "2026-09-08T18:59:49.478Z" },
{ url = "https://files.pythonhosted.org/packages/ba/58/b1021f9065d315a5d325a913cc76c996a1629fb032df9c3fa5c9939bf692/mapbox_earcut-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:eeea00eeb70873d840290e98471671d02e4ac0536b847b785177a14001e33a95", size = 279405, upload-time = "2026-09-08T18:59:50.936Z" },
{ url = "https://files.pythonhosted.org/packages/fb/95/f9b0ecb85c62768b2827ab50fcda3228e5f133c0e11aa9dc4759c6f4c1dd/mapbox_earcut-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:d48c8abdf0268903689335f4b609022b588b84d98a455c2a2cb0d6bacd5bad50", size = 448075, upload-time = "2026-09-08T18:59:52.76Z" },
{ url = "https://files.pythonhosted.org/packages/c9/8d/9225758660ddf74fa757a044efdd40bc3e9ad509bc012c0efd58082012dc/mapbox_earcut-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fe652ace9a050b9f6447e9d7a9f2465fab951547e295ba1de7f2404f8bdf326f", size = 69339, upload-time = "2026-09-08T18:59:54.207Z" },
{ url = "https://files.pythonhosted.org/packages/b1/c7/34fc86374e487adeae197f42883664abe72c48663e9d106b6b431645d053/mapbox_earcut-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:01e1df4c841ebdccce44e07046e6d23f307019957f69751439da744f7b09a262", size = 66326, upload-time = "2026-09-08T18:59:55.603Z" },
{ url = "https://files.pythonhosted.org/packages/38/24/edf60cbcdf8327e18279dce6963496542dfefbf3c49641a8a68e753b9a6d/mapbox_earcut-2.1.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:611a6fc362c6a24cd7fc230e2740300e60bc5360e6085e36b40c5776eaf156a5", size = 72924, upload-time = "2026-09-08T18:59:56.854Z" },
{ url = "https://files.pythonhosted.org/packages/fe/a1/841d702350b644808fc9c4793910045a9fb63a387aabe132de225d0a07cc/mapbox_earcut-2.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cee9422275dd8700bb04f82eac3410d03c283630a1cc536b1a09cc721a5c0a1", size = 76712, upload-time = "2026-09-08T18:59:58.146Z" },
{ url = "https://files.pythonhosted.org/packages/47/ca/349241fb7971173f2bb7f626396b2d504c7ed3fc1f46f19c18769865fc94/mapbox_earcut-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d2d87ecc9a00faafb7a0e6c0179bc61e54f79c23f44c33269605475d5eeceb07", size = 168702, upload-time = "2026-09-08T18:59:59.439Z" },
{ url = "https://files.pythonhosted.org/packages/38/45/1a43da1002787a1b4dcc3740d0216d60239cf92a878ac6cc1ca8d2d4d089/mapbox_earcut-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:086cf89caf061da1669add4f92aa76e5e90a232edc7f82445df362334fd4c3f8", size = 174200, upload-time = "2026-09-08T19:00:01.047Z" },
{ url = "https://files.pythonhosted.org/packages/e2/65/c97fabf109c2e381611e20a9657a4097d90dcbd82abb2e183e0247a81a8f/mapbox_earcut-2.1.0-cp313-cp313-win32.whl", hash = "sha256:fed97aaa1cd846c1a24867a43fe0a6603e84b25543cec9f58aced65a8c5c723f", size = 272923, upload-time = "2026-09-08T19:00:02.618Z" },
{ url = "https://files.pythonhosted.org/packages/60/e1/dc52f1b18347c29742ff1a82c0f88553c8fac71fdc541cabe553ddead1c4/mapbox_earcut-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:0224b889f67b2868b4cf4dc3ba7dd7006122ece2199159147edce80e05df079c", size = 279382, upload-time = "2026-09-08T19:00:04.145Z" },
{ url = "https://files.pythonhosted.org/packages/e9/45/cc48844e03f595b535facd5f5b481db05f98e017b3858fab96c639b5da50/mapbox_earcut-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:7b3441c188704bd03ca7c320c444378eda529ef494220b9b735389b87765a38e", size = 448060, upload-time = "2026-09-08T19:00:05.728Z" },
{ url = "https://files.pythonhosted.org/packages/56/f5/d927a6c18e628fe76ee6d5be1991a9ed55f07b8d608f37acd78aa9bf0337/mapbox_earcut-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:01754511b3b2adf9068a0aec595f6b3c2ef91e101942f0805153a555bfb4226b", size = 69484, upload-time = "2026-09-08T19:00:07.089Z" },
{ url = "https://files.pythonhosted.org/packages/4c/b7/7a558c3a36ec6818632dce1c4e45ea4bac764b4e484a9f15a44e33116c9a/mapbox_earcut-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8e2d94b2a5ff27082975c57372fc6c966111482b82f42c322dbae22a11737c78", size = 66389, upload-time = "2026-09-08T19:00:08.467Z" },
{ url = "https://files.pythonhosted.org/packages/44/d1/499e87fc1b630ef2d1ac3083587258d0dea4e5e7b1048e73d9d9770f133d/mapbox_earcut-2.1.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b51988c17538fb98dd18a45656ea1d54c30b4d384eabd20eb8ad0a142116495f", size = 73010, upload-time = "2026-09-08T19:00:09.732Z" },
{ url = "https://files.pythonhosted.org/packages/86/4f/3655c691732f14cc829fa45a4629085a7ada9e1bb65caf231e6ce982e9d4/mapbox_earcut-2.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:db56b69aace164fb82884f9ca36864ea3d6e8ec643defebec83541dddf41be32", size = 76774, upload-time = "2026-09-08T19:00:10.94Z" },
{ url = "https://files.pythonhosted.org/packages/60/12/9ea657f8117c3c0d6e0d96218395c57f0b289654955f90405d7981e5b816/mapbox_earcut-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0b300e634ca98f44bd7300d06f177197e5872f63675a4003ea1857d16f364571", size = 168768, upload-time = "2026-09-08T19:00:12.479Z" },
{ url = "https://files.pythonhosted.org/packages/ee/ba/3be16d09fcb5277cb1f27a65566584d0bac33664aa3e74f20b224cfda762/mapbox_earcut-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5488bdd616a4ab9b8ec2b177f9d94ca33243020209fb5025ca0c6d19fd043a76", size = 174279, upload-time = "2026-09-08T19:00:14.256Z" },
{ url = "https://files.pythonhosted.org/packages/4e/1e/7c3e2cdf47c70526376769dd981ed0e608e44a9772e1edd213e3f211e31b/mapbox_earcut-2.1.0-cp314-cp314-win32.whl", hash = "sha256:ef8e993d04ce5f8e198f55af4f5de0a64164c7d2f914fee29731d2c14da63bcf", size = 280875, upload-time = "2026-09-08T19:00:15.776Z" },
{ url = "https://files.pythonhosted.org/packages/92/e8/a94aa807140140d515fccb7393c75d2aaddac2403a207588fb3da1a2b688/mapbox_earcut-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:d8816d6b4b83d2a3e4b9a2f685a9d16418c680825ac277642c05975a08af073c", size = 288975, upload-time = "2026-09-08T19:00:17.285Z" },
{ url = "https://files.pythonhosted.org/packages/30/91/4d53f1cd838936da7cf7f14a641a07bff730cc19f5be3e3b77eb75475e9e/mapbox_earcut-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:5d28efcc3448f524ad45f52db9046b3296de32a0406f62ba203b360e3492d7fa", size = 464111, upload-time = "2026-09-08T19:00:18.699Z" },
{ url = "https://files.pythonhosted.org/packages/36/95/4477d1231487470822a98cff8dae19720ae3eef1798fa4a51e97b8251452/mapbox_earcut-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1c924fd7f19a1b1a2421306c651b228d36be30c131a1d552c2303214c67cf186", size = 70579, upload-time = "2026-09-08T19:00:20.033Z" },
{ url = "https://files.pythonhosted.org/packages/05/01/89e382936dcc3a0cf8260fd30d428afec81fa53e35062b150ff95350b154/mapbox_earcut-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:01e35034f923d087359a8551ee8546abfcc4d711674859ca41385409d44e993b", size = 67583, upload-time = "2026-09-08T19:00:21.804Z" },
{ url = "https://files.pythonhosted.org/packages/86/5d/3e52d5dd94d7227d5161d2270a29b5831738a119ff343c78b111c070463c/mapbox_earcut-2.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66439a181c1416907607b97f3f749ad22cb8b5d410a37da3a7d3f4914739a19d", size = 72469, upload-time = "2026-09-08T19:00:23.051Z" },
{ url = "https://files.pythonhosted.org/packages/97/6e/4cdab89617fbc955b85c91250767b482dce19813515dcee2d78a76da0354/mapbox_earcut-2.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52f75cca40240f33fe18de005ee1ecb1c13af6ecc45d108400ac6489b54562ca", size = 75465, upload-time = "2026-09-08T19:00:24.31Z" },
{ url = "https://files.pythonhosted.org/packages/9c/8d/d86b1da12235b27d8530ca30e5499bba646468926978eb46bd8f4ce79747/mapbox_earcut-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:691825b5b93171b1725491fada1ddae70c37330360f70592bf07c1b770a2b437", size = 169874, upload-time = "2026-09-08T19:00:25.632Z" },
{ url = "https://files.pythonhosted.org/packages/e1/16/c852be8d82c438ac583b6a07db02ff5547634c64966f5bc5fd761f1a6589/mapbox_earcut-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d0eab4447b73c54b4b73ccc25881a66d533b13ca37d887cf654efc17b8da21f8", size = 174841, upload-time = "2026-09-08T19:00:27.31Z" },
{ url = "https://files.pythonhosted.org/packages/04/9e/b0c286fb9d2d71d08665d23f00906d402f67b4379570b5cf115a2e7c07d7/mapbox_earcut-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:46c75cbf1d10b3e3b88dc2e0047f6f0c715e1bd4d56931727751f776660c0357", size = 283466, upload-time = "2026-09-08T19:00:28.841Z" },
{ url = "https://files.pythonhosted.org/packages/a3/02/091aa8f58defed3696b12ff7d4a56e78fcb3416b3d4e32fb183a2ce31646/mapbox_earcut-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4b11782a4bec5c5b442ebf5421361ea4b8d343ac15fd58d8dee39c0affc81a4f", size = 290860, upload-time = "2026-09-08T19:00:30.399Z" },
{ url = "https://files.pythonhosted.org/packages/0a/c7/e410ccb819156c4500f363a4cf6f1bddbf1f48007b10fd2352d93b0ac2c4/mapbox_earcut-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2f5f9e0e7be256393cb872b3af3064bd7eaddda16c0d238588b5c8ba9a8544b7", size = 69626, upload-time = "2026-09-08T19:00:31.844Z" },
{ url = "https://files.pythonhosted.org/packages/ba/62/49ebf1d6afbbdba83b73efbb2e91b45701ac6f0f07d5cf9242c521d28992/mapbox_earcut-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6cccc1efbfbc989ed178f91d1e855969fdc4acf26d4396ad404fec011ec69973", size = 66539, upload-time = "2026-09-08T19:00:33.059Z" },
{ url = "https://files.pythonhosted.org/packages/3a/cf/b4a2bea95c0ab9b46206898308103087df993d656e0e089891be331dbf29/mapbox_earcut-2.1.0-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26e40f410c0b101626111252fd657d310220254434bd240d29503d83c0857d71", size = 73192, upload-time = "2026-09-08T19:00:34.319Z" },
{ url = "https://files.pythonhosted.org/packages/d1/be/af8af09b7c74613a38fe4ca23afbdd45e70bf9019ff4b91401ce879b8572/mapbox_earcut-2.1.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5b7b07974a3832175fb79bcfba32582cdb04142703d804c40fe1043d4dfca3f", size = 76920, upload-time = "2026-09-08T19:00:35.736Z" },
{ url = "https://files.pythonhosted.org/packages/70/91/bdc5aa7dae025b8faf022987397bb27081f3b95d49ed45b4c9347b7f8528/mapbox_earcut-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0d6e11e5e722cc29d2da08840802742b0337842bed0c6655b8785874e646cc34", size = 168860, upload-time = "2026-09-08T19:00:37.075Z" },
{ url = "https://files.pythonhosted.org/packages/05/d1/a72944ae9567d8745668dacefafa92d6a9aadac63b2d9c155feef7b8c87e/mapbox_earcut-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b7447082d864ac496aa6e0b23e31428bda9ec3ca217c09ea9af405274fb56410", size = 174434, upload-time = "2026-09-08T19:00:38.639Z" },
{ url = "https://files.pythonhosted.org/packages/5c/f6/3518161f9caa9c5f50cf59ed764749b3a4eb650c07cdb11bd2a6ec4a7a6e/mapbox_earcut-2.1.0-cp315-cp315-win32.whl", hash = "sha256:fbaae75dc6c34d94329f55068aa3c0e4285b8f54a84e569f3d7c14be4774295c", size = 280968, upload-time = "2026-09-08T19:00:40.307Z" },
{ url = "https://files.pythonhosted.org/packages/fb/21/ba8bc7f379005f2df1f21cafde1b7f5880f539af35356c67a1bc156d9367/mapbox_earcut-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:e1b21fd100655ae3c0a2f62e08994228051f9b0b31e081c535c1431361b54614", size = 289212, upload-time = "2026-09-08T19:00:41.851Z" },
{ url = "https://files.pythonhosted.org/packages/d6/22/cabd764d02ba6a4639354d78f65456f2f0bbc7ca4c553d39e96c71a52c2a/mapbox_earcut-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:0e1cd2514e2774c11f988f33af09b3ff30f534c6dd9630d6a0b99694274d4b6a", size = 70737, upload-time = "2026-09-08T19:00:43.774Z" },
{ url = "https://files.pythonhosted.org/packages/dd/ee/1e92aaa17861ee9a5e7b406e33325b89f3e83fbc5767763f5cb57fa20fdd/mapbox_earcut-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:260814d85d5cb7f02b99d0fb69b4ec83a0cc4aaedb2c7f9c69d9f47a8ad38ff5", size = 67734, upload-time = "2026-09-08T19:00:45.063Z" },
{ url = "https://files.pythonhosted.org/packages/ce/a2/ec7ea2249e5b47b5c779f3879e83a51072d19b78822655f56cbad716fb0f/mapbox_earcut-2.1.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3bce59b92603849ef49084cad77dda3da323970ba7a82de3a0a35fc962ab92a1", size = 72598, upload-time = "2026-09-08T19:00:46.289Z" },
{ url = "https://files.pythonhosted.org/packages/f0/fb/ed1e52409b65873c1cbfcd3b701ac9adecb506585268f8c4d813a359d51a/mapbox_earcut-2.1.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da53538a84bc1bb32d3983710d0014be6f41f2e5dc90fad51dfb53278b0a1698", size = 75645, upload-time = "2026-09-08T19:00:47.588Z" },
{ url = "https://files.pythonhosted.org/packages/9e/67/5d4d81b893b2d0a7950edcd688fdc8fcfde92c6c3aaa49dbc306a6912019/mapbox_earcut-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f90df2b36e6008a5993e29c74a9d711fa227600a6f9675cb182f7feef3dcfbd7", size = 170030, upload-time = "2026-09-08T19:00:49.108Z" },
{ url = "https://files.pythonhosted.org/packages/28/d3/f2e047ba37e25f98b4086afed39c9ee6566c98a9c5bd7a32a8f459df4c8c/mapbox_earcut-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:afa1e3c5b9d983b58e88cf5a1ab8d57c7817777a9a9bc13d26356f940fa0bad1", size = 175056, upload-time = "2026-09-08T19:00:50.677Z" },
{ url = "https://files.pythonhosted.org/packages/05/f5/0beffb7ad07b71ece774b201b6efe969571fa7697be9649ad53b4eb4d8f7/mapbox_earcut-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:d7dca5e525f54c28160d5515f5aeae135a35cb82408e3bd280b01f208d23f5e0", size = 283566, upload-time = "2026-09-08T19:00:52.234Z" },
{ url = "https://files.pythonhosted.org/packages/92/3c/b73e2b9e4291663eaf1c2548ffc9051ccd47abd0f68e99f39e3dd8c8d603/mapbox_earcut-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:265e2670bad18d86a6a542b09ae6426e8025fcc7bfc7b039027f6af380d2a604", size = 291083, upload-time = "2026-09-08T19:00:53.822Z" },
]
[[package]]
name = "markdown-it-py"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "moderngl"
version = "5.13.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "glcontext" },
]
sdist = { url = "https://files.pythonhosted.org/packages/92/8a/c39cf051226c51105aac20bbe613a0ef05d006c2f2b2403fad25ca80a23c/moderngl-5.13.0.tar.gz", hash = "sha256:7e8b39e85702e097c4d680f6cc0c51aabf0c1eac741719a159ac405d37c95404", size = 204155, upload-time = "2026-10-07T11:56:26.639Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/bd/32c67061d42bace3d3ae2af449d857ab16a4b91e5e836c27fc851b545ada/moderngl-5.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:16c403575ef4a654e6b063f8475df693d4e0ec8ae188ce22bfe27ccd1ed84e77", size = 113605, upload-time = "2026-10-07T11:55:25.096Z" },
{ url = "https://files.pythonhosted.org/packages/08/26/a1d82541c8d7601222df0ab2aeb6574911d9d56ff206e930fe06909b05a8/moderngl-5.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99a3b90929f11a2311328e6edac08fc70f1d6fe73f0f42ce222e1adfcaebd484", size = 305422, upload-time = "2026-10-07T11:55:26.727Z" },
{ url = "https://files.pythonhosted.org/packages/cc/28/bcdf1f518afc81a84667df9824eec33d6377bff1489c86f5e83707dd0bd5/moderngl-5.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e328b8465635de39e133bd3069a9ce4b0e230845f6b279e5bd9d3392b7bf1575", size = 1331172, upload-time = "2026-10-07T11:55:28.98Z" },
{ url = "https://files.pythonhosted.org/packages/43/67/bb1e4e43b85dc1eb3b04172a02e3f8f499ab76303ecc9483fe0bc5bf3b45/moderngl-5.13.0-cp312-cp312-win32.whl", hash = "sha256:4d715f876b1cd430d76ca12aa2744ff2e120a6ef9e89446777bd8e1cddb96fce", size = 103266, upload-time = "2026-10-07T11:55:30.724Z" },
{ url = "https://files.pythonhosted.org/packages/50/c2/36775568c48af98997d8abfdcc3ea0af254af1ef2d1128b152f948207ea6/moderngl-5.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fa18165d2e1d4c2e7587540fb7938b45f576c66cf14dc2ca672dc63dd124d64", size = 110202, upload-time = "2026-10-07T11:55:32.259Z" },
{ url = "https://files.pythonhosted.org/packages/f5/d9/20b29f42c93be74b1d1e75671da5f0a810570083957c249d7841debd0e6f/moderngl-5.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b761331c16ceee58139ad51f8d5d72bb95803e58daecc8f217b52c8de24c2812", size = 113590, upload-time = "2026-10-07T11:55:33.691Z" },
{ url = "https://files.pythonhosted.org/packages/61/e7/48c53d6907de40777c702929616ac78cb5bccc9bb80d7bcd3f5ee558a540/moderngl-5.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:998b368bc9c0c153b13c5edefd8d42642f5f80b3c07e5cb026984b09066fcb9a", size = 308291, upload-time = "2026-10-07T11:55:35.332Z" },
{ url = "https://files.pythonhosted.org/packages/f0/62/dce81ef3ae3695eb87aa6fb165518ef32b20e0afa0f47ea5c62c6bc1751f/moderngl-5.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ba5096819a158dce5460e5ec97c735a807cf1790dd8a3bcb42f334e7fe084877", size = 1331111, upload-time = "2026-10-07T11:55:37.29Z" },
{ url = "https://files.pythonhosted.org/packages/42/70/d2254b0fe628eea0b62664ac50801d49da6959fdf1917b059430a7604662/moderngl-5.13.0-cp313-cp313-win32.whl", hash = "sha256:9d01be9f0442b992d480336e5dd2d8456673b5018ceda86d341cf4ba46b08cc9", size = 103257, upload-time = "2026-10-07T11:55:38.907Z" },
{ url = "https://files.pythonhosted.org/packages/09/04/16dcab39dc56a0ba6164a37e7c521725130edfe1104df3efc5ce9b2ac417/moderngl-5.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:72dfa490288eceb2400b402e1a853e005a26cfc3643ad630488bf463d9cdfce3", size = 110179, upload-time = "2026-10-07T11:55:40.759Z" },
{ url = "https://files.pythonhosted.org/packages/3d/3e/f075b421c472243d1d1ace0746b2d3c281cd758b74a08988ff6dd72955b4/moderngl-5.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b3f85a69442fcbf60d82098f91cdd2e188e90000245be262d99f42c8e7ea16a5", size = 113621, upload-time = "2026-10-07T11:55:42.396Z" },
{ url = "https://files.pythonhosted.org/packages/c6/71/312dc730a2e544d2309ebb51cbc2696fca8f378af4e64ca5284824883cc6/moderngl-5.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e58fd41ce49b52701fec5a08d20733d64527e6efdabaf242b09e7f717734db1a", size = 307533, upload-time = "2026-10-07T11:55:44.04Z" },
{ url = "https://files.pythonhosted.org/packages/90/cf/ce8a94366bab1dbf9164b7f455c1500c916f5c33e511859f0d57669283ef/moderngl-5.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ab46afd16a05f3b85e86a6e442a83fb8e923426eee5095718d1ca7fa990c8b4c", size = 1330566, upload-time = "2026-10-07T11:55:45.922Z" },
{ url = "https://files.pythonhosted.org/packages/fd/53/4b79d04c5d0a1cd1528084544ba1e0c2a26b272d0446ea65f970650fa08e/moderngl-5.13.0-cp314-cp314-win32.whl", hash = "sha256:534cfb5eac1241bac22da051d22eeacacc7ad88a76c8286e5eb14a21aab8be02", size = 104664, upload-time = "2026-10-07T11:55:54.996Z" },
{ url = "https://files.pythonhosted.org/packages/6a/06/f64315fea412beb3db4673beba25578af481c615c7d22d91529de24b3f56/moderngl-5.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:317010ee5d9f26c32e00c06363032ba79463cf66d9178e5159d0827c159f2f08", size = 111885, upload-time = "2026-10-07T11:55:56.358Z" },
{ url = "https://files.pythonhosted.org/packages/10/27/0eafeda42619b5a50ba1d4a7c3f257c7651197e3ae265c89fdcfa1a86cd7/moderngl-5.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9f9fa53973fa2cc785b6457412422c456dbf2e0eebd86e8a7ed75e1882b2df5a", size = 115379, upload-time = "2026-10-07T11:55:47.312Z" },
{ url = "https://files.pythonhosted.org/packages/5d/ab/4d95eca3e57c40d0a8a954950c0191ee740d81513fed633432f99b1fff36/moderngl-5.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36d82c174fa99d565b4872e0c0aee43c19378dbbce41cfed9e74c9fbacb2dedf", size = 318014, upload-time = "2026-10-07T11:55:48.792Z" },
{ url = "https://files.pythonhosted.org/packages/27/f7/26ba1e7d98dc7ae209f2303e74945adc54999e550cfb061493af45afbea0/moderngl-5.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2145b322dc96b16c9851fbc37aa37570bb1b8576804e0a9253ba5f5bc16b28d5", size = 1338749, upload-time = "2026-10-07T11:55:50.914Z" },
{ url = "https://files.pythonhosted.org/packages/6f/5a/9f429a0fa22d9fd88f581a2418fc364882d39f802b6758ed44027183fb6f/moderngl-5.13.0-cp314-cp314t-win32.whl", hash = "sha256:f4c5b73e563e19d89ae3c6704079383e175e7982e9000d06854bee06e5cfb3c2", size = 105920, upload-time = "2026-10-07T11:55:52.138Z" },
{ url = "https://files.pythonhosted.org/packages/ef/98/700dd5b8c31935cceb224fd184c3e875535ce074908e7f08f06ef8bee8af/moderngl-5.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f39bebc12a1a81a43856bf2b1a0f0b27219d2c011d4b3b3b6aa44667987a8518", size = 113555, upload-time = "2026-10-07T11:55:53.358Z" },
{ url = "https://files.pythonhosted.org/packages/3f/d1/21d69e1262dc589c0e8c56ae4b7ac302c37264fe2814e56fe31bfb773c6a/moderngl-5.13.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:21f85f717eea2b7cd19232ffe59b3f1042b61af94aaf897cec20d1cc06278f86", size = 113699, upload-time = "2026-10-07T11:55:57.863Z" },
{ url = "https://files.pythonhosted.org/packages/9b/16/82e46964f429a35332f64d5ed2eac17f2adcfb9a13c8dac3d8d427a23830/moderngl-5.13.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cdb7f7d2f931bd3af1ed8d28cf3c33e0513f20bb92d0dca0f01f4c2f6c2e47cb", size = 309021, upload-time = "2026-10-07T11:55:59.369Z" },
{ url = "https://files.pythonhosted.org/packages/cc/b5/043f041b87499b4894473ce6ae19b763458ea7252083bd03968966fe9db8/moderngl-5.13.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b96defa6493018afecc21d6474bb19381627c4921d752386b07d9a080a80c371", size = 1330971, upload-time = "2026-10-07T11:56:01.46Z" },
{ url = "https://files.pythonhosted.org/packages/6e/28/540871f112f6d26a1ec489a7f4384f7bad9012867f47313f60a6a2ded5a1/moderngl-5.13.0-cp315-cp315-win32.whl", hash = "sha256:33bdc219d19f8b87a14d67f259d0e8937a0d2499a89f33985ff5830fdbc49357", size = 104656, upload-time = "2026-10-07T11:56:11.277Z" },
{ url = "https://files.pythonhosted.org/packages/fd/31/c7ee980702d7213b996586faa2daf5f4eef9478b7c1ef424b857837ca83c/moderngl-5.13.0-cp315-cp315-win_amd64.whl", hash = "sha256:a35780a87df76c9db560454762cee72081bdf8877d0646b9c7427af3a7f4dcf2", size = 111880, upload-time = "2026-10-07T11:56:12.913Z" },
{ url = "https://files.pythonhosted.org/packages/af/cd/eef2fcef0ac96fdecb3b9a4fef6327ef44d40acde779ff135cc6ff1f9560/moderngl-5.13.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:520700d655fe66bfadfcaeb894de23997b738dafc449968cf50cfd9b776093cb", size = 115473, upload-time = "2026-10-07T11:56:03.392Z" },
{ url = "https://files.pythonhosted.org/packages/e9/9f/06b2b9b14fe3ef897fe73474be0c2306eb35341b263f311fa33355cf0621/moderngl-5.13.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4672b147e7ce6ceac4e4d50466d1bd9a4e370c3f6536b1821620b6813d3e8001", size = 319366, upload-time = "2026-10-07T11:56:05.34Z" },
{ url = "https://files.pythonhosted.org/packages/b8/f3/2dca08cfa9cc5a329b5ed4e0da36d6fb00a2145c0105b2f38fc50026372f/moderngl-5.13.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4b0e779c33bf3615ec8494373a81c2252961687c5a54f228735ade74c96701fe", size = 1339647, upload-time = "2026-10-07T11:56:07.287Z" },
{ url = "https://files.pythonhosted.org/packages/5d/e0/fc4e21d3d75ac161e924f560e128434b6de0e838bb5581dbbfdb5116a14e/moderngl-5.13.0-cp315-cp315t-win32.whl", hash = "sha256:a891579f5ed60dfa9f14327a9817e7e29f1ae094f483e93bbe7d3183ecec1957", size = 105904, upload-time = "2026-10-07T11:56:08.524Z" },
{ url = "https://files.pythonhosted.org/packages/a6/63/e0795b28c35eba2b7ea5ad453ed7297dd5a6cebd2315aa29d585abefb74e/moderngl-5.13.0-cp315-cp315t-win_amd64.whl", hash = "sha256:ae2975233100ac0facb6bb413a24844e7199f4c8151f4d0d75ea4e061e2d4060", size = 113484, upload-time = "2026-10-07T11:56:09.637Z" },
]
[[package]]
name = "moderngl-window"
version = "3.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "moderngl" },
{ name = "numpy" },
{ name = "pillow" },
{ name = "pyglet" },
{ name = "pyglm" },
]
sdist = { url = "https://files.pythonhosted.org/packages/90/02/92e235891300c901f59647112a0267a07454f58aeb2041aa44f6b85f9cb3/moderngl_window-3.1.1.tar.gz", hash = "sha256:29c2827505f87399f3461d480b2778910fddeebe44ea803301215cf212a6c1bc", size = 353495, upload-time = "2025-01-19T10:07:56.133Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/23/41/d3503f07de8ad3597165f88b07e1064629f82e65385c4307875b2f6b137f/moderngl_window-3.1.1-py3-none-any.whl", hash = "sha256:e3b3ac2b4e23afcbfdac1971318a4db893bed3ba6a8fbde3367b1226af39b2e5", size = 382381, upload-time = "2025-01-19T10:07:53.606Z" },
]
[[package]]
name = "networkx"
version = "3.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/76/3af777226b63a5e64a6b36b1ec5855c14e2b94a37096d4760e595fc43511/networkx-3.7.tar.gz", hash = "sha256:fd77a511bd90f39f3d016351345b52cf5319b813bdca01de3f755d3cca62e96a", size = 1866482, upload-time = "2026-09-21T16:45:16.974Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/cd/fe58041e9011f307c490e3e17dd48cc516448f7c698a3f2d9d9d65d7e6a8/networkx-3.7-py3-none-any.whl", hash = "sha256:e3fd2c13a7814cee3746340d8d7f8598a67f16a58bf47fb7f8793fab6efca1b0", size = 2142205, upload-time = "2026-09-21T16:45:14.609Z" },
]
[[package]]
name = "numpy"
version = "2.5.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/01/11703282db468b85f6f7b8c7f22d058de5970d5c7e60a3a8aaa313c3de36/numpy-2.5.3.tar.gz", hash = "sha256:df2d5874ff183595a4ba404edd04f6bd9b5505c1d7708573f6a6c17489a67563", size = 20791231, upload-time = "2026-09-06T16:27:47.073Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d6/50/8fdbb16af64895706a45f06a4068e29db732ec180f3c1375f14123359138/numpy-2.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb189f09db39283b26bfd061ec16189e14f71c6755207f72a0f7540867afe5b9", size = 16994982, upload-time = "2026-09-06T16:24:29.244Z" },
{ url = "https://files.pythonhosted.org/packages/60/39/789131c1188c078dcb3a1692e72e1e050c68b88ffe72c9ccaac9bcd7a9cd/numpy-2.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f59a878c33d6b88122d80d239bb3b845d58708750b0cb06a09aebb9b18ec696c", size = 12009327, upload-time = "2026-09-06T16:24:32.491Z" },
{ url = "https://files.pythonhosted.org/packages/9c/59/a312e95696e5f601914dd8b6dd844692ba61670807417e24b68e337b5c70/numpy-2.5.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a72f874bc9e10e4b8f80426fb49716d5141f64442a0c8418065093ec8017fbb0", size = 5445405, upload-time = "2026-09-06T16:24:35.071Z" },
{ url = "https://files.pythonhosted.org/packages/30/d0/5623a1707ed4fe16e3909fe3cf5ee3da004ae677ad23d83bbf3adf1a6faf/numpy-2.5.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:fc36dc566135b5eceec4cf89758fcb719266a019ef07dae1754ae7c9f617ef3e", size = 6783213, upload-time = "2026-09-06T16:24:37.253Z" },
{ url = "https://files.pythonhosted.org/packages/f1/32/84146fc020ad3c25f805f70ab60da46fe3c540a21369754a7e4369754b6f/numpy-2.5.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76c2c1e6bfa5c84adc6434dfbf013aa92096a7985221762c8f11fedfd20fff58", size = 15687872, upload-time = "2026-09-06T16:24:39.751Z" },
{ url = "https://files.pythonhosted.org/packages/65/af/aa78d1a88805456e212b65461354cd943197fb9acecc4c90fd12295123a3/numpy-2.5.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7e18c623bb5c95acb3b3328861272816ba199fb531921c5d6d0b675f1fde9e3", size = 16717410, upload-time = "2026-09-06T16:24:42.745Z" },
{ url = "https://files.pythonhosted.org/packages/3b/24/faa79d865e69a97ba17473b23a1b74094b2259c03e820c70297293b9ea49/numpy-2.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f8929ee6c96bfbd7b4ed2032e0c03af86fe1826740ab61ddabf9072d06e57ff", size = 17040975, upload-time = "2026-09-06T16:24:45.961Z" },
{ url = "https://files.pythonhosted.org/packages/62/4a/8877e629445a7176297dffcaf9c485faa96a95d81728a62521ad55bd4c0f/numpy-2.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b5d93cf48f687479941d12b69c873ad2cc76bbd487f0091c2200636497f34034", size = 18476479, upload-time = "2026-09-06T16:24:49.35Z" },
{ url = "https://files.pythonhosted.org/packages/c8/db/35e1c2d38b04cbd5b731f9d71495e055e813197669d22b612f11748d2ff9/numpy-2.5.3-cp312-cp312-win32.whl", hash = "sha256:bf63afbe037eb5d2fe87fbcc7778e61da53ebaf21d938a4515aa73b62532a5d4", size = 6133378, upload-time = "2026-09-06T16:24:51.915Z" },
{ url = "https://files.pythonhosted.org/packages/3c/a1/accf6d4f0c80c5d9ba9735d6b1550e444180599f34dec69ca01360f717ad/numpy-2.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0a59a421a32580a009e8a1751345bf829631b990dc1794b80514ab722b435def", size = 12567828, upload-time = "2026-09-06T16:24:54.255Z" },
{ url = "https://files.pythonhosted.org/packages/22/43/1764aff32e4652526ae2f71fa8b3efd8d25c8a3d6926914454e47138ed1e/numpy-2.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:ccb32e0525d29e8b0572eb84c9a57af0e7a4e615726927506f55063c62414034", size = 10485432, upload-time = "2026-09-06T16:24:57.278Z" },
{ url = "https://files.pythonhosted.org/packages/79/e5/8fb89cd46d14e35699d13bf943a5f5f441ecee8667120a1f6105ab89e349/numpy-2.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:66a78fe4556c60aceda5916f9eacd638b18e9e681016ec302dcb4682d6d4d034", size = 16991061, upload-time = "2026-09-06T16:25:00.411Z" },
{ url = "https://files.pythonhosted.org/packages/2f/06/9dc9e48b5e5e941c8b10350c5ff2d721da42a20517d911d15544246775ff/numpy-2.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92f30e89b8ee0ecf363033576c422b2f58fed6a80bed0aa48dff6d14c654663e", size = 12003676, upload-time = "2026-09-06T16:25:03.475Z" },
{ url = "https://files.pythonhosted.org/packages/ab/2a/98282aa5b8f58b1157d440bb6282eed47e3632a5de53a714fbab17e659fe/numpy-2.5.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f9a2353b37a1a9e78fd82b27ad7e2a32a2d036604d18f02b05e3136c62ca3b09", size = 5439695, upload-time = "2026-09-06T16:25:05.978Z" },
{ url = "https://files.pythonhosted.org/packages/a1/f9/b6533d777be9d6ffd29dc1be0867e563e6e8cc9a220ff1b716adc317f060/numpy-2.5.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:ccbc4665079665c3cf3bab4db9f6b095370cd6437d66be549b6c2a1fd19e1958", size = 6779395, upload-time = "2026-09-06T16:25:08.599Z" },
{ url = "https://files.pythonhosted.org/packages/73/85/735720d04ec197c5dcfacdfc9922667c7f1f5f496a279b7ba4d7c74c4cc7/numpy-2.5.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c76d5dde9f445058f83d0c02af00557a4db91de9a9a57c0df87d1535001d654b", size = 15681750, upload-time = "2026-09-06T16:25:11.173Z" },
{ url = "https://files.pythonhosted.org/packages/3a/1b/3b16a9bc514a440a7a0883684111dcb1ef1aee960af2ca95da8fc775f124/numpy-2.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5fa86b80fd24bcd1aff83ad23be44ea323de3f787be8f8b15d4a65621e25321", size = 16708577, upload-time = "2026-09-06T16:25:14.171Z" },
{ url = "https://files.pythonhosted.org/packages/69/c4/386f397831b07328b639c96c5b62719346cf4baf07c68d927239752b1534/numpy-2.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd4cb9ad3c7889b9b3fe0a9a9fb5d2ed26f9879bff2608d9f01aed147a20d231", size = 17042047, upload-time = "2026-09-06T16:25:17.582Z" },
{ url = "https://files.pythonhosted.org/packages/5f/3e/a700ecbf36e85ae8328fd3b0e12eeddc22ed6358a64cb2bd913e0d195d65/numpy-2.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1302b90c0e52281681b2975adfe8a860cb7b12216a27b4b0b4207c44bf7bccf0", size = 18465724, upload-time = "2026-09-06T16:25:20.949Z" },
{ url = "https://files.pythonhosted.org/packages/41/ee/38e785e88a4045f6ad1d1f2808dcdfafdca48c760260c0587bf171e29fc9/numpy-2.5.3-cp313-cp313-win32.whl", hash = "sha256:1c80eabb4035ecf4ca9cd49cde8a9fdd69a729e63e6474887d1523ade7aa277f", size = 6129003, upload-time = "2026-09-06T16:25:23.664Z" },
{ url = "https://files.pythonhosted.org/packages/f3/ec/100f2b1794ede74a9b3d7ec6b9736927f56713414c1dfe19ab6c383494bf/numpy-2.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:71cad2b2a7451ab79d8f5e71b453485b6775963d5cf794179144a7463fe6e8ec", size = 12560965, upload-time = "2026-09-06T16:25:26.602Z" },
{ url = "https://files.pythonhosted.org/packages/80/b1/7dc825ca94c12acebbce4c37caa5e198695eb31424bc579679f32b1bb49d/numpy-2.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:8e4dd766076855b5ff7ea52fa5f07ce26286726e0f8bff446b7739d02e6ea204", size = 10482343, upload-time = "2026-09-06T16:25:29.772Z" },
{ url = "https://files.pythonhosted.org/packages/70/78/cf416f15dc29375a229d9dfebf8db6e313f291580b39fa1a568b6052bb07/numpy-2.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:350ba9783ce969cf9f7ce6e6a9a58e1a6e2a19ca025b7ee448c4db727706212a", size = 16998686, upload-time = "2026-09-06T16:25:33.171Z" },
{ url = "https://files.pythonhosted.org/packages/9e/59/abcc2d8def4fd60eec7d87f92d27c13448ffd9ab14339bcc63a0d7a2fdea/numpy-2.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:012e66aca395d795496446e52aeeb5866312a5d4d3f27da270e5a0b43f70dc5c", size = 12013862, upload-time = "2026-09-06T16:25:36.748Z" },
{ url = "https://files.pythonhosted.org/packages/94/75/4640d2d6e4b64a049e48425a82728a41ef4adb61332d2cba68055774878b/numpy-2.5.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:adc1ada2662f8a5f960b8a10d9986897e7499ef07e06d4cfe7197f8cce923c07", size = 5449793, upload-time = "2026-09-06T16:25:39.476Z" },
{ url = "https://files.pythonhosted.org/packages/96/cd/625b57ae33d4ca560f32cc0b47b4a5922146d9beb998ddf773900d440a73/numpy-2.5.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:54a115e5a73b8fc44f0cebef486365a1894b5c9760685d4558b72b7c3eb846e0", size = 6785176, upload-time = "2026-09-06T16:25:42.069Z" },
{ url = "https://files.pythonhosted.org/packages/9c/72/12918652e7912ef9751e8694c88820fcd1908e0618cb23f5f3caa6004b7b/numpy-2.5.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be5a8381859b6da607c84f4f7d6847725f1cf1853ef8a2c9e115b7d58bef47dc", size = 15703377, upload-time = "2026-09-06T16:25:45.135Z" },
{ url = "https://files.pythonhosted.org/packages/45/8f/9beacf79ca7c650688ad0baa80931adb988fe6e6e5d5903c23cc3dbd70eb/numpy-2.5.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0521d0f4aebb6e06189451025fa17a913287b13c03d5fe05c017333b654ea5b", size = 16711928, upload-time = "2026-09-06T16:25:48.461Z" },
{ url = "https://files.pythonhosted.org/packages/09/8d/41d0a56e1ac4c87495c897a211b1368691b7237aadabec8b3b8f3a74d48f/numpy-2.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9deb49575e5b0b94ed72c8a64ec4d033381adc27e9060ae842971f697ba96104", size = 17059507, upload-time = "2026-09-06T16:25:51.873Z" },
{ url = "https://files.pythonhosted.org/packages/08/1e/0dfbc5cc251d54e2af790f254d24ec38637fa97ec7d5d11de7ffed787098/numpy-2.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b00eefbcf0f292945c4b4dec2ae845389ef5bcdcd596e6e4328051db5b5ba694", size = 18471002, upload-time = "2026-09-06T16:25:55.233Z" },
{ url = "https://files.pythonhosted.org/packages/b5/2c/dfa40f6991f8185c8c30ffd023dfcbb11888e823cfab9557b920f3bb7bed/numpy-2.5.3-cp314-cp314-win32.whl", hash = "sha256:c2381f82999704f818e2c987a865050e285ec3621262c66d40f5a96c8f899f8e", size = 6180485, upload-time = "2026-09-06T16:25:58.157Z" },
{ url = "https://files.pythonhosted.org/packages/a4/73/d2c08231e4fde7e415501fd02c715d96e98599b2d8384445933944152984/numpy-2.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:2c25dfa72943e4336ddb6b0ee4277b47a0c85bede0807530ec68103bf58e2c10", size = 12698179, upload-time = "2026-09-06T16:26:00.789Z" },
{ url = "https://files.pythonhosted.org/packages/5c/e9/dcdcc9b95cf5f49815055573aee1b11cfbf5299f38a180e437ded050810f/numpy-2.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:15aa985ac73a8db02db7663381aa109510449d3819d37206caed27b33a65a8a6", size = 10769383, upload-time = "2026-09-06T16:26:04.011Z" },
{ url = "https://files.pythonhosted.org/packages/49/c4/af8bc08a7ef4e1529a7c0cf24969accce316b783999802089a581ec99272/numpy-2.5.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ac7bb1c52d445bd4f8f7f97fefe6abc3a084dc4d63df50d79b17fa2b78e89297", size = 12132668, upload-time = "2026-09-06T16:26:07.138Z" },
{ url = "https://files.pythonhosted.org/packages/c5/ae/0f15eb56d4ec5e13c1f7ff04ff407f997d1acbadb45d3e1f2e2645a8f43c/numpy-2.5.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e6ab667ba76450084eb64013762c438ea76d9d29cc676dcd6c2e9892ba37f841", size = 5568580, upload-time = "2026-09-06T16:26:09.828Z" },
{ url = "https://files.pythonhosted.org/packages/23/fb/c72a8f25d4b6e96c354e7ab45ace3b27dc11e5d6a13b6c7d0cd6b08bf112/numpy-2.5.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f7fabeb6cea87d65f3b926de33d03fb016cfdc29314c90974383b5582ae72891", size = 6882634, upload-time = "2026-09-06T16:26:12.524Z" },
{ url = "https://files.pythonhosted.org/packages/07/a9/968c90ed2ab15060c338e8137f1215b5a60756ae07328e0a60d1c6734df4/numpy-2.5.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fb6f8fb9ff0b3a69f52c66ce397b0246583e9f28616231b0e32ca49259a5fa6", size = 15748923, upload-time = "2026-09-06T16:26:15.092Z" },
{ url = "https://files.pythonhosted.org/packages/59/08/9df04103947b95e3b6b1f2ed1a70521f325647a31b82da6a2aae3a485508/numpy-2.5.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93e1f5447e2b1e479d7bd74701e84746b86450cff1fc368b132d195e2b8f8211", size = 16746748, upload-time = "2026-09-06T16:26:18.43Z" },
{ url = "https://files.pythonhosted.org/packages/41/a0/14c8d5fe5b53a334aabb653deb391c0fef49558f491880ea300ed6785224/numpy-2.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c00abe94c1a69d75d827dcf1c025b25c8a45d230b3bcd77a9020883a1b047653", size = 17111561, upload-time = "2026-09-06T16:26:22.113Z" },
{ url = "https://files.pythonhosted.org/packages/c4/a6/d7e96e42f01522e154c32489640f16dfc4f6181d165d05fc3bec8c2c4999/numpy-2.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:536f963710a4e63934d80ac0dc4f478804a83e9a84b6828018f25d09953ada33", size = 18513945, upload-time = "2026-09-06T16:26:25.401Z" },
{ url = "https://files.pythonhosted.org/packages/25/39/3453afb7119d0449ef11c886874120ff180e2c337760e0e2d88f70f1a945/numpy-2.5.3-cp314-cp314t-win32.whl", hash = "sha256:4c8a6d2ebce6305fd82fbefca827775437147052a976ee7c94b36a0c1b52ac6c", size = 6335421, upload-time = "2026-09-06T16:26:28.175Z" },
{ url = "https://files.pythonhosted.org/packages/99/01/22815d2b19a1a746b1d45205cffebb3fe511a18acb75fba6c88491fc9894/numpy-2.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9a37475425b431b4d060f23b4f52cd2f3aef6bc7c654bd760adf0040eec9d435", size = 12896420, upload-time = "2026-09-06T16:26:31.265Z" },
{ url = "https://files.pythonhosted.org/packages/fa/ee/a7cbba67eeaff038dc29ca8b98a88396c8b0cc9c89d4924f4a27a5c9150b/numpy-2.5.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2d8240cb4c16fd831074aa2b2cf9fc54664d826341d61c372245b96a74a49a9a", size = 10857177, upload-time = "2026-09-06T16:26:34.167Z" },
{ url = "https://files.pythonhosted.org/packages/45/56/78194492883ff5eec90423fe56a3a44b154da047d88a6307f629713c584f/numpy-2.5.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a6391fafaba97500887132cd582abc6e19452b1ac775a47caa7b24490e152058", size = 16996531, upload-time = "2026-09-06T16:26:37.287Z" },
{ url = "https://files.pythonhosted.org/packages/11/39/dd55c0af90bbab564b09ae3b0aa60ec5c02b900fa4f1ba23440525c8b32d/numpy-2.5.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:09d5a423c71ad5feb5625844ad58050e35df43871004b52ac9c0ad44a56775be", size = 12012569, upload-time = "2026-09-06T16:26:40.707Z" },
{ url = "https://files.pythonhosted.org/packages/b6/51/04f67d32e4862b281b1cb84ceeaed3421189a84fb6fb51a391cd6d5009f7/numpy-2.5.3-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:f9579f383d1bf9df80081e72760e84960a7fd4f88cf0c9e535a8597c9bb646f5", size = 5448498, upload-time = "2026-09-06T16:26:43.435Z" },
{ url = "https://files.pythonhosted.org/packages/a3/c9/25b4dc0dd1344ec26c7319e84fd4e9809d2b5628f4e12decd618036e5178/numpy-2.5.3-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:86bff898a431c0fb71f7610b75726e75a54d47b37edc9d537f48de63bb3c0b90", size = 6783026, upload-time = "2026-09-06T16:26:46.374Z" },
{ url = "https://files.pythonhosted.org/packages/fc/c7/29285be1e5232a6e7ee3268a33c85843f5a8ee93350c6465cddd66ebbf76/numpy-2.5.3-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f3ed25271581281f2fccb1adcedfcde4c07362eec69189b50baf6f90e3ae159", size = 15697322, upload-time = "2026-09-06T16:26:49.415Z" },
{ url = "https://files.pythonhosted.org/packages/55/49/bbad5335fb4996a16881f853ff3e0ba582f01720e55c89b1c06b8fc42a90/numpy-2.5.3-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffdc76bfcae6b255dff75202c5e7feaf95b40246bc0a17944facc1fecf9f79ab", size = 16708995, upload-time = "2026-09-06T16:26:53.127Z" },
{ url = "https://files.pythonhosted.org/packages/ef/e9/1df35483760b04a65ea44669f89dc64f30e5aca098b48ceb8b1310b0e0fe/numpy-2.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:116f96cadd935c6122e9228d676fe7ede19e741f5c8bb1c3cddbe0c51ccebea2", size = 17052508, upload-time = "2026-09-06T16:26:56.464Z" },
{ url = "https://files.pythonhosted.org/packages/b8/99/66e54da8265cc8be8a7382bf96edce17aaa2837d6f484432025932a3caa5/numpy-2.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:09ffa5d903faeaa5c4dd05009cf81c8bab9f2cb37c548b8d39b65b4cfa7c97f7", size = 18468224, upload-time = "2026-09-06T16:26:59.966Z" },
{ url = "https://files.pythonhosted.org/packages/01/bc/b5e90a91c115168d793dfd2ad9c69c438c2fe7a13a437e770bc5b078e732/numpy-2.5.3-cp315-cp315-win32.whl", hash = "sha256:e01c918ac3d48e18a927cf7b14a26a3e29ff2bdf2eacb976da0aecd6a43ed034", size = 6179919, upload-time = "2026-09-06T16:27:03.166Z" },
{ url = "https://files.pythonhosted.org/packages/37/ea/780748fd3985109075514ef8fc64cd25f943e40dde13a6d59141eb268fc8/numpy-2.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:e931e4f499e0dc7ef29d269a8e5b35dd722e5d14be07df6240166ea7c6532fae", size = 12697656, upload-time = "2026-09-06T16:27:06.153Z" },
{ url = "https://files.pythonhosted.org/packages/b3/16/407be69a2a87c8cab64d95975a8977a426a29e138f07e276ec258f0fe4e5/numpy-2.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:26e15e4aecd8617dfbaecb37d223e365d7b39411fba20454be2670a96aa74cb5", size = 10767601, upload-time = "2026-09-06T16:27:09.297Z" },
{ url = "https://files.pythonhosted.org/packages/44/bf/a97ffb01e41d50a32a9177aef942a4d0e389a3daf451d04e5f38ef6afb87/numpy-2.5.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:6cef4bb1706dfec49243c05d921eefb4e190d41e2528b30d8035ea1f36b4c24a", size = 17090092, upload-time = "2026-09-06T16:27:12.907Z" },
{ url = "https://files.pythonhosted.org/packages/d1/24/136c02f2c2af9a067a84d0c3aa10c99012c0476fa5066732fa4a4202557d/numpy-2.5.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d1c89973648c85069c5046ad460f7b8a00218b29a2e42359ac8cc63e9ab94832", size = 12129429, upload-time = "2026-09-06T16:27:16.089Z" },
{ url = "https://files.pythonhosted.org/packages/fe/6c/b47582d6597789bf946d5efbeb6b9e56fd8bcbd5efc6fbf51dbe1ea31eb3/numpy-2.5.3-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:214045a5bf00113a146ab9ee9730c44501af6723cdf1f6830932f7b5ef2e7af0", size = 5565452, upload-time = "2026-09-06T16:27:19.868Z" },
{ url = "https://files.pythonhosted.org/packages/be/b4/ef3cc6da73774202d4deae16bb321fd8298a4e0561e3539f8c4be237d916/numpy-2.5.3-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:8617bbfae4486cf99c9f899966699428d19da931d06ca94ad3da986c76e15997", size = 6876736, upload-time = "2026-09-06T16:27:22.232Z" },
{ url = "https://files.pythonhosted.org/packages/9e/24/e3813329498596cb842703dcacac1741612ed9fb9c4e6a3e0c7e2ebbc597/numpy-2.5.3-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:595d020938c84e320bcf40ad71089e108eac0d377cd018e14a8c094f39e98d85", size = 15745777, upload-time = "2026-09-06T16:27:25.181Z" },
{ url = "https://files.pythonhosted.org/packages/4a/9e/4e7a07fd0776dc2210cdacf2010be8665194d094defc10c419d7dea794cc/numpy-2.5.3-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f24021b9f22bc6301c37b196974a92c1c18dccedb6fef3dd252e95f2d6adbe4", size = 16746949, upload-time = "2026-09-06T16:27:28.576Z" },
{ url = "https://files.pythonhosted.org/packages/91/db/01674c0e20335057813a00c2ebd546ed25bff9ed7914f9bced00f8c55d94/numpy-2.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:71b39d9f935b6ec0f8753e3e2afb51e3efba6f2e05b68b32a40754d24bcd4a3c", size = 17108994, upload-time = "2026-09-06T16:27:31.946Z" },
{ url = "https://files.pythonhosted.org/packages/45/7a/584c5e71f8d378e57cac0b033891ed65c683ef90573ba4854e8c28203db0/numpy-2.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6b05c171afb3aa07adbd20abc00aea86fe375beb0fdb9ef780ec5b7f63bab1c0", size = 18512266, upload-time = "2026-09-06T16:27:35.196Z" },
{ url = "https://files.pythonhosted.org/packages/a1/d2/4e1014173aa3c55e6a756e0e567290743a6ab33a288460374d7ef6bcd239/numpy-2.5.3-cp315-cp315t-win32.whl", hash = "sha256:f54660b0eb6b0b9f36e7fe1cdfdff472028dd0d14acd9b9b65098efbad059469", size = 6330292, upload-time = "2026-09-06T16:27:38.149Z" },
{ url = "https://files.pythonhosted.org/packages/6c/b0/ff5658a58199b7bcaad87bf260eef6713d9d42cca4e028f935b4fc5fbac6/numpy-2.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:1aad64d99730d013cfc6debafed22783b4fc5a7f4b8bc744d2d8cf7dcc880551", size = 12884918, upload-time = "2026-09-06T16:27:40.965Z" },
{ url = "https://files.pythonhosted.org/packages/fb/0b/b12a2df5d1b774bd9007a6fdff9381145b6223d37f11afc9c37ab0efd9a1/numpy-2.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:befa1ae5bd6030b3f512b43ff3fa5290bbed6b84411a44244b14adf835f5b89d", size = 10850807, upload-time = "2026-09-06T16:27:43.868Z" },
]
[[package]]
name = "pillow"
version = "12.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" },
{ url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" },
{ url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" },
{ url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" },
{ url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" },
{ url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" },
{ url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" },
{ url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" },
{ url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" },
{ url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" },
{ url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" },
{ url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" },
{ url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" },
{ url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" },
{ url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" },
{ url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" },
{ url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" },
{ url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" },
{ url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" },
{ url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" },
{ url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" },
{ url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" },
{ url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" },
{ url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" },
{ url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" },
{ url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" },
{ url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" },
{ url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" },
{ url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" },
{ url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" },
{ url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" },
{ url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" },
{ url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" },
{ url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" },
{ url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" },
{ url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" },
{ url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" },
{ url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" },
{ url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" },
{ url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" },
{ url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" },
{ url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" },
{ url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" },
{ url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" },
{ url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" },
{ url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" },
{ url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" },
{ url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" },
{ url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" },
{ url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" },
{ url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" },
{ url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" },
{ url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" },
{ url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" },
{ url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" },
{ url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" },
{ url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" },
{ url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" },
{ url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" },
{ url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" },
{ url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" },
{ url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" },
]
[[package]]
name = "pycairo"
version = "1.29.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bb/5e/19ab2572981a996ce96b853a189d2dcced40506b67d9555a7568457abcf2/pycairo-1.29.2.tar.gz", hash = "sha256:3e69fff74fe64f5ba2dfa31f67c6bdf26413342574047437d2ac520d35e9a489", size = 666308, upload-time = "2026-10-04T19:11:03.81Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/be/5433f918a50238f848e7f6e4d9662331f4c556a01e2e8dc58945c2af440f/pycairo-1.29.2-cp312-cp312-win32.whl", hash = "sha256:75b1b6ff515b6bcb89e86c5f2bb4762c46f0fcbde6c444ea035c8fecd1bc8b55", size = 752061, upload-time = "2026-10-04T19:11:31.193Z" },
{ url = "https://files.pythonhosted.org/packages/7e/0e/1571890c2ab04a6e4b59758ffbeab3c141fe08f909fb0d2f6e8cacfbe47a/pycairo-1.29.2-cp312-cp312-win_amd64.whl", hash = "sha256:b97fd674dea445eae40387353f32665dbe19080a2ef63df97fd81273afc6e9d1", size = 846938, upload-time = "2026-10-04T19:11:32.717Z" },
{ url = "https://files.pythonhosted.org/packages/80/07/890c5d83d4f7d4972c496f6966c1257a46384fb93fbbfb8c207daea92e57/pycairo-1.29.2-cp312-cp312-win_arm64.whl", hash = "sha256:a9ed77de6f9ba148d493360d1b2a93be8a785c8ae855d4fd6e8df293ed58ddd8", size = 695598, upload-time = "2026-10-04T19:11:34.111Z" },
{ url = "https://files.pythonhosted.org/packages/03/e9/03a625a9da4f0f0f5cb9420b7e6419478363d27c91888d82717d38a224d2/pycairo-1.29.2-cp313-cp313-win32.whl", hash = "sha256:50c30f88fa12b722ac8f997b67f83f5ea8fad06bc1d6c33dbdab504eb9978e56", size = 751443, upload-time = "2026-10-04T19:11:35.895Z" },
{ url = "https://files.pythonhosted.org/packages/23/44/7f39568959f5b2997bb888728423aeab00617cf8844f8126ec9ddeee86c2/pycairo-1.29.2-cp313-cp313-win_amd64.whl", hash = "sha256:0b87aa05a50a2c8cf2489dffc1b12f88b6e2fe384075f8c799367625081ca3cd", size = 845293, upload-time = "2026-10-04T19:11:37.541Z" },
{ url = "https://files.pythonhosted.org/packages/8f/90/83ebe0ae1d66378306085c505e7bf7abd0060714695e303877c800ff6d5c/pycairo-1.29.2-cp313-cp313-win_arm64.whl", hash = "sha256:806cd0f1266776fb6e881ce322569f0439993d66df0eeb8a255b1316f947acd0", size = 694636, upload-time = "2026-10-04T19:11:39.185Z" },
{ url = "https://files.pythonhosted.org/packages/53/d4/48e4b327563beae41c5eabea70c4b8b9aec790361dec02c338d4e8e2ac21/pycairo-1.29.2-cp314-cp314-win32.whl", hash = "sha256:98360270afaa909bcc1769f299160123322da29d2a7310763b0ddc16e624d5c6", size = 767785, upload-time = "2026-10-04T19:11:43.794Z" },
{ url = "https://files.pythonhosted.org/packages/42/bf/9d9eb2d0780440b1b5fe916e6a3abfa11e05f750b0ad643987cb9c0761d2/pycairo-1.29.2-cp314-cp314-win_amd64.whl", hash = "sha256:786373ba1bd78fbdec02cb1c99254eef12438bf942dd13534fe61b948becb527", size = 872750, upload-time = "2026-10-04T19:11:45.545Z" },
{ url = "https://files.pythonhosted.org/packages/78/36/f785d5a974809f36247ee56d15ca135433d1bf3c2adf1eb8fd66150b6a5e/pycairo-1.29.2-cp314-cp314-win_arm64.whl", hash = "sha256:9bb09bca782b5e21a4beb698075b5504f234af0597920f0c0439979226a76182", size = 720245, upload-time = "2026-10-04T19:11:47.69Z" },
{ url = "https://files.pythonhosted.org/packages/4f/74/c751bab17d770a3092d79b111281a7dca420b1490dd6e6f5408faab884ee/pycairo-1.29.2-cp314-cp314t-win_amd64.whl", hash = "sha256:bc9e26b7e9577d3655509766275653aa945f101fb28c197496facf8c9ba90321", size = 875205, upload-time = "2026-10-04T19:11:40.692Z" },
{ url = "https://files.pythonhosted.org/packages/b8/61/2d6f8361ba8497bb23bbb8ec8b91959b2e71366a3af2830c5a758663eabd/pycairo-1.29.2-cp314-cp314t-win_arm64.whl", hash = "sha256:29e3389c0b6d3dafda1b2360a8938790c2c7b6971b78e109a565c1cb91f8ae9a", size = 721982, upload-time = "2026-10-04T19:11:42.146Z" },
{ url = "https://files.pythonhosted.org/packages/92/05/427798a6722ee60ff9e1dbde0d6b67d56bdf4362c48c3239cf790804359e/pycairo-1.29.2-cp315-cp315-win_amd64.whl", hash = "sha256:c5dfa99afdde95b82325f240d144008a6b5be99405e74070540a1b7a857eb2a7", size = 872704, upload-time = "2026-10-04T19:11:53.589Z" },
{ url = "https://files.pythonhosted.org/packages/5e/58/708d622971b11215d113a27b8a3ca21815383ff2441eac8922b03a22aa0a/pycairo-1.29.2-cp315-cp315-win_arm64.whl", hash = "sha256:6b0410280b82bf60a84185392f720c099d71524dd7983f93d9abe18c8b2cd3a6", size = 720251, upload-time = "2026-10-04T19:11:55.606Z" },
{ url = "https://files.pythonhosted.org/packages/a9/db/fb0d756cc1966a3e5f3c903f14136690d567983a571d1864e6e7ca1f8c21/pycairo-1.29.2-cp315-cp315t-win_amd64.whl", hash = "sha256:88cbf5632be0b9255822b457d3bfe4f9eb0bc24e5520f54b6c4334e4f5b0f5aa", size = 875248, upload-time = "2026-10-04T19:11:50.36Z" },
{ url = "https://files.pythonhosted.org/packages/0f/ec/3c2700abac22e3ea70c67cb27caa6c2960372ebb7361127328471a39fa7f/pycairo-1.29.2-cp315-cp315t-win_arm64.whl", hash = "sha256:0e49de5b93fc1e76e670f368b9bd0ffc881c72f506e4293c4b4d79f52cbe4c47", size = 721983, upload-time = "2026-10-04T19:11:51.895Z" },
]
[[package]]
name = "pydub"
version = "0.25.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326, upload-time = "2021-03-10T02:09:54.659Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" },
]
[[package]]
name = "pyglet"
version = "2.1.16"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8d/af/72a0c4fc95339ec590f441a591143e04acb27b2c94845735809c1630ade6/pyglet-2.1.16.tar.gz", hash = "sha256:ca90ee05532ced8330b8a8087ac2a7b69589b7fa7fb67f85aabe24aaf4ae42c0", size = 6598246, upload-time = "2026-08-01T01:32:35.895Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/23/93/b81e304f8954876fed84c5afffe83d2c1c0ceed064d5003b87fbfe9f9076/pyglet-2.1.16-py3-none-any.whl", hash = "sha256:27f9cabf97a64b15d335cf70d9e8979bbab5f84a9900a058206bcd3a2de341ae", size = 1037857, upload-time = "2026-08-01T01:32:30.566Z" },
]
[[package]]
name = "pyglm"
version = "2.8.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/41/8b/bdaf7b9cacecd28f7b4c6fc2d7d136824c506ad38cfdb37a05ea7ec88694/pyglm-2.8.3.tar.gz", hash = "sha256:161781ea4d1267f796b645f85ebff53aeb8ee4f13b4e993c04d64c96d286e534", size = 584038, upload-time = "2025-11-26T12:12:59.47Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/a0/8759ed290b8d9830a6beda947c48cfb4e7ac9a14c04c8b612e1954dd0cf8/pyglm-2.8.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72c75bd76ade851bf5c2b12cbe384193ea901a73474d57e4d7044bbbcd7d08fd", size = 1608534, upload-time = "2025-11-26T12:11:29.277Z" },
{ url = "https://files.pythonhosted.org/packages/77/36/5feaf47a5f105cf478505cde5e32b4ce19649a11f802afaee1063ad3adaf/pyglm-2.8.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:570da7032ff1185c842c5864d080ee5eab091fa53ec217b8d4d4034c22ba744f", size = 1362644, upload-time = "2025-11-26T12:11:30.774Z" },
{ url = "https://files.pythonhosted.org/packages/f9/6d/840566e3cbadb4d66d60c112f2c507260b46aa674e6fd22913493cfb7cd4/pyglm-2.8.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a14f1ffdc30a6f53334d16651888f60a2f8a63e05613357234c5592747b906bf", size = 12155701, upload-time = "2025-11-26T12:11:32.54Z" },
{ url = "https://files.pythonhosted.org/packages/f1/97/3e6648727f597885f4e6d92b77b49dc2fa2b65a2b9ea3ab35caaff06e072/pyglm-2.8.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d2305b258bebc358e00b8c85c3bc2d6f78d64f9b3f8802e1699c3e8dae54361", size = 12785652, upload-time = "2025-11-26T12:11:34.449Z" },
{ url = "https://files.pythonhosted.org/packages/e4/9c/04c525d78356e6493d0abc3ea945802befd5b813b1820e5b78dd5ca2f072/pyglm-2.8.3-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:f80d208e558a024a3dc83300bd661a71aa4cda884a256c5b87179059a1c55373", size = 10886479, upload-time = "2025-11-26T12:11:36.324Z" },
{ url = "https://files.pythonhosted.org/packages/eb/20/d4003e53a590b8dd8fa9ab2e6848a1acaeb8d239c95161dc2e05f5972c9c/pyglm-2.8.3-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:8abf221111db95e34620e446a853d43c887ced10d5c217e2df18471c9ac683ef", size = 11711522, upload-time = "2025-11-26T12:11:38.639Z" },
{ url = "https://files.pythonhosted.org/packages/f0/15/2b1a7761b33c309f4c14ce505f671fb75cc2c4b285af8fdcfb6d580cc1be/pyglm-2.8.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71ace7b15dd6d2ea9dbeadd43738ce69e7134547fc4674617084df5096e9b866", size = 11938446, upload-time = "2025-11-26T12:11:40.36Z" },
{ url = "https://files.pythonhosted.org/packages/9b/e7/9cbee60fbd66592a0cc46ad4e26c4bd26d55f45411323a5b03e3bd9a33d5/pyglm-2.8.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:04be7de24547130b8d413bc96ab9998d95db89cbaf224f401624ac1fa62bc431", size = 12773288, upload-time = "2025-11-26T12:11:42.44Z" },
{ url = "https://files.pythonhosted.org/packages/70/11/c80ffa11495b5788cfa5d021ae3bdbef20c941ead87911b2d50d534b2054/pyglm-2.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:5f7ff6f4ac14b092d897931b9ef76bb8108b5faa6f5118963c9de86c6fa18efc", size = 1663004, upload-time = "2025-11-26T12:11:44.1Z" },
{ url = "https://files.pythonhosted.org/packages/f5/d6/a24ac1280fc1cd03fe1ff0fa2632fbafdee1a4abe9cd319cb0ae6aed99bf/pyglm-2.8.3-cp312-cp312-win_arm64.whl", hash = "sha256:ba2de300dbaf39cb6cd82ce7db05a8dcf85bb7dacf1ec65db18b1ebc34438a0d", size = 1239976, upload-time = "2025-11-26T12:11:45.196Z" },
{ url = "https://files.pythonhosted.org/packages/2d/22/ee11dff20adfc6aac3e0482ed275843e0b9854dbc2e812ab56cdacbed8d6/pyglm-2.8.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a6d07b6b73e55a36e1b6daa63acb53f4912f2ddf88f434af4cc70441435aef8", size = 1608547, upload-time = "2025-11-26T12:11:46.426Z" },
{ url = "https://files.pythonhosted.org/packages/1e/5f/10c4cd636c3e6c63328a476f84bb03501d8016f12053c3a82ba588fc61ce/pyglm-2.8.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eaf6241f7c3ff169575e11da78f4439422517e42558961332db4cd09e9599267", size = 1362651, upload-time = "2025-11-26T12:11:47.555Z" },
{ url = "https://files.pythonhosted.org/packages/ff/70/2c7fe768900ee9d0f87e7a89375fa7d83b5b0a8f0eee8d0ad06b22a96e37/pyglm-2.8.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e1ec0dfb8f2c848c4ee6330c70a1ca9333004776c8e5ec76096e0b67c739f688", size = 12157160, upload-time = "2025-11-26T12:11:49.042Z" },
{ url = "https://files.pythonhosted.org/packages/b0/a6/befefccf1c8a0a66f09a7a1a1d324a3f988cb5e6b634026280518e5a83cd/pyglm-2.8.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3eea9093210afb946769c84fe31f17a9f73696a1161ee84fb42e9255f8c5cc8a", size = 12787899, upload-time = "2025-11-26T12:11:50.946Z" },
{ url = "https://files.pythonhosted.org/packages/7c/d2/5475d0791b585ae26dc0d690862a0e1e6fda243552a1718628dd937e5543/pyglm-2.8.3-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:0c652650912dbd88994fa02ec9dba2f3b35dc3995427b6ae8056ae542d5a060a", size = 10886936, upload-time = "2025-11-26T12:11:53.628Z" },
{ url = "https://files.pythonhosted.org/packages/e1/a1/035068410f60ed53007e0488af96f7e4634b679e9156d1090e70732b2159/pyglm-2.8.3-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:e3bdda68f75ad270e66b0fc7a1883749489cca14c44cbe22bac038fa170a8a1c", size = 11711601, upload-time = "2025-11-26T12:11:55.367Z" },
{ url = "https://files.pythonhosted.org/packages/b5/e6/286d1879eae87199f086136b08c70a3bff27880417f98dadc0cbcdb65e50/pyglm-2.8.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:08f8b1bd0d80ce396ee9cd5d3d4c7aeb4bbfa2a54dc73413924e3c9982412528", size = 11939755, upload-time = "2025-11-26T12:11:57.23Z" },
{ url = "https://files.pythonhosted.org/packages/72/58/f40f109ac025bb18412db78e6e73ffc20a17b6b495de90352f5aabfb9982/pyglm-2.8.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:12fa61feefa5a255097d0887415bce9dd72c1dd5e5a8c6577121ee5348336a16", size = 12774242, upload-time = "2025-11-26T12:11:59.051Z" },
{ url = "https://files.pythonhosted.org/packages/17/fd/71b44ee5ac341e9979731a7b868e237f3775e140a9481cea79bae7abb83c/pyglm-2.8.3-cp313-cp313-win_amd64.whl", hash = "sha256:33118ef1d678ab573546757dee7f0a1ca2fba8e8d7760c9fe6320fe0cfa3deb7", size = 1663010, upload-time = "2025-11-26T12:12:00.914Z" },
{ url = "https://files.pythonhosted.org/packages/fa/1e/9b8ba9d4627585797d8bda952412c93a4c09b70cb25e64756a52accddc1e/pyglm-2.8.3-cp313-cp313-win_arm64.whl", hash = "sha256:73ff3785dfc4ce017626d7ab56d6711a7119c29e2e71294efed73810c1d307f9", size = 1239893, upload-time = "2025-11-26T12:12:02.032Z" },
{ url = "https://files.pythonhosted.org/packages/65/fe/494d7dce3fcaf0123e787320b4a558d8fe733d6d0e23b7fe51e687f88dc2/pyglm-2.8.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b312012458d0537b3d84f24f4ba51fd3930df7a0773fc643e36f8df27b807c7a", size = 1610593, upload-time = "2025-11-26T12:12:03.136Z" },
{ url = "https://files.pythonhosted.org/packages/27/c0/37cced4a1b29957a29baebc1f0034be5d5419adf12da4b99140c56152cdb/pyglm-2.8.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3249bfe5352e18cc777fac591665679a99f270e1ff27cd11dc349af07684f007", size = 1366295, upload-time = "2025-11-26T12:12:04.176Z" },
{ url = "https://files.pythonhosted.org/packages/a7/ba/186176d1c3e26196ae4bec4b228bdbb1304576f2038a7f1635070923ee48/pyglm-2.8.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb8ea0d6721c763a26eacde59b2c9165719050dcf49c99a2857f6e1e5a5f30bb", size = 12140715, upload-time = "2025-11-26T12:12:05.429Z" },
{ url = "https://files.pythonhosted.org/packages/eb/3e/8d9f307649e9b79b34ae51303e46e7012cc2550d60e6dd00ea8d3d8c9cf2/pyglm-2.8.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:83effb89e2cf6dd79cf9ebecf2f9fbda3d25a92b61af264de3cadad408911de6", size = 12764551, upload-time = "2025-11-26T12:12:07.403Z" },
{ url = "https://files.pythonhosted.org/packages/4c/d6/2481433abe537d7019d9ce75eba86b57e1731e3e0352fd1ac7f31f4d1883/pyglm-2.8.3-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:e958d65ed55f2716fd8a3a2ef872cc52893ea7300d7feec62dccb27ec25fbc2f", size = 10869966, upload-time = "2025-11-26T12:12:09.198Z" },
{ url = "https://files.pythonhosted.org/packages/fe/bf/3c78a9718e1d26c5b6ec468a584eb30cd797c46b6edd08e79cb1d67e8bf0/pyglm-2.8.3-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:ec7cc14d2eb9f46a18012ee7c1a164e0395b058ceb6e341bf6d986316b698574", size = 11693025, upload-time = "2025-11-26T12:12:10.948Z" },
{ url = "https://files.pythonhosted.org/packages/d3/0d/00ef293153b6ca7e56d1be6facbd9d0a8e331d0c6d3114f47f2cf2913eda/pyglm-2.8.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4beb0ada21e7641a577f274496451befcde79965467ef4027bd933334b3de39b", size = 11920727, upload-time = "2025-11-26T12:12:12.845Z" },
{ url = "https://files.pythonhosted.org/packages/4e/10/d8ecf9b5ac3b6fc1b179b33dfbd087b8f4d4b12bab99b5794f569a15a99a/pyglm-2.8.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77878c1b8c713b9e39fe32f870d82e20c864da3b11a3b875eca06c270b04cdbe", size = 12759345, upload-time = "2025-11-26T12:12:14.705Z" },
{ url = "https://files.pythonhosted.org/packages/a1/d1/cc1f75ee77fd2f9bcb122bc7f3a628710fe3f0bc517333fa9f38268a7065/pyglm-2.8.3-cp314-cp314-win_amd64.whl", hash = "sha256:15c77bc46ff69d945565309e13ca99c4a001d6a941a80c45f26fbdec80fa16c4", size = 1710524, upload-time = "2025-11-26T12:12:16.486Z" },
{ url = "https://files.pythonhosted.org/packages/35/51/74c0a3107567dc769c06b6c04c7bc828f33792e089036c637334b7e4c573/pyglm-2.8.3-cp314-cp314-win_arm64.whl", hash = "sha256:2b16ec33bd43c514502bae8de2b319d168259090e101e1cde79cd0a7d33e1185", size = 1271521, upload-time = "2025-11-26T12:12:17.611Z" },
{ url = "https://files.pythonhosted.org/packages/32/8e/867045b54da8257b21c71c0255ba7a231c251aae4b5f26b12eb50b651cc7/pyglm-2.8.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6a1f1ab8debc06e0fdedb3f4285ded4bec38bf075652c393039838504767e6cf", size = 1633871, upload-time = "2025-11-26T12:12:18.791Z" },
{ url = "https://files.pythonhosted.org/packages/93/f5/81bb8b52e132dc1ecc87b7ffb50c714b4fd2f71f98b801dd376e036f942b/pyglm-2.8.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a7835d18747ab9f8e736e343cc35bee0a514f18add282f1fc8035945fcf9d9bd", size = 1387757, upload-time = "2025-11-26T12:12:20.497Z" },
{ url = "https://files.pythonhosted.org/packages/32/40/8581283c00e2a18a6bb20a9192e25a792b605c53dee1c3abb8871eb0d3ca/pyglm-2.8.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:af3ddee3d150bbef68ee7338ccd3e0710b75b08121b8efd52d786b0d2b6731be", size = 12724921, upload-time = "2025-11-26T12:12:21.802Z" },
{ url = "https://files.pythonhosted.org/packages/a0/0e/bc3c03038da822d1a66c38e166d4c89b6bdab846e576ac226442813af7f4/pyglm-2.8.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a76c8eaba0c58f5738e87be5efcd16c4e75540fe6ebfdf15c236a799a61358e", size = 13317946, upload-time = "2025-11-26T12:12:23.947Z" },
{ url = "https://files.pythonhosted.org/packages/3b/21/b1f52dc73d610e36aa3ccf3ef61634530e6f97c2ca809a057839bf578667/pyglm-2.8.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:879ee9ab3c8ab47b1de59fe7e593eda854b8349f274ca60f057b83f3a405b84d", size = 11371582, upload-time = "2025-11-26T12:12:25.768Z" },
{ url = "https://files.pythonhosted.org/packages/16/aa/e03cc7a2daceb1bbdf98780a991a323e4078254584cf6664984a626ecfe1/pyglm-2.8.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:3e37c15b6c3e08f960b34ff9ec42e73469dfd868aec214e8a347da6c9d0245d6", size = 12192848, upload-time = "2025-11-26T12:12:28.606Z" },
{ url = "https://files.pythonhosted.org/packages/69/07/be893aee24a9a8b5400feda4d032fad9d281047d4379aa9f5ef4bea1f6a4/pyglm-2.8.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd34a8670debef4a55bc756b14cbe8b0a4daa49f8f6850c86c5e11d20554927f", size = 12414214, upload-time = "2025-11-26T12:12:30.438Z" },
{ url = "https://files.pythonhosted.org/packages/0c/06/6bb4e8a09f7dd2bf8dc1f4cc5edc938fc1247a1b2770833515fb418fc695/pyglm-2.8.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:699f852e0335b79d0b664ba1c2d02cb4689256cda786e7780e821f60b0824c46", size = 13237516, upload-time = "2025-11-26T12:12:32.412Z" },
{ url = "https://files.pythonhosted.org/packages/2b/5c/7f15edd05020540748dad8f5eae0a07ed7f14f699bff530172b8850bd998/pyglm-2.8.3-cp314-cp314t-win_amd64.whl", hash = "sha256:78caadaf9cc2ddea1c55b0d44fa8032f35c9f821a6f152b72422e5657d38f01d", size = 1764614, upload-time = "2025-11-26T12:12:34.498Z" },
{ url = "https://files.pythonhosted.org/packages/a7/f9/8bc8d010503a250319c55d2ceec5f90b0e807c7f85d581638c5ecd0a81de/pyglm-2.8.3-cp314-cp314t-win_arm64.whl", hash = "sha256:69400ad1852ca0972e4d9cbef9d9510941d4b81dc0fffebc5ac796a85440a119", size = 1292992, upload-time = "2025-11-26T12:12:35.681Z" },
]
[[package]]
name = "pygments"
version = "2.21.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
]
[[package]]
name = "pyobjc-core"
version = "12.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a5/78/abc4ce5920305780aeb36b4067a86253378b36e29ba96673a3deb02eb03a/pyobjc_core-12.2.2.tar.gz", hash = "sha256:3906452339cd06a3bb07df103c2511d4cb0f7a22d8771c0b802eba15d9a642b6", size = 1067701, upload-time = "2026-08-11T19:43:39.059Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/86/b2/bbf7f049880ab40d110e66f25122342a1f6c98d6fe3c59bb98985503c660/pyobjc_core-12.2.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:122e6ad302a2abf5d4d4adb0156db751600ddf2768441696cba17b31323085e7", size = 6427637, upload-time = "2026-08-11T14:51:36.038Z" },
{ url = "https://files.pythonhosted.org/packages/1b/ed/a8bf040caf3704023d74086b7fb96cf4ed2e844e24bd94e5248ba214b700/pyobjc_core-12.2.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:950bd2d9c74634398c4e3d24ef2f213d4e23d705083697464fa67afedc53c1ad", size = 6427113, upload-time = "2026-08-11T15:04:39.424Z" },
{ url = "https://files.pythonhosted.org/packages/e7/5a/760f8b9e116edd43c57e33844dc17619158fbdd311250d4209910192d72d/pyobjc_core-12.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3772b406edb3ff78171530a17cda1c4a7817f87b87ded0d8715b3fa664df16db", size = 6666817, upload-time = "2026-08-11T19:30:17.01Z" },
{ url = "https://files.pythonhosted.org/packages/13/37/486d38a173b0b8dce973a3e13c74cf402ed1b8621586b5963bc9efd49a48/pyobjc_core-12.2.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2062e8ad30a310441cd022544a897553408bebeaa7820d5edba3c96fd7fd693b", size = 6421184, upload-time = "2026-08-11T19:30:21.081Z" },
{ url = "https://files.pythonhosted.org/packages/04/f1/d138fd9b9a66ea8db56a8138b77d3413b85da3defe13363a19f364f85529/pyobjc_core-12.2.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2c7ef3d2f865b4b3ebb14ec3556f7a3e8abb6d130c67275cd9daa08dbd6e4e4e", size = 6671824, upload-time = "2026-08-11T19:30:25.005Z" },
{ url = "https://files.pythonhosted.org/packages/d5/85/577e2265cccf59daf48c460f0a8deeaf7dbe2991227a8859ab1eeab4945e/pyobjc_core-12.2.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:89acc6bc13aaa6e3f52b0ce652ede7e201edb6bf062741b246b0c5a44582f25f", size = 6477694, upload-time = "2026-08-11T19:30:28.821Z" },
{ url = "https://files.pythonhosted.org/packages/77/0a/bd9f830c64c6f334530831e75c01bfe0a770a3fbb00fddc70329223118b3/pyobjc_core-12.2.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59a77038ebe0ab1240f61c341e7fb67b8674f2b4cd41bc71a6472511a12b50f7", size = 6712233, upload-time = "2026-08-11T19:30:33.032Z" },
]
[[package]]
name = "pyobjc-framework-cocoa"
version = "12.2.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyobjc-core" },
]
sdist = { url = "https://files.pythonhosted.org/packages/75/76/49c6da2c6a831020b4854ba20079d5a1030474bffc776b7b73c2eeff8c15/pyobjc_framework_cocoa-12.2.2.tar.gz", hash = "sha256:c96c0ef69a71afbbb0e6a7d594b455c5fe47d62e0db376ee7a2b4b828c16ace9", size = 3132831, upload-time = "2026-08-11T19:44:02.288Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/2f/b67e73d8bc367e03fe7861cd9c49fff9dcfa6db83bc0630c0adcfb25b7fa/pyobjc_framework_cocoa-12.2.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e106f395531e67694376b0f1184612cbeea3ec8b9bf56b55ef41d026171d2a2d", size = 388117, upload-time = "2026-08-11T19:32:43.161Z" },
{ url = "https://files.pythonhosted.org/packages/db/e1/5d9b04ebb60042b9cb49adc2d33115e2f2c2e4ff7d548017bfaff8b7f536/pyobjc_framework_cocoa-12.2.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:600b1723184ca094931330e79355274949965460e23de38628d601b5a967baf9", size = 388181, upload-time = "2026-08-11T19:32:44.537Z" },
{ url = "https://files.pythonhosted.org/packages/b4/25/2a343357d5fe09bbe9c0e294dc03450866a0d6c1792fad36b6bcc00174c0/pyobjc_framework_cocoa-12.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:875f2aad73963faa81a6b36ae674fd494a4658d6d999e1075e0e2aca3d2391df", size = 392275, upload-time = "2026-08-11T19:32:45.631Z" },
{ url = "https://files.pythonhosted.org/packages/1f/1a/b99521999b9f54b89aad928ddff0faad507abfe33bc46599454bfa48a4b2/pyobjc_framework_cocoa-12.2.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:889d7bbd4ba2d4941078bfbbfb882138e51dbead27df006abfe0f2e0d49b5b2e", size = 388368, upload-time = "2026-08-11T19:32:46.781Z" },
{ url = "https://files.pythonhosted.org/packages/6d/26/0c697dbc73dcc76bc0f68ea5aeed25bf7b05217df5102659e878501b2d5f/pyobjc_framework_cocoa-12.2.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:de69c5933750f3a4599ed962eccd92b6a71914c7e4318dacc7895738a8ae60d7", size = 392404, upload-time = "2026-08-11T19:32:47.918Z" },
{ url = "https://files.pythonhosted.org/packages/df/82/502f740fd8f4e9ef741c9d40ba67467ab2c8196f2c09dcba12936d28a4fd/pyobjc_framework_cocoa-12.2.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:0e8ace0d44a00d281281a723d17fcd05eea7544a38a6a512e1fd018ddb7aece2", size = 388585, upload-time = "2026-08-11T19:32:49.171Z" },
{ url = "https://files.pythonhosted.org/packages/7d/3b/07ce3c0ab8d1e9e1bed74fea1bf1cce73527a365a7a23c755051d3be9865/pyobjc_framework_cocoa-12.2.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:8fe5b2e79c9530f667b4e58a87a3a15ea62f86a5d19eec405517ecbd4f454868", size = 392693, upload-time = "2026-08-11T19:32:50.283Z" },
]
[[package]]
name = "rich"
version = "15.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
]
[[package]]
name = "scipy"
version = "1.18.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7e/74/66de6258867beb2ef08f35f9f2ac017a52cacd5081714d239ff1a442d458/scipy-1.18.1.tar.gz", hash = "sha256:52c4b7422442aba924d03ad4019852b08a92e64ea187b933135687bfe2747307", size = 30781235, upload-time = "2026-08-21T23:28:50.599Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/f7/240c110c08693826b4513a52f5717d62ec7c7af72f2920821247c03b17b3/scipy-1.18.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:457fd7a2a8edeb044ab6ffbc0aa03ff6cd18491356e5e0c834d76ce621b916d1", size = 31111061, upload-time = "2026-08-21T23:23:44.522Z" },
{ url = "https://files.pythonhosted.org/packages/05/4a/78c6285577c375e7cf27277ea8ee6961224327f1e1a0c44af5f17f23635c/scipy-1.18.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:e708533e8b2ae2497d65346538a7dcc92814410b25b81432eac66de0f2af8265", size = 28733332, upload-time = "2026-08-21T23:23:50.015Z" },
{ url = "https://files.pythonhosted.org/packages/a5/f6/a5b82f8abbe14d134691b8b903696f701d25a081353a29dc655c364d9e62/scipy-1.18.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:7bbf207c4453ce1ad2e00b17313852b33310b83090c2311bdaf97f93c0380d12", size = 20475078, upload-time = "2026-08-21T23:23:54.138Z" },
{ url = "https://files.pythonhosted.org/packages/23/22/0858a0bbd6b3e825ceb8cd9baf9eaf3b2f2b1d77727eb6be40500bcdc92f/scipy-1.18.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:78c0665edead396b1abb4897c41a5c1d9bf090c8a637a4c20a61678e0a264e66", size = 23108904, upload-time = "2026-08-21T23:23:57.824Z" },
{ url = "https://files.pythonhosted.org/packages/75/9a/2e71719f31eaefe0e3a1706c4a1ded94e664bfd95ffca2b219a671faee01/scipy-1.18.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c085faa2cfa879c5141df483f836f4d691045a078224a670fa570fa01612d89", size = 34025113, upload-time = "2026-08-21T23:24:02.209Z" },
{ url = "https://files.pythonhosted.org/packages/df/64/ff35eb9e54894cf471ff4716abd3c81eb0a0626869217ce3e6ba4ccf17d7/scipy-1.18.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f55fa87b6c612ecd6b058f167c53231b1d14e412efe361d3d6e38b3631c73218", size = 35344199, upload-time = "2026-08-21T23:24:07.844Z" },
{ url = "https://files.pythonhosted.org/packages/d3/af/c5538be1792f7034c12c7db6ee67cace58253c7b87b122d68253eaf5de89/scipy-1.18.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c35d74ce0e193ff740c2f2be2ac913ddc232fe6c1ff40b26cfecb9c670c63314", size = 35639587, upload-time = "2026-08-21T23:24:13.05Z" },
{ url = "https://files.pythonhosted.org/packages/91/4c/075e4f66471bac101141ac739e9e135549be1bae584571bd03a530c056e1/scipy-1.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d2924a03db38dc2e848bca2fe9f077dafb891480b91a00a0963a8cf86dfc31c1", size = 37480330, upload-time = "2026-08-21T23:24:19.608Z" },
{ url = "https://files.pythonhosted.org/packages/39/e7/979fd14e75008623df31ba70d6bb144700f68feadcea042021c06a05bf82/scipy-1.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:5e4d44984abc0020154ea81b247adeddcc3ac5527b975ff798bd1ba0adc513c2", size = 36658278, upload-time = "2026-08-21T23:24:25.463Z" },
{ url = "https://files.pythonhosted.org/packages/c7/0b/e1525354ff9d7d5feb6d1b31af6d14072e5c91e9607b421fa1ec889660b3/scipy-1.18.1-cp312-cp312-win_arm64.whl", hash = "sha256:d65d448389b8436493abcf629cc94ad0cf32aecaf06e1acca1de53cc795f2f12", size = 24400588, upload-time = "2026-08-21T23:24:30.579Z" },
{ url = "https://files.pythonhosted.org/packages/b6/55/4540ee0f9c42a9ad7109d0d1a8cc70de54c3572b01c6693a2b1c70e90ceb/scipy-1.18.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:3ab3523da44749156e1f68b464dc56af11ae4cbc5c739a49d05f32b982eca9f3", size = 31089958, upload-time = "2026-08-21T23:24:35.8Z" },
{ url = "https://files.pythonhosted.org/packages/2a/f5/769f36d14922b8071a43e95d24d18b6bdafad10d7f5cf647867e1ac052bc/scipy-1.18.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6fb6a55cc0ba97b59a1f288fb86dc6fce8bdfc0fffcbfd015e3a954bf2a2d93", size = 28715106, upload-time = "2026-08-21T23:24:40.775Z" },
{ url = "https://files.pythonhosted.org/packages/9a/d7/21d890274f75ea37a8209d5519e72da3da90302e3b9fb8397a0918386a62/scipy-1.18.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ea324d9dd34c38bfb9bec8ca4d1b407db97dbb74029f566b8e322b1b6fe56fe6", size = 20456846, upload-time = "2026-08-21T23:24:45.066Z" },
{ url = "https://files.pythonhosted.org/packages/ec/01/798430ecea2e78ec7c02663d5f71c007bb6abeca931080debd40d7fa55ea/scipy-1.18.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:75b00eb8fb802090aa903f4ea1c7f5a584779f967361e68b7e98e531cc2d7174", size = 23087986, upload-time = "2026-08-21T23:24:49.539Z" },
{ url = "https://files.pythonhosted.org/packages/e6/5f/4634e9d35c68496e4e34cb6946eafab044458e6cedab42b40b6588e475b6/scipy-1.18.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d416b16cccfd70fbf62400e84d0bb2f4e6af519a45557f1692c749b37f14b315", size = 33998146, upload-time = "2026-08-21T23:24:54.714Z" },
{ url = "https://files.pythonhosted.org/packages/41/48/6450ed9243315322bbc19ac57b9b70d66a20bf1d38d124c96bc4bf6af9ea/scipy-1.18.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fdaf5ea890a6183d0565f51a61799d67081bd5b1cf03c5f4b3fd3732108625c9", size = 35312578, upload-time = "2026-08-21T23:25:00.44Z" },
{ url = "https://files.pythonhosted.org/packages/00/bd/bf5a4be6a3525676499f6dff307991739ff6fdcad1481b1aeb6745339f58/scipy-1.18.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c825cef2f49e46753726a7181a8e199804a912b29519ada542c6ebc654951899", size = 35612621, upload-time = "2026-08-21T23:25:06.144Z" },
{ url = "https://files.pythonhosted.org/packages/bd/4e/3c45c33e00a77996c4b1cb707929f833ba7b1d522ee29f882512c330676d/scipy-1.18.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3b417bf8c2c7c16e8f58ad91db17783ec911ac16e7b50eb6eab6e809b4f5b07", size = 37457323, upload-time = "2026-08-21T23:25:12.483Z" },
{ url = "https://files.pythonhosted.org/packages/93/0e/e0348fbc0dbab65c114cf78957e7dfeb49f8e8b556b4d930cc12ff195e18/scipy-1.18.1-cp313-cp313-win_amd64.whl", hash = "sha256:559ed65f60c1af5a03f3912605a1b5114f522c7c32fb23c3376ae8f03219fe28", size = 36622841, upload-time = "2026-08-21T23:25:18.722Z" },
{ url = "https://files.pythonhosted.org/packages/50/a8/6a77f5f267c555108f0a864b6db714363dab567a8266422a79a385f9232b/scipy-1.18.1-cp313-cp313-win_arm64.whl", hash = "sha256:cd479fc04dd9401e3b4f49e76518768ef99c4f517a98c284eb091fd725719adf", size = 24399315, upload-time = "2026-08-21T23:25:23.458Z" },
{ url = "https://files.pythonhosted.org/packages/06/d5/d8eb4e280ddb56a4ab2c6f02ee49b56b23f6e977cf0802fd6d68dbef14f5/scipy-1.18.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:83de5453a7799afc9048b4616bd085cef126e36412f0ea2f6370c36a2a3a51e7", size = 31090936, upload-time = "2026-08-21T23:25:28.686Z" },
{ url = "https://files.pythonhosted.org/packages/2a/49/59ea385dc3a62ff498ddf3cfff7c2b41b0f9f9d3c4122b3f1dcb6d6327fe/scipy-1.18.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9554bcc6d715ee87a633a3cc8e7703c6628b100dd29cb8a2efc4c0533c7ff729", size = 28725221, upload-time = "2026-08-21T23:25:33.244Z" },
{ url = "https://files.pythonhosted.org/packages/70/e8/6b0c288c50942d78193696c9f15f9a0874f5178aa0ddf40f83d9924b3e8d/scipy-1.18.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:011413b7426b75012840e35649e00fe0a2c3bae89fed433876e3a99251572efc", size = 20466839, upload-time = "2026-08-21T23:25:37.516Z" },
{ url = "https://files.pythonhosted.org/packages/4b/e0/54fd3793c729e3b936782f181b59cbb1205bf250ab605a16cb1ba61cdd5e/scipy-1.18.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:88f0e784020649f88ea48c9f5ddfa403bf9205820667c0914740b392035afb82", size = 23089121, upload-time = "2026-08-21T23:25:42.019Z" },
{ url = "https://files.pythonhosted.org/packages/0b/56/030af62bea3cf878e0028515dff78c123b01633606a879b63f42d2db99cc/scipy-1.18.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d3ab0e8c69a17dd3559eab8cbb88f258e285c94d572c2719033f90f83290c89", size = 34053851, upload-time = "2026-08-21T23:25:47.998Z" },
{ url = "https://files.pythonhosted.org/packages/6b/89/2a844506d49651e9aa1af6ef95b6bd8031cb1d5a4375edec6155037e04cf/scipy-1.18.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac0333bdf38309aa3dcbe7e3fa7ea29e7a2c37c6ea306a757b700ded8e4596ad", size = 35329183, upload-time = "2026-08-21T23:25:53.522Z" },
{ url = "https://files.pythonhosted.org/packages/eb/56/c7370c3640e92ac9613cbf26cb3f729f9b12ddf1727b55b94b53b24d6f48/scipy-1.18.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:911de823097db8b63f034299d12662db93344e6ffa0b881cbb57748974b70168", size = 35672551, upload-time = "2026-08-21T23:25:59.387Z" },
{ url = "https://files.pythonhosted.org/packages/24/16/ec8536f351421f8bf60a1120930638f83790f4710b8230446aca3d6159d4/scipy-1.18.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:95298364e251be3e60249facbeeca03631d3bb7584f85879516ec55ac717b81f", size = 37469416, upload-time = "2026-08-21T23:26:05.432Z" },
{ url = "https://files.pythonhosted.org/packages/52/94/d73da0d28f16c45bb9b0a5691b91610b0275c5ef0eb5e43c87cf2dc1bf31/scipy-1.18.1-cp314-cp314-win_amd64.whl", hash = "sha256:78a0d7c918e74a232394117160e7e3db503377572a45bcef8826e4ab8a35feba", size = 37362755, upload-time = "2026-08-21T23:26:11.366Z" },
{ url = "https://files.pythonhosted.org/packages/89/25/e996e4dc74e10e227b1e14db5eaf6608bb6dd33884a64851c38f18dd4249/scipy-1.18.1-cp314-cp314-win_arm64.whl", hash = "sha256:cbf38d043c1aa4ab306e1ada6ab6eddacc3322a20b7af1b30bc93254b366fe09", size = 25036090, upload-time = "2026-08-21T23:26:15.887Z" },
{ url = "https://files.pythonhosted.org/packages/fa/c9/c00213f92309d753b48903e6a451b87eb52ff5b7a16e789d1568bbf221c4/scipy-1.18.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0fcb3c93519f27bb4f0c4b0f7802cdcaca7fcf93267b75edda2e9f4e8a55cbd7", size = 31485550, upload-time = "2026-08-21T23:26:20.776Z" },
{ url = "https://files.pythonhosted.org/packages/74/b2/e3067c487982d4eeab2938928529410370c06fea84a4d3f4925e7d96647d/scipy-1.18.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ddef79fb382df40104a19bb7151b3b23e57c1778fcf857c71ceecd9bd264513f", size = 29174642, upload-time = "2026-08-21T23:26:25.395Z" },
{ url = "https://files.pythonhosted.org/packages/d5/ab/374c9fe2d1ec014e576c781a4b5d8e1ba340e8f6b4638c16f711d2b194f0/scipy-1.18.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0e82073ecc7acc6436fac4b31674109c7e1d3e596789767eda01258a8c9e8123", size = 20916357, upload-time = "2026-08-21T23:26:30.112Z" },
{ url = "https://files.pythonhosted.org/packages/90/38/223915c88a17317cafbf8ca2a42b11c265a9fb1e804aa665544132b5fe8a/scipy-1.18.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8bcf3c1ba5d6456e2effd30fcbd3459b044d683fcdac79a2e6830f0bdf7de487", size = 23482611, upload-time = "2026-08-21T23:26:34.846Z" },
{ url = "https://files.pythonhosted.org/packages/c4/d1/db0948da8ca57a80b36520ef0a768b967d99f3af65f4b6f1bf6362ad4dd4/scipy-1.18.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cfbf154f2ba187f2ed6cce2639efff7d105f1140573642c0161615b6d91d6a87", size = 34143202, upload-time = "2026-08-21T23:26:40.4Z" },
{ url = "https://files.pythonhosted.org/packages/87/53/39d046cc7574ed6acacb6bd5723e220107ece80bff12faaf3efc4ddeede4/scipy-1.18.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1d33a7836f7ddc1993427966a0823468ec41bcbdb1a9f9942d1d7e57f803ba3", size = 35380876, upload-time = "2026-08-21T23:26:46.1Z" },
{ url = "https://files.pythonhosted.org/packages/f9/da/32e0e799d875a85ca57d9bde6c78148afcc0e38276df683d95854eadc8c3/scipy-1.18.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4b8bc363b6d65ee2152bec57568e3c52639bb34c46057b09857a307ed5e21d", size = 35770885, upload-time = "2026-08-21T23:26:51.533Z" },
{ url = "https://files.pythonhosted.org/packages/88/2e/f97a666d362fee68b18f41c9c30ed502ca5c98b549749bfcb52a8b74d1eb/scipy-1.18.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11c423f1049c5755ad4409af52a9ada1cff96fe9b50795d4af3619f292901239", size = 37525424, upload-time = "2026-08-21T23:26:56.751Z" },
{ url = "https://files.pythonhosted.org/packages/ca/d5/a9e765a84654ebba8479a1fd1b059ced1af72b168a3b2a3a46540ea38d20/scipy-1.18.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c24acac1e18912761c4700239bbc1fd32f615af690f1584d49b35859be51324d", size = 37416961, upload-time = "2026-08-21T23:27:01.546Z" },
{ url = "https://files.pythonhosted.org/packages/ee/16/e79e0d1c63ef698879d85439d37e9fb434e3b804e506a6991038d086ebd9/scipy-1.18.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9f2897bf7737392ad0d5213ea7b6add72a4edf5679b3153106aeb88b6507b3b9", size = 25331848, upload-time = "2026-08-21T23:27:05.884Z" },
{ url = "https://files.pythonhosted.org/packages/be/4f/1bd37c883b67163e2ca1f60977a399500e6879c15defecac62831c8d078d/scipy-1.18.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:eb0dfcf4e28a99c12c999744a2ff67c9b06200e20401c7c88186e33552a46331", size = 31091484, upload-time = "2026-08-21T23:27:11.051Z" },
{ url = "https://files.pythonhosted.org/packages/8c/c5/ba929d7feb9b2332f96827c12e0e924b61973b59b4dea383b603372c65ce/scipy-1.18.1-cp315-cp315-macosx_12_0_arm64.whl", hash = "sha256:30f464bee641fa8e282577c7dce027308403213c6ca8270bba73285c91024bc5", size = 28725057, upload-time = "2026-08-21T23:27:15.9Z" },
{ url = "https://files.pythonhosted.org/packages/a4/19/68f1c50f609d955d230e66d25d02bd3e1e167ec540232135354fb9a4b9e3/scipy-1.18.1-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:1bca3b943fc2567ea49cd02c99abde49da4d5178ec46f624bd8255cda8755beb", size = 20466734, upload-time = "2026-08-21T23:27:20.044Z" },
{ url = "https://files.pythonhosted.org/packages/ef/6d/319fa29b73d1802fa80b32a6eaf3f5be456ef81526da2716a9493bcb5501/scipy-1.18.1-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:c9d18a33309122074ea483dd92dd444189166b8b2ec429fe9ed5ac73c7a0aa23", size = 23089664, upload-time = "2026-08-21T23:27:24.345Z" },
{ url = "https://files.pythonhosted.org/packages/b7/db/30992f9b51a63de671daf3888ffd18378b6cb9ec9f2c972264238ffa7fd6/scipy-1.18.1-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82f201b4c878551d48558337aab270d3c6cca5507b8737c8d8a608d234cccde0", size = 34054035, upload-time = "2026-08-21T23:27:29.409Z" },
{ url = "https://files.pythonhosted.org/packages/91/d4/bf3e735dc0b9d5a8ff45079d2540e17d3aff7a2f0048dd8f552ffd031d2b/scipy-1.18.1-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ac49ea97594532dd44b7136094d35f5440fa06e6d9c6384a74c01764df388c5", size = 35333883, upload-time = "2026-08-21T23:27:34.293Z" },
{ url = "https://files.pythonhosted.org/packages/19/93/12d78ce9f871fe945fca588d32644e6e63f553c2a35c564d73f3b22a3313/scipy-1.18.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:ceb30a00ce7c92d459819443d29ca486d882b83fb6738bdcbb2a1cce94ac5daa", size = 35673124, upload-time = "2026-08-21T23:27:39.059Z" },
{ url = "https://files.pythonhosted.org/packages/70/cd/886219313a1012a48e6ae0ec4f302c837151beb92e1ff0d709ef8fdfc488/scipy-1.18.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f29633129f9fa7e88a3f0fca835de2d030bfc9643f7799e1a0c46cee24d38fc7", size = 37470753, upload-time = "2026-08-21T23:27:44.435Z" },
{ url = "https://files.pythonhosted.org/packages/17/6c/a776888ce618bee54fbde26172f0f46ac1da70d27b63861797fe78e1904b/scipy-1.18.1-cp315-cp315-win_amd64.whl", hash = "sha256:92c14f5bdbfb6216315ce33e78080474082de8b3830122ba97809bfbe65f75c0", size = 37361483, upload-time = "2026-08-21T23:27:49.334Z" },
{ url = "https://files.pythonhosted.org/packages/ab/09/97b651691322ebee97999b017ffc18a15a0b815103844c97e8da9d469731/scipy-1.18.1-cp315-cp315-win_arm64.whl", hash = "sha256:e402cf31eb68f453dbb2d36fc6d722b33f24a55d68b2ae1d92fa6305ca71c298", size = 25035883, upload-time = "2026-08-21T23:27:53.596Z" },
{ url = "https://files.pythonhosted.org/packages/ed/0f/9ec20467bbabd0d44e2a77d0fd3d124f884b4d67df92af82c91d2d6a486f/scipy-1.18.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2a0b02f9fc46f8520330c23d45e6560db7e3a0d927232139427637f98943e11d", size = 31474926, upload-time = "2026-08-21T23:27:57.993Z" },
{ url = "https://files.pythonhosted.org/packages/8a/58/dcb79161e56efbedc50079fcd2f5fe427a0ebb53022eb476aa73c015ad8f/scipy-1.18.1-cp315-cp315t-macosx_12_0_arm64.whl", hash = "sha256:1d73131e358976663dd969e1fb4ed1404b815cd977eaaedc3b3a133ba2d81c35", size = 29164940, upload-time = "2026-08-21T23:28:03.062Z" },
{ url = "https://files.pythonhosted.org/packages/71/d3/1eeea80c817fcb8ef7bd4a05a58824977a0e57a375cfc3d7ea7c911c01ad/scipy-1.18.1-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:bff0b729edd992766136b34e39cc76bc2fad905aa58897ee72a9cd000a6d8443", size = 20906742, upload-time = "2026-08-21T23:28:07.642Z" },
{ url = "https://files.pythonhosted.org/packages/54/46/e59350428b6099301a20128108c995e2eb175a43f383af9a346e38824f9b/scipy-1.18.1-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:10ac20c69d880f77f375db44c22e3e6a644f9fefa291d4cd2fb9790a89fc99fd", size = 23472183, upload-time = "2026-08-21T23:28:12.109Z" },
{ url = "https://files.pythonhosted.org/packages/89/31/cc91623fa98f0621766a0f0aaaadb2c66de74a7ea7e3837164f6e4354260/scipy-1.18.1-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33a834464fdabc0f26a45508df31b3cc5d028e04dbf6c5ed398541418e0a12fe", size = 34130796, upload-time = "2026-08-21T23:28:17.906Z" },
{ url = "https://files.pythonhosted.org/packages/fc/3e/8572ef536957ddb8aa81bb4090d9e25f257e3b4e05d97deb54319deb8a3a/scipy-1.18.1-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49023963c193dacee096301452f223ee24d86ec5807f8df93c0f7221d119e305", size = 35374253, upload-time = "2026-08-21T23:28:23.732Z" },
{ url = "https://files.pythonhosted.org/packages/b5/c6/59fdeffb4f1435299f93d9dc8140b43ad2916e6cfc944be6c3041fcec86d/scipy-1.18.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:d84a09d0dad90ba6525d8ac1c2334b33e64bf3ccfe9e841f02feb867a22681e4", size = 35758543, upload-time = "2026-08-21T23:28:29.431Z" },
{ url = "https://files.pythonhosted.org/packages/cf/d9/135be205d9de8783193aff9cc3bf483a03a38e4b29432c954e8cb66ac14e/scipy-1.18.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:179ce34a8d0fe273d8883ba59e17e052247d08973dfcb743ca52bb1cce2d60b0", size = 37521946, upload-time = "2026-08-21T23:28:35.245Z" },
{ url = "https://files.pythonhosted.org/packages/5c/a2/5b7d5270621ab7cfa3f7766067bf95dc360b5efb6394694e8143b4156e2b/scipy-1.18.1-cp315-cp315t-win_amd64.whl", hash = "sha256:5632e3ae3d09197c446310cd5187de63e28448ce22f0f67b2b93d97503c0c230", size = 37408295, upload-time = "2026-08-21T23:28:40.724Z" },
{ url = "https://files.pythonhosted.org/packages/63/ad/741c19fcb66755ff953daf9243af8480e4bf3d7fbe57583c178c7d2b6b51/scipy-1.18.1-cp315-cp315t-win_arm64.whl", hash = "sha256:eda632a7981f69730d6281f451db9c1c370993a2c0d7ddb43e2a809a2862b83a", size = 25319710, upload-time = "2026-08-21T23:28:45.713Z" },
]
[[package]]
name = "screeninfo"
version = "0.8.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cython", marker = "sys_platform == 'darwin'" },
{ name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ec/bb/e69e5e628d43f118e0af4fc063c20058faa8635c95a1296764acc8167e27/screeninfo-0.8.1.tar.gz", hash = "sha256:9983076bcc7e34402a1a9e4d7dabf3729411fd2abb3f3b4be7eba73519cd2ed1", size = 10666, upload-time = "2022-09-09T11:35:23.419Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/bf/c5205d480307bef660e56544b9e3d7ff687da776abb30c9cb3f330887570/screeninfo-0.8.1-py3-none-any.whl", hash = "sha256:e97d6b173856edcfa3bd282f81deb528188aff14b11ec3e195584e7641be733c", size = 12907, upload-time = "2022-09-09T11:35:21.351Z" },
]
[[package]]
name = "skia-pathops"
version = "0.9.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4a/f6/ab37d6fa21f25965d4ad059745c76f13ddfb92a2c06a842a42ad77961c24/skia_pathops-0.9.2.tar.gz", hash = "sha256:4b6d8459f6f4a69282cb26fca0c2bb0b321cc58a9bf9cc6579a52a391edc0319", size = 65115346, upload-time = "2026-02-16T10:30:22.753Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/75/10/45af3ea2dee706a340601da315e450c449f76aaa18651cfe2677b0c65161/skia_pathops-0.9.2-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:c7a925b919c050df1e0545d26a69b64d9c8e1959e4dca73b06df088bf01d30f0", size = 2897494, upload-time = "2026-02-16T10:30:00.265Z" },
{ url = "https://files.pythonhosted.org/packages/b4/20/711d733b252696aedd78175e0f60acd42299de680d5a50a2750b96578841/skia_pathops-0.9.2-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d94c18a60b1e56910240a65dcc841cca37067f6432a4881d20baff17b6d77915", size = 3300314, upload-time = "2026-02-16T10:30:02.53Z" },
{ url = "https://files.pythonhosted.org/packages/95/47/bccbdb7ee12bbcc43c1051f9fd6933d16e3ffc8f20263155bd03709bde9e/skia_pathops-0.9.2-cp310-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b28883f209a0df84da14e980e7a07c55ed93b913955694d90ddef2f29a261b", size = 2999370, upload-time = "2026-02-16T10:30:04.445Z" },
{ url = "https://files.pythonhosted.org/packages/5e/55/d5a1925d61ab683aaf08a3a80749371991f82fdeb53e3dbeae0046ce80ac/skia_pathops-0.9.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8122e50e03472657781c951f3019d9b85d9b1cbc93ebac635b673f99b18895b6", size = 3955374, upload-time = "2026-02-16T10:30:06.308Z" },
{ url = "https://files.pythonhosted.org/packages/36/49/c81e1a551726b8153c7a600b041e4480ac85db13985b7b260a01d85e0a6c/skia_pathops-0.9.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f8c72c70e57dd6655f880ae63642ff26753bb888f4c6411709a12cc10bee005e", size = 4316667, upload-time = "2026-02-16T10:30:08.326Z" },
{ url = "https://files.pythonhosted.org/packages/bb/b4/484ba09325c75d4470578b3f16bfe5a30cc348ece2566df59702c487084c/skia_pathops-0.9.2-cp310-abi3-win32.whl", hash = "sha256:3b9ef6629d2c8cb8604a7e22d4d2fe2c0160788b9792145a2cf0f968ec245751", size = 1450912, upload-time = "2026-02-16T10:30:09.762Z" },
{ url = "https://files.pythonhosted.org/packages/ff/96/bd1bfaf5335242a86d8af940ff2124861e9bc6a5520cf96fda374fd10945/skia_pathops-0.9.2-cp310-abi3-win_amd64.whl", hash = "sha256:9ae72d801b5f4c5dbb937fb25476a12fa2da70860f28dd637da2b529dbd8271c", size = 1782901, upload-time = "2026-02-16T10:30:11.546Z" },
]
[[package]]
name = "slime-research-video"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "av" },
{ name = "imageio-ffmpeg" },
{ name = "manim" },
{ name = "pillow" },
]
[package.metadata]
requires-dist = [
{ name = "av", specifier = ">=19.0.1" },
{ name = "imageio-ffmpeg", specifier = ">=0.6.0" },
{ name = "manim", specifier = ">=0.22.0" },
{ name = "pillow", specifier = ">=12.3.0" },
]
[[package]]
name = "soupsieve"
version = "2.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/71/c3/1b817965ac12dc002d7c9cd7dfffdd7d4fbf9b45763ef2ffe7b86ee94670/soupsieve-2.10.tar.gz", hash = "sha256:49e9380d7d2905463583bafe285e818c7366a9ed7b3aee221c1ac79c905d8bc0", size = 124337, upload-time = "2026-09-24T02:36:35.048Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/66/87/5ed59e1d0290564e2027ed066c52b059892c4637741337fc0967183c8d4d/soupsieve-2.10-py3-none-any.whl", hash = "sha256:8596eb8967d744174820280fa62b4542a2e955bfaccca73ed8a13c6eb8e9b502", size = 38019, upload-time = "2026-09-24T02:36:33.709Z" },
]
[[package]]
name = "srt"
version = "3.5.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/66/b7/4a1bc231e0681ebf339337b0cd05b91dc6a0d701fa852bb812e244b7a030/srt-3.5.3.tar.gz", hash = "sha256:4884315043a4f0740fd1f878ed6caa376ac06d70e135f306a6dc44632eed0cc0", size = 28296, upload-time = "2023-03-28T02:35:44.007Z" }
[[package]]
name = "svgelements"
version = "1.9.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5d/29/1c93c94a2289675ba2ff898612f9c9a03f46d69f253bdf4da0dfc08a599d/svgelements-1.9.6.tar.gz", hash = "sha256:7c02ad6404cd3d1771fd50e40fbfc0550b0893933466f86a6eb815f3ba3f37f7", size = 162145, upload-time = "2023-08-17T02:01:51.822Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/2c/6c9bb53db56c8a12a736d2158a8b842a5993b96daabc29d90a098e840280/svgelements-1.9.6-py2.py3-none-any.whl", hash = "sha256:8a5cf2cc066d98e713d5b875b1d6e5eeb9b92e855e835ebd7caab2713ae1dcad", size = 137856, upload-time = "2023-08-17T02:01:48.76Z" },
]
[[package]]
name = "tqdm"
version = "4.70.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0d/ea/b2a5bd54b28a324dae8211928b2d730b6547500342c7e6c6dea08bd0a485/tqdm-4.70.1.tar.gz", hash = "sha256:cefd0eca11b2a37a3aee776544d4f4ae913f02688135b5556b8788dfa474afc4", size = 171846, upload-time = "2026-09-11T07:25:16.601Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/03/921a3d3c75785aca9ebfbfcabfbc3a1be12e2ab5265deb026d55a5a3f83e/tqdm-4.70.1-py3-none-any.whl", hash = "sha256:c293e525e6fef9c20e8728fd4612df02a0aa31bb5fe91ecd93e123b1b7bffa73", size = 80199, upload-time = "2026-09-11T07:25:14.599Z" },
]
[[package]]
name = "typing-extensions"
version = "4.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
]
[[package]]
name = "watchdog"
version = "6.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" },
{ url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" },
{ url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" },
{ url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" },
{ url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" },
{ url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" },
{ url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
{ url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
{ url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
{ url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
{ url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
{ url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
{ url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
{ url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
{ url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
{ url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
]

36
show/video/verify.py Normal file
View file

@ -0,0 +1,36 @@
"""Check rendered media, matching captions, and frozen benchmark provenance."""
import argparse
import hashlib
import json
from pathlib import Path
import av
HERE = Path(__file__).resolve().parent
parser = argparse.ArgumentParser()
parser.add_argument('--quality', default='final', choices=['preview', 'final'])
parser.add_argument('--scene', default='SlimeFilm')
args = parser.parse_args()
name = f'{args.scene}-{args.quality}'
report = json.loads((HERE / f'output/{name}.render.json').read_text())
timeline = json.loads((HERE / f'output/{name}.timeline.json').read_text())
for source, digest in report['source_sha256'].items():
assert hashlib.sha256((HERE/source).read_bytes()).hexdigest() == digest, f'changed after render: {source}'
data = json.loads((HERE/'data/benchmarks.json').read_text())
for record in (data,):
path = HERE/record['source']
assert hashlib.sha256(path.read_bytes()).hexdigest() == record['source_sha256'], f'benchmark source changed: {path}'
for cue in timeline['cues']:
assert 0 <= cue['start'] < cue['end'] <= timeline['duration']+.1
for a,b in zip(timeline['cues'],timeline['cues'][1:]):
assert a['end'] <= b['start']+.01, 'overlapping captions'
assert len(timeline['cues']) == report['caption_count']
with av.open(str(HERE/'output'/report['file'])) as container:
stream = container.streams.video[0]
expected = (854,480) if args.quality == 'preview' else (1920,1080)
assert (stream.width,stream.height) == expected
duration = float(stream.duration*stream.time_base)
assert abs(duration-timeline['duration']) < .15, 'video and caption timelines differ'
frames = sum(1 for _ in container.decode(stream))
assert frames > 0
print(f'OK: {report["file"]}; {frames} decoded frames; {len(timeline["cues"])} captions; source hashes match')