This commit is contained in:
2025-09-26 16:55:57 +08:00
parent 57a7e9216e
commit 0597239207
12 changed files with 1412 additions and 4 deletions

View File

@@ -0,0 +1,8 @@
"""
Cross Validation module for EHS winrate data validation
"""
from .cross_validation import DataValidator
from .parse_data import XTaskDataParser
__all__ = ['DataValidator', 'XTaskDataParser']

View File

@@ -0,0 +1,357 @@
#!/usr/bin/env python3
import numpy as np
from typing import List, Dict, Tuple
from scipy.stats import wasserstein_distance
import sys
from .parse_data import XTaskDataParser
from shortdeck.gen_hist import ShortDeckHistGenerator
class DataValidator:
def __init__(self, data_path: str = "ehs_data"):
self.parser = XTaskDataParser(data_path)
self.generator = ShortDeckHistGenerator()
print(" DataValidator初始化完成")
print(f" 生成器短牌型大小: {len(self.generator.full_deck)}")
def validate_river_samples(self, max_samples: int = 20) :
# print(f"\n 验证river_EHS样本 (最大样本数: {max_samples})")
try:
print(" 解析导出的river数据...")
print('='*60)
river_records = self.parser.parse_river_ehs_with_cards()
if not river_records:
return {'error': '没有解析到river记录', 'success': False}
sample_records = np.random.choice(river_records, size=max_samples, replace=False)
print(f" 选择 {len(sample_records)} 个样本进行验证")
matches = 0
errors = 0
differences = []
for i, record in enumerate(sample_records):
try:
player_cards = record.player_cards
board_cards = record.board_cards
src_river_ehs = record.ehs
cur_river_ehs = self.generator.generate_river_ehs(player_cards, board_cards)
ehs_difference = abs(src_river_ehs - cur_river_ehs)
player_str = " ".join(str(c) for c in player_cards)
board_str = " ".join(str(c) for c in board_cards)
print(f" 样本 {i+1}: [{player_str}] + [{board_str}]")
print(f" 原始EHS: {src_river_ehs:.6f}")
print(f" 重算EHS: {cur_river_ehs:.6f}")
print(f" 差异: {ehs_difference:.6f}")
# 判断匹配 (允许小的数值差异)
tolerance = 1e-6
if ehs_difference < tolerance:
matches += 1
else:
differences.append(ehs_difference)
except Exception as e:
errors += 1
if errors <= 3:
print(f" 样本 {i+1} 计算失败: {e}")
# 统计结果
total_samples = len(sample_records)
match_rate = matches / total_samples if total_samples > 0 else 0
mean_diff = np.mean(differences) if differences else 0
max_diff = np.max(differences) if differences else 0
result = {
'total_samples': total_samples,
'matches': matches,
'match_rate': match_rate,
'mean_difference': mean_diff,
'max_difference': max_diff,
'errors': errors,
'success': match_rate > 0.8 and mean_diff < 0.05
}
print(f" River验证完成:")
print('='*60)
print(f" 匹配数: {matches}/{total_samples} ({match_rate:.1%})")
print(f" 平均差异: {mean_diff:.6f}")
print(f" 最大差异: {max_diff:.6f}")
return result
except Exception as e:
print(f" River验证失败: {e}")
return {'error': str(e), 'success': False}
def validate_turn_samples(self, max_samples: int = 10):
# print(f"\n 验证turn_HIST样本 (最大样本数: {max_samples})")
try:
print(" 解析导出的Turn数据...")
print('='*60)
print('='*60)
turn_records = self.parser.parse_turn_hist_with_cards(max_records=max_samples)
if not turn_records:
return {'error': '没有解析到Turn记录', 'success': False}
print(f" 解析到 {len(turn_records)} 个Turn样本")
low_emd_count = 0
emd_distances = []
errors = 0
for i, record in enumerate(turn_records):
try:
player_cards = record.player_cards
board_cards = record.board_cards
src_hist = np.array(record.bins)
cur_hist = self.generator.generate_turn_histogram(
player_cards, board_cards, num_bins=len(src_hist)
)
cur_hist = np.array(cur_hist)
# 归一化
src_hist_norm = src_hist / src_hist.sum() if src_hist.sum() > 0 else src_hist
cur_hist_norm = cur_hist / cur_hist.sum() if cur_hist.sum() > 0 else cur_hist
# 计算EMD距离
emd_dist = wasserstein_distance(
range(len(src_hist_norm)),
range(len(cur_hist_norm)),
src_hist_norm,
cur_hist_norm
)
is_low_emd = emd_dist < 0.2
if is_low_emd:
low_emd_count += 1
# 显示详细信息前3个样本
if i < 3:
player_str = " ".join(str(c) for c in player_cards)
board_str = " ".join(str(c) for c in board_cards)
print(f" 样本 {i+1}: [{player_str}] + [{board_str}]")
print(f" 原始直方图: bins={len(src_hist)}, sum={src_hist.sum():.3f}, 非零bins={np.count_nonzero(src_hist)}")
print(f" 生成直方图: bins={len(cur_hist)}, sum={cur_hist.sum():.3f}, 非零bins={np.count_nonzero(cur_hist)}")
print(f" 归一化后EMD距离: {emd_dist:.6f}")
except Exception as e:
errors += 1
if errors <= 3:
print(f" 样本 {i+1} 计算失败: {e}")
# 统计结果
total_samples = len(turn_records)
low_emd_rate = low_emd_count / total_samples if total_samples > 0 else 0
mean_emd = np.mean(emd_distances) if emd_distances else float('inf')
result = {
'total_samples': total_samples,
'low_emd_count': low_emd_count,
'low_emd_rate': low_emd_rate,
'mean_emd_distance': mean_emd,
'emd_distances': emd_distances,
'errors': errors,
'success': low_emd_rate > 0.6 and mean_emd < 0.5
}
print(f" Turn验证完成:")
print(f" 低EMD数: {low_emd_count}/{total_samples} ({low_emd_rate:.1%})")
print(f" 平均EMD: {mean_emd:.6f}")
return result
except Exception as e:
print(f" Turn验证失败: {e}")
return {'error': str(e), 'success': False}
def validate_flop_samples(self, max_samples: int = 5):
# print(f"\n 验证Flop直方图样本 (最大样本数: {max_samples})")
try:
print(" 解析导出Flop数据...")
print('='*60)
print('='*60)
print('='*60)
flop_records = self.parser.parse_flop_hist_with_cards(max_records=max_samples)
if not flop_records:
return {'error': '没有解析到Flop记录', 'success': False}
print(f" 解析到 {len(flop_records)} 个Flop样本")
low_emd_count = 0
emd_distances = []
errors = 0
for i, record in enumerate(flop_records):
try:
print(f" 处理样本 {i+1}/{len(flop_records)}...")
player_cards = record.player_cards
board_cards = record.board_cards
src_hist = np.array(record.bins)
cur_hist = self.generator.generate_flop_histogram(
player_cards, board_cards, num_bins=len(src_hist)
)
cur_hist = np.array(cur_hist)
src_hist_norm = src_hist / src_hist.sum() if src_hist.sum() > 0 else src_hist
cur_hist_norm = cur_hist / cur_hist.sum() if cur_hist.sum() > 0 else cur_hist
# 计算EMD距离
emd_dist = wasserstein_distance(
range(len(src_hist_norm)),
range(len(cur_hist_norm)),
src_hist_norm,
cur_hist_norm
)
# emd_distances.append(emd_dist)
# is_low_emd = emd_dist < 0.2 # EMD阈值
# if is_low_emd:
# low_emd_count += 1
# 显示详细信息
player_str = " ".join(str(c) for c in player_cards)
board_str = " ".join(str(c) for c in board_cards)
print(f" 样本 {i+1}: [{player_str}] + [{board_str}]")
print(f" 原始直方图: bins={len(src_hist)}, sum={src_hist.sum():.3f}, 非零bins={np.count_nonzero(src_hist)}")
print(f" 生成直方图: bins={len(cur_hist)}, sum={cur_hist.sum():.3f}, 非零bins={np.count_nonzero(cur_hist)}")
print(f" 归一化后EMD距离: {emd_dist:.6f}")
except Exception as e:
errors += 1
if errors <= 3:
print(f" 样本 {i+1} 计算失败: {e}")
# 统计结果
total_samples = len(flop_records)
low_emd_rate = low_emd_count / total_samples if total_samples > 0 else 0
mean_emd = np.mean(emd_distances) if emd_distances else float('inf')
result = {
'total_samples': total_samples,
'low_emd_count': low_emd_count,
'low_emd_rate': low_emd_rate,
'mean_emd_distance': mean_emd,
'emd_distances': emd_distances,
'errors': errors,
'success': low_emd_rate > 0.4 and mean_emd < 0.5
}
print(f" Flop验证完成:")
print(f" 低EMD数: {low_emd_count}/{total_samples} ({low_emd_rate:.1%})")
print(f" 平均EMD: {mean_emd:.6f}")
return result
except Exception as e:
print(f" Flop验证失败: {e}")
return {'error': str(e), 'success': False}
def run_full_validation(self, river_samples: int = 20, turn_samples: int = 10, flop_samples: int = 5) -> Dict:
print(" 导出数据EHS验证")
print("*"*60)
print("验证策略: 从xtask导出数据中抽取牌面 → 短牌型生成器重计算 → 比较一致性")
# 执行各阶段验证
results = {}
results['river'] = self.validate_river_samples(river_samples)
results['turn'] = self.validate_turn_samples(turn_samples)
results['flop'] = self.validate_flop_samples(flop_samples)
print(f"\n{'='*60}")
print(" 验证完毕")
print(f"{'='*60}")
passed_stages = 0
total_stages = 3
# River结果
print(f"\n RIVER阶段:")
if 'error' not in results['river']:
status = " 通过" if results['river']['success'] else " 失败"
print(f" 验证结果: {status}")
print(f" 样本数量: {results['river']['total_samples']}")
print(f" 匹配率: {results['river']['match_rate']:.1%}")
print(f" 平均差异: {results['river']['mean_difference']:.6f}")
if results['river']['success']:
passed_stages += 1
else:
print(f" 错误: {results['river']['error']}")
# Turn结果
print(f"\n TURN阶段:")
if 'error' not in results['turn']:
status = " 通过" if results['turn']['success'] else " 失败"
print(f" 验证结果: {status}")
print(f" 样本数量: {results['turn']['total_samples']}")
print(f" 低EMD率: {results['turn']['low_emd_rate']:.1%}")
print(f" 平均EMD: {results['turn']['mean_emd_distance']:.6f}")
if results['turn']['success']:
passed_stages += 1
else:
print(f" 错误: {results['turn']['error']}")
# Flop结果
print(f"\n FLOP阶段:")
if 'error' not in results['flop']:
status = " 通过" if results['flop']['success'] else " 失败"
print(f" 验证结果: {status}")
print(f" 样本数量: {results['flop']['total_samples']}")
print(f" 低EMD率: {results['flop']['low_emd_rate']:.1%}")
print(f" 平均EMD: {results['flop']['mean_emd_distance']:.6f}")
if results['flop']['success']:
passed_stages += 1
else:
print(f" 错误: {results['flop']['error']}")
# 总体结果
passed_stages = 0
total_stages = 3
if results.get('river') and results['river'].get('success', False):
passed_stages += 1
if results.get('turn') and results['turn'].get('success', False):
passed_stages += 1
if results.get('flop') and results['flop'].get('success', False):
passed_stages += 1
overall_rate = passed_stages / total_stages
# print(f"\n 总体验证通过率: {passed_stages}/{total_stages} ({overall_rate:.1%})")
# if overall_rate >= 0.7:
# print(" 数据验证成功!短牌型生成器与解析数据基本一致。")
# else:
# print(" 验证存在问题,生成器可能与实际数据不匹配,需要调试。")
return {
'results': results,
'passed_stages': passed_stages,
'total_stages': total_stages,
'overall_success': overall_rate >= 0.7
}

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""
调试
turn/flop阶段EMD在导出的数据与生成的数据间的差异
"""
import numpy as np
from cross_validation import DataValidator
validator = DataValidator()
print("解析Turn样本...")
turn_records = validator.parser.parse_turn_hist_with_cards(max_records=1)
if turn_records:
record = turn_records[0]
player_cards = record.player_cards
board_cards = record.board_cards
src_hist = record.bins
cur_hist = validator.generator.generate_turn_histogram(
player_cards, board_cards, num_bins=len(src_hist)
)
src_hist_norm = src_hist / src_hist.sum() if src_hist.sum() > 0 else src_hist
print(f"\n牌面: {[str(c) for c in player_cards]} + {[str(c) for c in board_cards]}")
print(f"bin数量: {len(src_hist)} vs {len(cur_hist)}")
print(f"原始直方图 - 和: {src_hist.sum():.3f}, 归一化后: {src_hist_norm.sum():.3f}")
print(f"生成直方图 - 和: {sum(cur_hist):.3f}")
print("\n前10个bin对比:")
print("Bin 原始值 归一化 生成值")
for i in range(min(10, len(src_hist))):
print(f"{i:3d} {src_hist[i]:8.3f} {src_hist_norm[i]:8.3f} {cur_hist[i]:8.3f}")
# 查看非零bin的分布
src_nonzero = np.nonzero(src_hist_norm)[0]
cur_nonzero = np.nonzero(cur_hist)[0]
print(f"\n非零bins位置:")
print(f"原始: {src_nonzero[:10]}...")
print(f"生成: {cur_nonzero[:10]}...")
# 计算分布的统计特征
src_mean = np.average(range(len(src_hist_norm)), weights=src_hist_norm)
cur_mean = np.average(range(len(cur_hist)), weights=cur_hist)
print(f"\n分布特征:")
print(f"原始分布重心: {src_mean:.2f}")
print(f"生成分布重心: {cur_mean:.2f}")
print(f"重心差异: {abs(src_mean - cur_mean):.2f}")

View File

@@ -0,0 +1,429 @@
import numpy as np
from typing import List, Dict, Tuple, Optional
from dataclasses import dataclass
from poker.card import Card, ShortDeckRank, Suit
@dataclass
class RiverEHSRecord:
"""river_EHS """
board_id: int
player_id: int
ehs: float
board_cards: List[Card] # 5张公共牌
player_cards: List[Card] # 2张手牌
@dataclass
class TurnHistRecord:
"""turn_hist"""
board_id: int
player_id: int
bins: np.ndarray
board_cards: List[Card] # 4张公共牌
player_cards: List[Card] # 2张手牌
@dataclass
class FlopHistRecord:
"""flop_hist"""
board_id: int
player_id: int
bins: np.ndarray
board_cards: List[Card] # 3张公共牌
player_cards: List[Card] # 2张手牌
class OpenPQLDecoder:
"""open-pql 完整解码"""
def __init__(self):
# Card64
self.OFFSET_SUIT = 16 # 每个suit占16位
self.OFFSET_S = 0 # S: [15:0]
self.OFFSET_H = 16 # H: [31:16]
self.OFFSET_D = 32 # D: [47:32]
self.OFFSET_C = 48 # C: [63:48]
# open-pql 到 短牌型(6-A36张牌)
self.opql_to_poker_rank = {
# 0: Rank.TWO,
# 1: Rank.THREE,
# 2: Rank.FOUR,
# 3: Rank.FIVE,
4: ShortDeckRank.SIX,
5: ShortDeckRank.SEVEN,
6: ShortDeckRank.EIGHT,
7: ShortDeckRank.NINE,
8: ShortDeckRank.TEN,
9: ShortDeckRank.JACK,
10: ShortDeckRank.QUEEN,
11: ShortDeckRank.KING,
12: ShortDeckRank.ACE,
}
self.opql_to_poker_suit = {
0: Suit.SPADES, # S -> SPADES
1: Suit.HEARTS, # H -> HEARTS
2: Suit.DIAMONDS, # D -> DIAMONDS
3: Suit.CLUBS, # C -> CLUBS
}
print("========== OpenPQLDecoder (短牌型36张牌) ===============")
def u64_from_ranksuit(self, rank, suit) -> int:
"""对应 Card64::u64_from_ranksuit_i8"""
return 1 << rank << (suit * self.OFFSET_SUIT)
def decode_card64(self, card64_u64) -> List[Card]:
"""从 Card64 的 u64 表示解码出所有牌"""
cards = []
# 按照 Card64 解析每个suit
# 每个suit16位
for opql_suit in range(4): # 0,1,2,3
suit_offset = opql_suit * self.OFFSET_SUIT
suit_bits = (card64_u64 >> suit_offset) & 0xFFFF
# 检查这个suit的每个rank
for opql_rank in range(13): # 0-12
if suit_bits & (1 << opql_rank):
poker_rank = self.opql_to_poker_rank[opql_rank]
poker_suit = self.opql_to_poker_suit[opql_suit]
cards.append(Card(poker_rank, poker_suit))
cards.sort(key=lambda c: (c.rank.numeric_value, c.suit.value))
return cards
def decode_hand_n_2(self, id_u16) -> List[Card]:
"""解码 Hand<2> 的 u16 ID"""
card0_u8 = id_u16 & 0xFF # 低8位
card1_u8 = (id_u16 >> 8) & 0xFF # 高8位
try:
card0 = self._card_from_u8(card0_u8)
card1 = self._card_from_u8(card1_u8)
cards = [card0, card1]
cards.sort(key=lambda c: (c.rank.numeric_value, c.suit.value))
return cards
except (ValueError, TypeError) as e:
raise ValueError(f"解码 Hand<2> 失败: id_u16={id_u16:04x}, card0_u8={card0_u8:02x}, card1_u8={card1_u8:02x}: {e}")
def _card_from_u8(self, v) -> Card:
"""
对应 open-pql Card::from_u8()
单张牌decode
"""
SHIFT_SUIT = 4
opql_rank = v & 0b1111 # 低4位
opql_suit = v >> SHIFT_SUIT # 高4位
if opql_rank not in self.opql_to_poker_rank:
raise ValueError(f"无效的rank: {opql_rank}")
if opql_suit not in self.opql_to_poker_suit:
raise ValueError(f"无效的suit: {opql_suit}")
poker_rank = self.opql_to_poker_rank[opql_rank]
poker_suit = self.opql_to_poker_suit[opql_suit]
return Card(poker_rank, poker_suit)
def decode_board_id(self, board_id, num_cards) -> List[Card]:
"""解码公共牌 ID"""
if num_cards == 2:
# 2张牌使用 Hand<2> 的编码方式
return self.decode_hand_n_2(board_id)
else:
# 3, 4, 5张公共牌使用 Card64 的编码方式
cards = self.decode_card64(board_id)
if len(cards) != num_cards:
raise ValueError(f"解码出 {len(cards)} 张牌,期望 {num_cards} 张,board_id={board_id:016x}")
return cards
def decode_player_id(self, player_id) -> List[Card]:
"""解码玩家手牌 ID (2张牌)"""
return self.decode_hand_n_2(player_id)
def decode_board_unique_card(self, all_cards) -> List[Card]:
"""验证并返回不重复的牌面"""
unique_cards = []
for card in all_cards:
is_duplicate = False
for existing_card in unique_cards:
if card.rank == existing_card.rank and card.suit == existing_card.suit:
is_duplicate = True
break
if not is_duplicate:
unique_cards.append(card)
return unique_cards
class XTaskDataParser:
def __init__(self, data_path: str = "ehs_data"):
self.data_path = data_path
self.decoder = OpenPQLDecoder()
def parse_river_ehs_with_cards(self, filename: str = "river_ehs.npy") -> List[RiverEHSRecord]:
filepath = f"{self.data_path}/{filename}"
try:
raw_data = np.load(filepath)
print(f"加载river_EHS: {raw_data.shape} 条记录,数据类型: {raw_data.dtype}")
records = []
decode_errors = 0
for i, row in enumerate(raw_data):
try:
board_id = int(row['board'])
player_id = int(row['player'])
ehs = float(row['ehs'])
# 解码公共牌 (5张)
board_cards = self.decoder.decode_board_id(board_id, 5)
# 解码玩家手牌 (2张)
player_cards = self.decoder.decode_player_id(player_id)
# 验证牌面不重复
all_cards = board_cards + player_cards
unique_cards = self.decoder.decode_board_unique_card(all_cards)
if len(unique_cards) != 7:
print(f"记录 {i}: 存在重复牌面")
print(f" Board: {[str(c) for c in board_cards]}")
print(f" Player: {[str(c) for c in player_cards]}")
# 创建完整记录
record = RiverEHSRecord(
board_id=board_id,
player_id=player_id,
ehs=ehs,
board_cards=board_cards,
player_cards=player_cards
)
records.append(record)
except Exception as e:
decode_errors += 1
if decode_errors <= 3:
print(f"记录 {i} 解码失败: {e}")
print(f" board_id={row['board']:016x}, player_id={row['player']:04x}")
continue
# 显示进度
if (i + 1) % 10000 == 0:
success_rate = len(records) / (i + 1) * 100
print(f" 已处理 {i+1:,}/{len(raw_data):,} 条记录,成功率 {success_rate:.1f}%")
total_records = len(raw_data)
success_records = len(records)
success_rate = success_records / total_records * 100
print(f"river_EHS解析完成:")
print(f" 总记录数: {total_records:,}")
print(f" 成功解码: {success_records:,} ({success_rate:.1f}%)")
print(f" 解码失败: {decode_errors:,}")
return records
except FileNotFoundError:
print(f"文件不存在: {filepath}")
raise
except Exception as e:
print(f"解析river_EHS数据失败: {e}")
raise
def parse_turn_hist_with_cards(self, filename: str = "turn_hist.npy", max_records: int = 1000) -> List[TurnHistRecord]:
filepath = f"{self.data_path}/{filename}"
try:
raw_data = np.load(filepath)
print(f"加载turn_hist: {raw_data.shape} 条记录,限制处理: {max_records:,} 条记录")
records = []
decode_errors = 0
# 限制处理数量以避免内存问题
# todo:抽样优化
data_to_process = raw_data[:max_records] if len(raw_data) > max_records else raw_data
for i, row in enumerate(data_to_process):
try:
board_id = int(row['board'])
player_id = int(row['player'])
bins = row['bins'].copy()
# 解码公共牌 (4张)
board_cards = self.decoder.decode_board_id(board_id, 4)
# 解码玩家手牌 (2张)
player_cards = self.decoder.decode_player_id(player_id)
# 验证牌面不重复
all_cards = board_cards + player_cards
unique_cards = self.decoder.decode_board_unique_card(all_cards)
if len(unique_cards) != 6:
decode_errors += 1
print(f"记录 {i}: 存在重复牌面")
record = TurnHistRecord(
board_id=board_id,
player_id=player_id,
bins=bins, # 存储30个bins的numpy数组
board_cards=board_cards,
player_cards=player_cards
)
records.append(record)
except Exception as e:
decode_errors += 1
if decode_errors <= 3:
print(f"记录 {i} 解码失败: {e}")
continue
if (i + 1) % 100 == 0:
print(f" 已处理 {i+1}/{len(data_to_process)} 条记录...")
print(f"turn_hist解析完成: 成功 {len(records)}/{len(data_to_process)} 条记录")
return records
except Exception as e:
print(f" 解析turn_hist数据失败: {e}")
raise
def parse_flop_hist_with_cards(self, filename: str = "flop_hist.npy", max_records: int = 500) -> List[FlopHistRecord]:
"""解析flop_hist"""
filepath = f"{self.data_path}/{filename}"
try:
raw_data = np.load(filepath)
print(f"加载flop_hist: {raw_data.shape} 条记录,限制处理: {max_records:,} 条记录")
records = []
decode_errors = 0
data_to_process = raw_data[:max_records] if len(raw_data) > max_records else raw_data
for i, row in enumerate(data_to_process):
try:
board_id = int(row['board'])
player_id = int(row['player'])
bins = row['bins'].copy()
# 解码公共牌 (3张)
board_cards = self.decoder.decode_board_id(board_id, 3)
# 解码玩家手牌 (2张)
player_cards = self.decoder.decode_player_id(player_id)
# 验证牌面不重复
all_cards = board_cards + player_cards
unique_cards = self.decoder.decode_board_unique_card(all_cards)
if len(unique_cards) != 5:
decode_errors += 1
print(f"记录 {i}: 存在重复牌面")
record = FlopHistRecord(
board_id=board_id,
player_id=player_id,
bins=bins, # 存储465个bins的numpy数组
board_cards=board_cards,
player_cards=player_cards
)
records.append(record)
except Exception as e:
decode_errors += 1
if decode_errors <= 3:
print(f"记录 {i} 解码失败: {e}")
continue
if (i + 1) % 50 == 0:
print(f" 已处理 {i+1}/{len(data_to_process)} 条记录...")
print(f"flop_hist解析完成: 成功 {len(records)}/{len(data_to_process)} 条记录")
return records
except Exception as e:
print(f"解析flop_hist数据失败: {e}")
raise
def analyze_parsed_data():
print("开始解析 xtask 数据并解码牌面...\n")
try:
parser = XTaskDataParser(data_path="ehs_data")
# 1. 解析river_EHS数据
print("=" * 60)
print(" 解析river_EHS")
print("=" * 60)
river_records = parser.parse_river_ehs_with_cards()
# 显示river_EHS样本
print(f"\nriver_EHS数据样本 (前10条):")
for i, record in enumerate(river_records[:10]):
board_str = " ".join(str(c) for c in record.board_cards)
player_str = " ".join(str(c) for c in record.player_cards)
print(f" {i+1:2d}. Board:[{board_str:14s}] Player:[{player_str:5s}] EHS:{record.ehs:.4f}")
# 2. 解析turn_hist数据
print(f"\n" + "=" * 60)
print(" 解析turn_hist数据")
print("=" * 60)
turn_records = parser.parse_turn_hist_with_cards(max_records=100) # 限制数量
# 显示turn_hist数据样本
print(f"\nturn_hist数据样本 (前5条):")
for i, record in enumerate(turn_records[:5]):
board_str = " ".join(str(c) for c in record.board_cards)
player_str = " ".join(str(c) for c in record.player_cards)
bins_stats = f"mean={record.bins.mean():.3f}, std={record.bins.std():.3f}"
print(f" {i+1}. Board:[{board_str:11s}] Player:[{player_str:5s}] Bins:{bins_stats}")
# 3. 解析flop_hist数据
print(f"\n" + "=" * 60)
print(" 解析flop_hist数据")
print("=" * 60)
flop_records = parser.parse_flop_hist_with_cards(max_records=50) # 限制数量
# 显示flop_数据样本
print(f"\n flop_hist数据样本 (前5条):")
for i, record in enumerate(flop_records[:5]):
board_str = " ".join(str(c) for c in record.board_cards)
player_str = " ".join(str(c) for c in record.player_cards)
bins_stats = f"mean={record.bins.mean():.3f}, std={record.bins.std():.3f}"
print(f" {i+1}. Board:[{board_str:8s}] Player:[{player_str:5s}] Bins:{bins_stats}")
# 4. 统计摘要
print(f"\n" + "=" * 60)
print("解析统计摘要")
print("=" * 60)
print(f"river_EHS记录: {len(river_records):,}")
print(f"turn_hist记录: {len(turn_records):,}")
print(f"flop_hist记录: {len(flop_records):,}")
# EHS 统计
if river_records:
ehs_values = [r.ehs for r in river_records]
print(f"river_EHS统计:")
print(f" 范围: [{min(ehs_values):.4f}, {max(ehs_values):.4f}]")
print(f" 均值: {np.mean(ehs_values):.4f}")
print(f" 标准差: {np.std(ehs_values):.4f}")
print(f"\n数据解析完成!所有 Board ID 和 Player ID 已成功解码为具体牌面。")
return {
'river_records': river_records,
'turn_records': turn_records,
'flop_records': flop_records
}
except Exception as e:
print(f" 数据解析失败: {e}")
return None

View File

@@ -2,8 +2,8 @@
Poker hand evaluation module Poker hand evaluation module
""" """
from .card import Card, Suit, Rank from .card import Card, Suit, Rank, ShortDeckRank
from .hand_ranking import HandRanking, HandType from .hand_ranking import HandRanking, HandType
from .hand_evaluator import HandEvaluator from .hand_evaluator import HandEvaluator
__all__ = ['Card', 'Suit', 'Rank', 'HandRanking', 'HandType', 'HandEvaluator'] __all__ = ['Card', 'Suit', 'Rank', 'HandRanking', 'HandType', 'HandEvaluator', 'ShortDeckRank']

View File

@@ -6,6 +6,7 @@ readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
"pytest>=8.4.2", "pytest>=8.4.2",
"scipy>=1.16.2",
] ]
[tool.pytest.ini_options] [tool.pytest.ini_options]

View File

@@ -1,10 +1,12 @@
from poker.card import ShortDeckRank from poker.card import ShortDeckRank
from .hand_evaluator import ShortDeckHandEvaluator from .hand_evaluator import ShortDeckHandEvaluator
from .hand_ranking import ShortDeckHandType, ShortDeckHandRanking from .hand_ranking import ShortDeckHandType, ShortDeckHandRanking
from .gen_hist import ShortDeckHistGenerator
__all__ = [ __all__ = [
'ShortDeckRank', 'ShortDeckRank',
'ShortDeckHandEvaluator', 'ShortDeckHandEvaluator',
'ShortDeckHandType', 'ShortDeckHandType',
'ShortDeckHandRanking' 'ShortDeckHandRanking',
'ShortDeckHistGenerator'
] ]

243
shortdeck/gen_hist.py Normal file
View File

@@ -0,0 +1,243 @@
#!/usr/bin/env python3
import numpy as np
from typing import List, Dict, Tuple
import itertools
from poker.card import Card, ShortDeckRank, Suit
from shortdeck.hand_evaluator import HandEvaluator
class ShortDeckHistGenerator:
def __init__(self):
# 36张牌6-A
self.full_deck = []
for rank in ShortDeckRank:
for suit in Suit:
self.full_deck.append(Card(rank, suit))
self.hand_evaluator = HandEvaluator()
print(f"初始化短牌型EHS直方图生成器牌组大小: {len(self.full_deck)}")
print(f"牌型范围: {ShortDeckRank.SIX.name}-{ShortDeckRank.ACE.name}")
def generate_river_ehs(self, player_cards, board_cards) -> float:
"""
River阶段胜率计算确定性结果
5张公共牌已知直接计算对所有可能对手的胜率
player_cards: 玩家的2张底牌
board_cards: 5张公共牌
Returns:
胜率值 (0-1之间)
"""
if len(player_cards) != 2:
raise ValueError("玩家必须有2张底牌")
if len(board_cards) != 5:
raise ValueError("River阶段必须有5张公共牌")
# 计算玩家的7张牌牌力
player_7_cards = player_cards + board_cards
player_strength = self.hand_evaluator.evaluate_hand(player_7_cards)
# 获取剩余的牌
used_cards = player_cards + board_cards
remaining_cards = [card for card in self.full_deck if card not in used_cards]
# 计算对所有可能对手的胜率
wins = 0
total_player2s = 0
# 遍历所有可能的对手底牌组合
for player2_cards in itertools.combinations(remaining_cards, 2):
player2_7_cards = list(player2_cards) + board_cards
player2_strength = self.hand_evaluator.evaluate_hand(player2_7_cards)
if player_strength > player2_strength:
wins += 2
elif player_strength == player2_strength:
wins += 1
total_player2s += 2
ehs = wins / total_player2s
return ehs
def generate_turn_histogram(self, player_cards, board_cards, num_bins: int = 30) -> np.ndarray:
"""
player_cards: 玩家的2张底牌
board_cards: 4张公共牌
num_bins: 30个bin短牌型应该是30
"""
if len(player_cards) != 2:
raise ValueError("玩家必须有2张底牌")
if len(board_cards) != 4:
raise ValueError("Turn阶段必须有4张公共牌")
# 获取剩余的牌可作为River牌
used_cards = player_cards + board_cards
remaining_cards = [card for card in self.full_deck if card not in used_cards]
ehs_values = []
histogram = []
# 对每张可能的River牌计算EHS
for river_card in remaining_cards:
full_board = board_cards + [river_card]
ehs = self.generate_river_ehs(player_cards, full_board)
# ehs_values.append(ehs)
histogram.append(ehs)
if (len(histogram) != num_bins):
print(f" Turn计算: generate nums{len(histogram)} ,num_bins={num_bins}")
raise
# 生成直方图
# histogram, _ = np.histogram(ehs_values, bins=num_bins, range=(0, 1))
# 归一化
# histogram = histogram.astype(float)
# if histogram.sum() > 0:
# histogram /= histogram.sum()
return histogram
def generate_flop_histogram(self, player_cards, board_cards, num_bins: int = 465) -> np.ndarray:
"""
Flop阶段EHS直方图
3张公共牌已知枚举所有可能的Turn+River牌组合计算EHS分布
player_cards: 玩家的2张牌
board_cards: 3张公共牌
num_bins: 直方图bin数量短牌型应该是C(36-5,2) = 465
"""
if len(player_cards) != 2:
raise ValueError("玩家必须有2张牌")
if len(board_cards) != 3:
raise ValueError("Flop阶段必须有3张公共牌")
# 获取剩余的牌
used_cards = player_cards + board_cards
remaining_cards = [card for card in self.full_deck if card not in used_cards]
ehs_values = []
histogram = []
# 枚举所有可能的Turn+River组合C(31,2) = 465
for turn_river_combo in itertools.combinations(remaining_cards, 2):
turn_card, river_card = turn_river_combo
full_board = board_cards + [turn_card, river_card]
ehs = self.generate_river_ehs(player_cards, full_board)
# ehs_values.append(ehs)
histogram.append(ehs)
# # 验证组合数
# expected_combinations = len(list(itertools.combinations(remaining_cards, 2)))
# print(f" Flop计算: C({len(remaining_cards)},2) = {expected_combinations} 种组合")
# 生成直方图
# histogram, _ = np.histogram(ehs_values, bins=num_bins, range=(0, 1))
# 归一化
# histogram = histogram.astype(float)
# if histogram.sum() > 0:
# histogram /= histogram.sum()
if (len(histogram) != num_bins):
print(f" Turn计算: generate nums{len(histogram)} ,num_bins={num_bins}")
raise
return histogram
# # 从一副牌中抽样验证 或者 从解析的数据中抽样验证
# def generate_sample_data(self, num_samples: int = 3) -> Dict:
# print(f"\n 生成样本数据 (每阶段 {num_samples} 个样本)")
# results = {
# 'river': [],
# 'turn': [],
# 'flop': []
# }
# for i in range(num_samples):
# # River样本7张牌
# river_cards = np.random.choice(self.full_deck, size=7, replace=False).tolist()
# player_cards = river_cards[:2]
# board_cards = river_cards[2:7]
# river_ehs = self.generate_river_ehs(player_cards, board_cards)
# results['river'].append({
# 'player_cards': player_cards,
# 'board_cards': board_cards,
# 'ehs': river_ehs
# })
# # Turn样本6张牌
# turn_cards = np.random.choice(self.full_deck, size=6, replace=False).tolist()
# player_cards = turn_cards[:2]
# board_cards = turn_cards[2:6]
# turn_hist = self.generate_turn_histogram(player_cards, board_cards, num_bins=30)
# results['turn'].append({
# 'player_cards': player_cards,
# 'board_cards': board_cards,
# 'histogram': turn_hist,
# 'mean': float(np.mean(turn_hist)),
# 'std': float(np.std(turn_hist))
# })
# # Flop样本5张牌
# flop_cards = np.random.choice(self.full_deck, size=5, replace=False).tolist()
# player_cards = flop_cards[:2]
# board_cards = flop_cards[2:5]
# flop_hist = self.generate_flop_histogram(player_cards, board_cards, num_bins=465)
# results['flop'].append({
# 'player_cards': [str(c) for c in player_cards],
# 'board_cards': [str(c) for c in board_cards],
# 'histogram_stats': flop_hist,
# 'mean': float(np.mean(flop_hist)),
# 'std': float(np.std(flop_hist))
# })
# return results
# def main():
# """测试短牌型EHS直方图生成器"""
# print("短牌型EHS直方图生成器测试")
# print("="*60)
# generator = ShortDeckHistGenerator()
# # 测试River EHS
# print(f"\n测试River EHS计算...")
# test_cards = np.random.choice(generator.full_deck, size=7, replace=False).tolist()
# player_cards = test_cards[:2]
# board_cards = test_cards[2:7]
# player_str = " ".join(str(c) for c in player_cards)
# board_str = " ".join(str(c) for c in board_cards)
# river_ehs = generator.generate_river_ehs(player_cards, board_cards)
# print(f" 玩家底牌: [{player_str}]")
# print(f" 公共牌: [{board_str}]")
# print(f" River EHS: {river_ehs:.4f}")
# # 测试Turn直方图
# print(f"\n测试Turn直方图生成...")
# turn_cards = np.random.choice(generator.full_deck, size=6, replace=False).tolist()
# player_cards = turn_cards[:2]
# board_cards = turn_cards[2:6]
# turn_hist = generator.generate_turn_histogram(player_cards, board_cards, num_bins=30)
# # 测试Flop直方图
# print(f"\n测试Flop直方图生成...")
# flop_cards = np.random.choice(generator.full_deck, size=5, replace=False).tolist()
# player_cards = flop_cards[:2]
# board_cards = flop_cards[2:5]
# flop_hist = generator.generate_flop_histogram(player_cards, board_cards, num_bins=465)
# print(f" Flop直方图: mean={np.mean(flop_hist):.3f}, 非零bins={np.count_nonzero(flop_hist)}")
# # 生成样本数据
# print(f"\n生成样本数据...")
# sample_data = generator.generate_sample_data(num_samples=3)

32
task5_main.py Normal file
View File

@@ -0,0 +1,32 @@
from cross_validation import DataValidator
import sys
def main():
"""主函数"""
import argparse
parser = argparse.ArgumentParser(
description='从xtask导出数据中抽取牌面进行EHS验证',
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument('--river-samples', type=int, default=10, help='River样本数 (默认: 10)')
parser.add_argument('--turn-samples', type=int, default=5, help='Turn样本数 (默认: 5)')
parser.add_argument('--flop-samples', type=int, default=3, help='Flop样本数 (默认: 3)')
parser.add_argument('--data-path', type=str, default='ehs_data', help='数据路径 (默认: ehs_data)')
args = parser.parse_args()
validator = DataValidator(data_path=args.data_path)
results = validator.run_full_validation(
river_samples=args.river_samples,
turn_samples=args.turn_samples,
flop_samples=args.flop_samples
)
return 0 if results['overall_success'] else 1
if __name__ == '__main__':
sys.exit(main())

177
task5_readme.md Normal file
View File

@@ -0,0 +1,177 @@
# Task5: 短牌型EHS交叉验证系统
## 概述
Task5实现了一个完整的短牌型德州扑克EHS交叉验证系统用于验证短牌型本地生成与xtask导出的EHS的一致性。
## 核心功能
### 验证目标
从task5_main.py解析xtask导出的数据 → 短牌型生成器重新计算EHS/HIST → EMD距离比较一致性
### 三阶段验证
1. **River阶段**: EHS单值精确匹配验证
2. **Turn阶段**: 30-bin直方图分布验证
3. **Flop阶段**: 465-bin直方图分布验证
## 文件结构
```
├── task5_main.py
├── task5_readme.md
├── cross_validation/
│ ├── __init__.py
│ ├── cross_validation.py #交叉验证
│ └── parse_data.py # 导出数据解
├── shortdeck/
│ └── gen_hist.py # 生成直方图
└── ehs_data/ # xtask导出数据
├── river_ehs.npy
├── turn_hist.npy
└── flop_hist.npy
```
## 关键特性
### 数据类型
用解析器将xtask导出的数据存储未以下结构
```python
# River EHS记录
RiverEHSRecord: board_id, player_id, ehs, board_cards, player_cards
# Turn直方图记录
TurnHistRecord: board_id, player_id, bins[30], board_cards, player_cards
# Flop直方图记录
FlopHistRecord: board_id, player_id, bins[465], board_cards, player_cards
```
### 验证流程
1. **数据解析**: 从.npy文件解析原始数据生成上述结构
2. **牌面解码**: 在解析的数据中取样使用样本的board_cards/player_cards组合成具体牌面
3. **重新计算**: 使用短牌型生成器根据turn/flop/riverplayer_cards+board_cards重算EHS/HIST
4. **一致性比较**: EMD距离/数值差异分析
5. **结果统计**: 通过率、平均误差、分布特征
## 使用方法
### 基本运行
```bash
python task5_main.py
```
### 参数配置
```bash
python task5_main.py --river-samples 10 --turn-samples 5 --flop-samples 3
```
### 参数说明
- `--river-samples`: River阶段验证样本数默认20
- `--turn-samples`: Turn阶段验证样本数默认10
- `--flop-samples`: Flop阶段验证样本数默认5
## 输出示例
```
========== OpenPQLDecoder (短牌型36张牌) ===============
初始化短牌型EHS直方图生成器牌组大小: 36
牌型范围: SIX-ACE
验证River EHS样本 (最大样本数: 5)
样本 1: [Qd Ad] + [6s Th Qc Kd Ac]
原始EHS: 0.692118
重算EHS: 0.692118
差异: 0.000000
验证Turn直方图样本 (最大样本数: 3)
样本 1: [8h 8s] + [6s Qd Kd Kh]
原始直方图: bins=30, sum=13.640, 非零bins=30
生成直方图: bins=30, sum=13.677, 非零bins=30
归一化后EMD距离: 0.021985
验证Flop直方图样本 (最大样本数: 2)
样本 1: [Qd Kd] + [7h Qh Qs]
原始直方图: bins=465, sum=422.320, 非零bins=465
生成直方图: bins=465, sum=427.315, 非零bins=465
归一化后EMD距离: 1.509076
```
## 验证标准
### River阶段
- **成功标准**: 匹配率 > 80% 且平均差异 < 0.05
- **评估方法**: 直接数值比较容差1e-6
### Turn/Flop阶段
- **成功标准**: 低EMD率 > 60% 且平均EMD < 0.5
- **EMD阈值**: < 0.2视为低距离
- **评估方法**: Wasserstein距离量化分布差异
## 数据处理
### 解码
```python
# Board ID → 公共牌组合
board_cards = decoder.decode_board_id(board_id, num_cards)
# Player ID → 手牌组合
player_cards = decoder.decode_player_id(player_id)
```
### 导出数据结构
```python
# river_ehs.npy
{
'board': int64, # 公共牌ID5张牌的编码
'player': int64, # 玩家手牌ID2张牌的编码
'ehs': float64 # EHS值0.0-1.0之间的浮点数)
}
# turn_hist.npy
{
'board': int64, # 公共牌ID4张牌的编码
'player': int64, # 玩家手牌ID2张牌的编码
'bins': ndarray # 30维直方图数组
}
# flop_hist.npy
{
'board': int64, # 公共牌ID3张牌的编码
'player': int64, # 玩家手牌ID2张牌的编码
'bins': ndarray # 465维直方图数组
}
```
### 编码方式
- **Card64编码用于3-5张公共牌**
64位每个suit占16位
每位表示对应rank的牌是否存在
用于board_id公共牌编码
- **Hand<2>编码用于2张牌**
16位低8位和高8位分别编码两张牌
每张牌用8位编码高4位=suit低4位=rank
用于player_id手牌编码
## 短牌型
- **牌组**: 6, 7, 8, 9, T, J, Q, K, A (四花色共36张)
- **组合数**: Flop阶段C(31,2)=465, Turn阶段30种可能
- **EHS计算**: 枚举所有对手组合,计算胜率期望
## 版本信息
- **Version**: 1.0
- **Python**: 3.13+
- **依赖**: numpy, scipy, dataclasses
## 遗留
1. **抽样优化**
2. **牌面同构优化**
3. **解析数据存储**
4. **反向验证对比**

109
uv.lock generated
View File

@@ -81,6 +81,58 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
] ]
[[package]]
name = "numpy"
version = "2.3.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029", size = 20576648, upload-time = "2025-09-09T16:54:12.543Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7d/b9/984c2b1ee61a8b803bf63582b4ac4242cf76e2dbd663efeafcb620cc0ccb/numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf", size = 20949588, upload-time = "2025-09-09T15:56:59.087Z" },
{ url = "https://files.pythonhosted.org/packages/a6/e4/07970e3bed0b1384d22af1e9912527ecbeb47d3b26e9b6a3bced068b3bea/numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7", size = 14177802, upload-time = "2025-09-09T15:57:01.73Z" },
{ url = "https://files.pythonhosted.org/packages/35/c7/477a83887f9de61f1203bad89cf208b7c19cc9fef0cebef65d5a1a0619f2/numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6", size = 5106537, upload-time = "2025-09-09T15:57:03.765Z" },
{ url = "https://files.pythonhosted.org/packages/52/47/93b953bd5866a6f6986344d045a207d3f1cfbad99db29f534ea9cee5108c/numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7", size = 6640743, upload-time = "2025-09-09T15:57:07.921Z" },
{ url = "https://files.pythonhosted.org/packages/23/83/377f84aaeb800b64c0ef4de58b08769e782edcefa4fea712910b6f0afd3c/numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c", size = 14278881, upload-time = "2025-09-09T15:57:11.349Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93", size = 16636301, upload-time = "2025-09-09T15:57:14.245Z" },
{ url = "https://files.pythonhosted.org/packages/a2/59/1287924242eb4fa3f9b3a2c30400f2e17eb2707020d1c5e3086fe7330717/numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae", size = 16053645, upload-time = "2025-09-09T15:57:16.534Z" },
{ url = "https://files.pythonhosted.org/packages/e6/93/b3d47ed882027c35e94ac2320c37e452a549f582a5e801f2d34b56973c97/numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86", size = 18578179, upload-time = "2025-09-09T15:57:18.883Z" },
{ url = "https://files.pythonhosted.org/packages/20/d9/487a2bccbf7cc9d4bfc5f0f197761a5ef27ba870f1e3bbb9afc4bbe3fcc2/numpy-2.3.3-cp313-cp313-win32.whl", hash = "sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8", size = 6312250, upload-time = "2025-09-09T15:57:21.296Z" },
{ url = "https://files.pythonhosted.org/packages/1b/b5/263ebbbbcede85028f30047eab3d58028d7ebe389d6493fc95ae66c636ab/numpy-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf", size = 12783269, upload-time = "2025-09-09T15:57:23.034Z" },
{ url = "https://files.pythonhosted.org/packages/fa/75/67b8ca554bbeaaeb3fac2e8bce46967a5a06544c9108ec0cf5cece559b6c/numpy-2.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5", size = 10195314, upload-time = "2025-09-09T15:57:25.045Z" },
{ url = "https://files.pythonhosted.org/packages/11/d0/0d1ddec56b162042ddfafeeb293bac672de9b0cfd688383590090963720a/numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc", size = 21048025, upload-time = "2025-09-09T15:57:27.257Z" },
{ url = "https://files.pythonhosted.org/packages/36/9e/1996ca6b6d00415b6acbdd3c42f7f03ea256e2c3f158f80bd7436a8a19f3/numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc", size = 14301053, upload-time = "2025-09-09T15:57:30.077Z" },
{ url = "https://files.pythonhosted.org/packages/05/24/43da09aa764c68694b76e84b3d3f0c44cb7c18cdc1ba80e48b0ac1d2cd39/numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b", size = 5229444, upload-time = "2025-09-09T15:57:32.733Z" },
{ url = "https://files.pythonhosted.org/packages/bc/14/50ffb0f22f7218ef8af28dd089f79f68289a7a05a208db9a2c5dcbe123c1/numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19", size = 6738039, upload-time = "2025-09-09T15:57:34.328Z" },
{ url = "https://files.pythonhosted.org/packages/55/52/af46ac0795e09657d45a7f4db961917314377edecf66db0e39fa7ab5c3d3/numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30", size = 14352314, upload-time = "2025-09-09T15:57:36.255Z" },
{ url = "https://files.pythonhosted.org/packages/a7/b1/dc226b4c90eb9f07a3fff95c2f0db3268e2e54e5cce97c4ac91518aee71b/numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e", size = 16701722, upload-time = "2025-09-09T15:57:38.622Z" },
{ url = "https://files.pythonhosted.org/packages/9d/9d/9d8d358f2eb5eced14dba99f110d83b5cd9a4460895230f3b396ad19a323/numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3", size = 16132755, upload-time = "2025-09-09T15:57:41.16Z" },
{ url = "https://files.pythonhosted.org/packages/b6/27/b3922660c45513f9377b3fb42240bec63f203c71416093476ec9aa0719dc/numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea", size = 18651560, upload-time = "2025-09-09T15:57:43.459Z" },
{ url = "https://files.pythonhosted.org/packages/5b/8e/3ab61a730bdbbc201bb245a71102aa609f0008b9ed15255500a99cd7f780/numpy-2.3.3-cp313-cp313t-win32.whl", hash = "sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd", size = 6442776, upload-time = "2025-09-09T15:57:45.793Z" },
{ url = "https://files.pythonhosted.org/packages/1c/3a/e22b766b11f6030dc2decdeff5c2fb1610768055603f9f3be88b6d192fb2/numpy-2.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d", size = 12927281, upload-time = "2025-09-09T15:57:47.492Z" },
{ url = "https://files.pythonhosted.org/packages/7b/42/c2e2bc48c5e9b2a83423f99733950fbefd86f165b468a3d85d52b30bf782/numpy-2.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1", size = 10265275, upload-time = "2025-09-09T15:57:49.647Z" },
{ url = "https://files.pythonhosted.org/packages/6b/01/342ad585ad82419b99bcf7cebe99e61da6bedb89e213c5fd71acc467faee/numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593", size = 20951527, upload-time = "2025-09-09T15:57:52.006Z" },
{ url = "https://files.pythonhosted.org/packages/ef/d8/204e0d73fc1b7a9ee80ab1fe1983dd33a4d64a4e30a05364b0208e9a241a/numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652", size = 14186159, upload-time = "2025-09-09T15:57:54.407Z" },
{ url = "https://files.pythonhosted.org/packages/22/af/f11c916d08f3a18fb8ba81ab72b5b74a6e42ead4c2846d270eb19845bf74/numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7", size = 5114624, upload-time = "2025-09-09T15:57:56.5Z" },
{ url = "https://files.pythonhosted.org/packages/fb/11/0ed919c8381ac9d2ffacd63fd1f0c34d27e99cab650f0eb6f110e6ae4858/numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a", size = 6642627, upload-time = "2025-09-09T15:57:58.206Z" },
{ url = "https://files.pythonhosted.org/packages/ee/83/deb5f77cb0f7ba6cb52b91ed388b47f8f3c2e9930d4665c600408d9b90b9/numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe", size = 14296926, upload-time = "2025-09-09T15:58:00.035Z" },
{ url = "https://files.pythonhosted.org/packages/77/cc/70e59dcb84f2b005d4f306310ff0a892518cc0c8000a33d0e6faf7ca8d80/numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421", size = 16638958, upload-time = "2025-09-09T15:58:02.738Z" },
{ url = "https://files.pythonhosted.org/packages/b6/5a/b2ab6c18b4257e099587d5b7f903317bd7115333ad8d4ec4874278eafa61/numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021", size = 16071920, upload-time = "2025-09-09T15:58:05.029Z" },
{ url = "https://files.pythonhosted.org/packages/b8/f1/8b3fdc44324a259298520dd82147ff648979bed085feeacc1250ef1656c0/numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf", size = 18577076, upload-time = "2025-09-09T15:58:07.745Z" },
{ url = "https://files.pythonhosted.org/packages/f0/a1/b87a284fb15a42e9274e7fcea0dad259d12ddbf07c1595b26883151ca3b4/numpy-2.3.3-cp314-cp314-win32.whl", hash = "sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0", size = 6366952, upload-time = "2025-09-09T15:58:10.096Z" },
{ url = "https://files.pythonhosted.org/packages/70/5f/1816f4d08f3b8f66576d8433a66f8fa35a5acfb3bbd0bf6c31183b003f3d/numpy-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8", size = 12919322, upload-time = "2025-09-09T15:58:12.138Z" },
{ url = "https://files.pythonhosted.org/packages/8c/de/072420342e46a8ea41c324a555fa90fcc11637583fb8df722936aed1736d/numpy-2.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe", size = 10478630, upload-time = "2025-09-09T15:58:14.64Z" },
{ url = "https://files.pythonhosted.org/packages/d5/df/ee2f1c0a9de7347f14da5dd3cd3c3b034d1b8607ccb6883d7dd5c035d631/numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00", size = 21047987, upload-time = "2025-09-09T15:58:16.889Z" },
{ url = "https://files.pythonhosted.org/packages/d6/92/9453bdc5a4e9e69cf4358463f25e8260e2ffc126d52e10038b9077815989/numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a", size = 14301076, upload-time = "2025-09-09T15:58:20.343Z" },
{ url = "https://files.pythonhosted.org/packages/13/77/1447b9eb500f028bb44253105bd67534af60499588a5149a94f18f2ca917/numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d", size = 5229491, upload-time = "2025-09-09T15:58:22.481Z" },
{ url = "https://files.pythonhosted.org/packages/3d/f9/d72221b6ca205f9736cb4b2ce3b002f6e45cd67cd6a6d1c8af11a2f0b649/numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a", size = 6737913, upload-time = "2025-09-09T15:58:24.569Z" },
{ url = "https://files.pythonhosted.org/packages/3c/5f/d12834711962ad9c46af72f79bb31e73e416ee49d17f4c797f72c96b6ca5/numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54", size = 14352811, upload-time = "2025-09-09T15:58:26.416Z" },
{ url = "https://files.pythonhosted.org/packages/a1/0d/fdbec6629d97fd1bebed56cd742884e4eead593611bbe1abc3eb40d304b2/numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e", size = 16702689, upload-time = "2025-09-09T15:58:28.831Z" },
{ url = "https://files.pythonhosted.org/packages/9b/09/0a35196dc5575adde1eb97ddfbc3e1687a814f905377621d18ca9bc2b7dd/numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097", size = 16133855, upload-time = "2025-09-09T15:58:31.349Z" },
{ url = "https://files.pythonhosted.org/packages/7a/ca/c9de3ea397d576f1b6753eaa906d4cdef1bf97589a6d9825a349b4729cc2/numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970", size = 18652520, upload-time = "2025-09-09T15:58:33.762Z" },
{ url = "https://files.pythonhosted.org/packages/fd/c2/e5ed830e08cd0196351db55db82f65bc0ab05da6ef2b72a836dcf1936d2f/numpy-2.3.3-cp314-cp314t-win32.whl", hash = "sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5", size = 6515371, upload-time = "2025-09-09T15:58:36.04Z" },
{ url = "https://files.pythonhosted.org/packages/47/c7/b0f6b5b67f6788a0725f744496badbb604d226bf233ba716683ebb47b570/numpy-2.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f", size = 13112576, upload-time = "2025-09-09T15:58:37.927Z" },
{ url = "https://files.pythonhosted.org/packages/06/b9/33bba5ff6fb679aa0b1f8a07e853f002a6b04b9394db3069a1270a7784ca/numpy-2.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b", size = 10545953, upload-time = "2025-09-09T15:58:40.576Z" },
]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "25.0" version = "25.0"
@@ -105,6 +157,7 @@ version = "0.1.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "pytest" }, { name = "pytest" },
{ name = "scipy" },
] ]
[package.dev-dependencies] [package.dev-dependencies]
@@ -114,7 +167,10 @@ dev = [
] ]
[package.metadata] [package.metadata]
requires-dist = [{ name = "pytest", specifier = ">=8.4.2" }] requires-dist = [
{ name = "pytest", specifier = ">=8.4.2" },
{ name = "scipy", specifier = ">=1.16.2" },
]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
@@ -160,3 +216,54 @@ sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
] ]
[[package]]
name = "scipy"
version = "1.16.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4c/3b/546a6f0bfe791bbb7f8d591613454d15097e53f906308ec6f7c1ce588e8e/scipy-1.16.2.tar.gz", hash = "sha256:af029b153d243a80afb6eabe40b0a07f8e35c9adc269c019f364ad747f826a6b", size = 30580599, upload-time = "2025-09-11T17:48:08.271Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/27/c5b52f1ee81727a9fc457f5ac1e9bf3d6eab311805ea615c83c27ba06400/scipy-1.16.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:84f7bf944b43e20b8a894f5fe593976926744f6c185bacfcbdfbb62736b5cc70", size = 36604856, upload-time = "2025-09-11T17:41:47.695Z" },
{ url = "https://files.pythonhosted.org/packages/32/a9/15c20d08e950b540184caa8ced675ba1128accb0e09c653780ba023a4110/scipy-1.16.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5c39026d12edc826a1ef2ad35ad1e6d7f087f934bb868fc43fa3049c8b8508f9", size = 28864626, upload-time = "2025-09-11T17:41:52.642Z" },
{ url = "https://files.pythonhosted.org/packages/4c/fc/ea36098df653cca26062a627c1a94b0de659e97127c8491e18713ca0e3b9/scipy-1.16.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e52729ffd45b68777c5319560014d6fd251294200625d9d70fd8626516fc49f5", size = 20855689, upload-time = "2025-09-11T17:41:57.886Z" },
{ url = "https://files.pythonhosted.org/packages/dc/6f/d0b53be55727f3e6d7c72687ec18ea6d0047cf95f1f77488b99a2bafaee1/scipy-1.16.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:024dd4a118cccec09ca3209b7e8e614931a6ffb804b2a601839499cb88bdf925", size = 23512151, upload-time = "2025-09-11T17:42:02.303Z" },
{ url = "https://files.pythonhosted.org/packages/11/85/bf7dab56e5c4b1d3d8eef92ca8ede788418ad38a7dc3ff50262f00808760/scipy-1.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7a5dc7ee9c33019973a470556081b0fd3c9f4c44019191039f9769183141a4d9", size = 33329824, upload-time = "2025-09-11T17:42:07.549Z" },
{ url = "https://files.pythonhosted.org/packages/da/6a/1a927b14ddc7714111ea51f4e568203b2bb6ed59bdd036d62127c1a360c8/scipy-1.16.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c2275ff105e508942f99d4e3bc56b6ef5e4b3c0af970386ca56b777608ce95b7", size = 35681881, upload-time = "2025-09-11T17:42:13.255Z" },
{ url = "https://files.pythonhosted.org/packages/c1/5f/331148ea5780b4fcc7007a4a6a6ee0a0c1507a796365cc642d4d226e1c3a/scipy-1.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af80196eaa84f033e48444d2e0786ec47d328ba00c71e4299b602235ffef9acb", size = 36006219, upload-time = "2025-09-11T17:42:18.765Z" },
{ url = "https://files.pythonhosted.org/packages/46/3a/e991aa9d2aec723b4a8dcfbfc8365edec5d5e5f9f133888067f1cbb7dfc1/scipy-1.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9fb1eb735fe3d6ed1f89918224e3385fbf6f9e23757cacc35f9c78d3b712dd6e", size = 38682147, upload-time = "2025-09-11T17:42:25.177Z" },
{ url = "https://files.pythonhosted.org/packages/a1/57/0f38e396ad19e41b4c5db66130167eef8ee620a49bc7d0512e3bb67e0cab/scipy-1.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:fda714cf45ba43c9d3bae8f2585c777f64e3f89a2e073b668b32ede412d8f52c", size = 38520766, upload-time = "2025-09-11T17:43:25.342Z" },
{ url = "https://files.pythonhosted.org/packages/1b/a5/85d3e867b6822d331e26c862a91375bb7746a0b458db5effa093d34cdb89/scipy-1.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:2f5350da923ccfd0b00e07c3e5cfb316c1c0d6c1d864c07a72d092e9f20db104", size = 25451169, upload-time = "2025-09-11T17:43:30.198Z" },
{ url = "https://files.pythonhosted.org/packages/09/d9/60679189bcebda55992d1a45498de6d080dcaf21ce0c8f24f888117e0c2d/scipy-1.16.2-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:53d8d2ee29b925344c13bda64ab51785f016b1b9617849dac10897f0701b20c1", size = 37012682, upload-time = "2025-09-11T17:42:30.677Z" },
{ url = "https://files.pythonhosted.org/packages/83/be/a99d13ee4d3b7887a96f8c71361b9659ba4ef34da0338f14891e102a127f/scipy-1.16.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:9e05e33657efb4c6a9d23bd8300101536abd99c85cca82da0bffff8d8764d08a", size = 29389926, upload-time = "2025-09-11T17:42:35.845Z" },
{ url = "https://files.pythonhosted.org/packages/bf/0a/130164a4881cec6ca8c00faf3b57926f28ed429cd6001a673f83c7c2a579/scipy-1.16.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:7fe65b36036357003b3ef9d37547abeefaa353b237e989c21027b8ed62b12d4f", size = 21381152, upload-time = "2025-09-11T17:42:40.07Z" },
{ url = "https://files.pythonhosted.org/packages/47/a6/503ffb0310ae77fba874e10cddfc4a1280bdcca1d13c3751b8c3c2996cf8/scipy-1.16.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6406d2ac6d40b861cccf57f49592f9779071655e9f75cd4f977fa0bdd09cb2e4", size = 23914410, upload-time = "2025-09-11T17:42:44.313Z" },
{ url = "https://files.pythonhosted.org/packages/fa/c7/1147774bcea50d00c02600aadaa919facbd8537997a62496270133536ed6/scipy-1.16.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff4dc42bd321991fbf611c23fc35912d690f731c9914bf3af8f417e64aca0f21", size = 33481880, upload-time = "2025-09-11T17:42:49.325Z" },
{ url = "https://files.pythonhosted.org/packages/6a/74/99d5415e4c3e46b2586f30cdbecb95e101c7192628a484a40dd0d163811a/scipy-1.16.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:654324826654d4d9133e10675325708fb954bc84dae6e9ad0a52e75c6b1a01d7", size = 35791425, upload-time = "2025-09-11T17:42:54.711Z" },
{ url = "https://files.pythonhosted.org/packages/1b/ee/a6559de7c1cc710e938c0355d9d4fbcd732dac4d0d131959d1f3b63eb29c/scipy-1.16.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63870a84cd15c44e65220eaed2dac0e8f8b26bbb991456a033c1d9abfe8a94f8", size = 36178622, upload-time = "2025-09-11T17:43:00.375Z" },
{ url = "https://files.pythonhosted.org/packages/4e/7b/f127a5795d5ba8ece4e0dce7d4a9fb7cb9e4f4757137757d7a69ab7d4f1a/scipy-1.16.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fa01f0f6a3050fa6a9771a95d5faccc8e2f5a92b4a2e5440a0fa7264a2398472", size = 38783985, upload-time = "2025-09-11T17:43:06.661Z" },
{ url = "https://files.pythonhosted.org/packages/3e/9f/bc81c1d1e033951eb5912cd3750cc005943afa3e65a725d2443a3b3c4347/scipy-1.16.2-cp313-cp313t-win_amd64.whl", hash = "sha256:116296e89fba96f76353a8579820c2512f6e55835d3fad7780fece04367de351", size = 38631367, upload-time = "2025-09-11T17:43:14.44Z" },
{ url = "https://files.pythonhosted.org/packages/d6/5e/2cc7555fd81d01814271412a1d59a289d25f8b63208a0a16c21069d55d3e/scipy-1.16.2-cp313-cp313t-win_arm64.whl", hash = "sha256:98e22834650be81d42982360382b43b17f7ba95e0e6993e2a4f5b9ad9283a94d", size = 25787992, upload-time = "2025-09-11T17:43:19.745Z" },
{ url = "https://files.pythonhosted.org/packages/8b/ac/ad8951250516db71619f0bd3b2eb2448db04b720a003dd98619b78b692c0/scipy-1.16.2-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:567e77755019bb7461513c87f02bb73fb65b11f049aaaa8ca17cfaa5a5c45d77", size = 36595109, upload-time = "2025-09-11T17:43:35.713Z" },
{ url = "https://files.pythonhosted.org/packages/ff/f6/5779049ed119c5b503b0f3dc6d6f3f68eefc3a9190d4ad4c276f854f051b/scipy-1.16.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:17d9bb346194e8967296621208fcdfd39b55498ef7d2f376884d5ac47cec1a70", size = 28859110, upload-time = "2025-09-11T17:43:40.814Z" },
{ url = "https://files.pythonhosted.org/packages/82/09/9986e410ae38bf0a0c737ff8189ac81a93b8e42349aac009891c054403d7/scipy-1.16.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:0a17541827a9b78b777d33b623a6dcfe2ef4a25806204d08ead0768f4e529a88", size = 20850110, upload-time = "2025-09-11T17:43:44.981Z" },
{ url = "https://files.pythonhosted.org/packages/0d/ad/485cdef2d9215e2a7df6d61b81d2ac073dfacf6ae24b9ae87274c4e936ae/scipy-1.16.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:d7d4c6ba016ffc0f9568d012f5f1eb77ddd99412aea121e6fa8b4c3b7cbad91f", size = 23497014, upload-time = "2025-09-11T17:43:49.074Z" },
{ url = "https://files.pythonhosted.org/packages/a7/74/f6a852e5d581122b8f0f831f1d1e32fb8987776ed3658e95c377d308ed86/scipy-1.16.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9702c4c023227785c779cba2e1d6f7635dbb5b2e0936cdd3a4ecb98d78fd41eb", size = 33401155, upload-time = "2025-09-11T17:43:54.661Z" },
{ url = "https://files.pythonhosted.org/packages/d9/f5/61d243bbc7c6e5e4e13dde9887e84a5cbe9e0f75fd09843044af1590844e/scipy-1.16.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1cdf0ac28948d225decdefcc45ad7dd91716c29ab56ef32f8e0d50657dffcc7", size = 35691174, upload-time = "2025-09-11T17:44:00.101Z" },
{ url = "https://files.pythonhosted.org/packages/03/99/59933956331f8cc57e406cdb7a483906c74706b156998f322913e789c7e1/scipy-1.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:70327d6aa572a17c2941cdfb20673f82e536e91850a2e4cb0c5b858b690e1548", size = 36070752, upload-time = "2025-09-11T17:44:05.619Z" },
{ url = "https://files.pythonhosted.org/packages/c6/7d/00f825cfb47ee19ef74ecf01244b43e95eae74e7e0ff796026ea7cd98456/scipy-1.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5221c0b2a4b58aa7c4ed0387d360fd90ee9086d383bb34d9f2789fafddc8a936", size = 38701010, upload-time = "2025-09-11T17:44:11.322Z" },
{ url = "https://files.pythonhosted.org/packages/e4/9f/b62587029980378304ba5a8563d376c96f40b1e133daacee76efdcae32de/scipy-1.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:f5a85d7b2b708025af08f060a496dd261055b617d776fc05a1a1cc69e09fe9ff", size = 39360061, upload-time = "2025-09-11T17:45:09.814Z" },
{ url = "https://files.pythonhosted.org/packages/82/04/7a2f1609921352c7fbee0815811b5050582f67f19983096c4769867ca45f/scipy-1.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:2cc73a33305b4b24556957d5857d6253ce1e2dcd67fa0ff46d87d1670b3e1e1d", size = 26126914, upload-time = "2025-09-11T17:45:14.73Z" },
{ url = "https://files.pythonhosted.org/packages/51/b9/60929ce350c16b221928725d2d1d7f86cf96b8bc07415547057d1196dc92/scipy-1.16.2-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:9ea2a3fed83065d77367775d689401a703d0f697420719ee10c0780bcab594d8", size = 37013193, upload-time = "2025-09-11T17:44:16.757Z" },
{ url = "https://files.pythonhosted.org/packages/2a/41/ed80e67782d4bc5fc85a966bc356c601afddd175856ba7c7bb6d9490607e/scipy-1.16.2-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7280d926f11ca945c3ef92ba960fa924e1465f8d07ce3a9923080363390624c4", size = 29390172, upload-time = "2025-09-11T17:44:21.783Z" },
{ url = "https://files.pythonhosted.org/packages/c4/a3/2f673ace4090452696ccded5f5f8efffb353b8f3628f823a110e0170b605/scipy-1.16.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:8afae1756f6a1fe04636407ef7dbece33d826a5d462b74f3d0eb82deabefd831", size = 21381326, upload-time = "2025-09-11T17:44:25.982Z" },
{ url = "https://files.pythonhosted.org/packages/42/bf/59df61c5d51395066c35836b78136accf506197617c8662e60ea209881e1/scipy-1.16.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:5c66511f29aa8d233388e7416a3f20d5cae7a2744d5cee2ecd38c081f4e861b3", size = 23915036, upload-time = "2025-09-11T17:44:30.527Z" },
{ url = "https://files.pythonhosted.org/packages/91/c3/edc7b300dc16847ad3672f1a6f3f7c5d13522b21b84b81c265f4f2760d4a/scipy-1.16.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efe6305aeaa0e96b0ccca5ff647a43737d9a092064a3894e46c414db84bc54ac", size = 33484341, upload-time = "2025-09-11T17:44:35.981Z" },
{ url = "https://files.pythonhosted.org/packages/26/c7/24d1524e72f06ff141e8d04b833c20db3021020563272ccb1b83860082a9/scipy-1.16.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f3a337d9ae06a1e8d655ee9d8ecb835ea5ddcdcbd8d23012afa055ab014f374", size = 35790840, upload-time = "2025-09-11T17:44:41.76Z" },
{ url = "https://files.pythonhosted.org/packages/aa/b7/5aaad984eeedd56858dc33d75efa59e8ce798d918e1033ef62d2708f2c3d/scipy-1.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bab3605795d269067d8ce78a910220262711b753de8913d3deeaedb5dded3bb6", size = 36174716, upload-time = "2025-09-11T17:44:47.316Z" },
{ url = "https://files.pythonhosted.org/packages/fd/c2/e276a237acb09824822b0ada11b028ed4067fdc367a946730979feacb870/scipy-1.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b0348d8ddb55be2a844c518cd8cc8deeeb8aeba707cf834db5758fc89b476a2c", size = 38790088, upload-time = "2025-09-11T17:44:53.011Z" },
{ url = "https://files.pythonhosted.org/packages/c6/b4/5c18a766e8353015439f3780f5fc473f36f9762edc1a2e45da3ff5a31b21/scipy-1.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:26284797e38b8a75e14ea6631d29bda11e76ceaa6ddb6fdebbfe4c4d90faf2f9", size = 39457455, upload-time = "2025-09-11T17:44:58.899Z" },
{ url = "https://files.pythonhosted.org/packages/97/30/2f9a5243008f76dfc5dee9a53dfb939d9b31e16ce4bd4f2e628bfc5d89d2/scipy-1.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d2a4472c231328d4de38d5f1f68fdd6d28a615138f842580a8a321b5845cf779", size = 26448374, upload-time = "2025-09-11T17:45:03.45Z" },
]