diff --git a/cross_validation/__init__.py b/cross_validation/__init__.py new file mode 100644 index 0000000..69df6f0 --- /dev/null +++ b/cross_validation/__init__.py @@ -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'] \ No newline at end of file diff --git a/cross_validation/cross_validation.py b/cross_validation/cross_validation.py new file mode 100644 index 0000000..15d8bf7 --- /dev/null +++ b/cross_validation/cross_validation.py @@ -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 + } diff --git a/cross_validation/debug_emd.py b/cross_validation/debug_emd.py new file mode 100644 index 0000000..06612a6 --- /dev/null +++ b/cross_validation/debug_emd.py @@ -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}") \ No newline at end of file diff --git a/cross_validation/parse_data.py b/cross_validation/parse_data.py new file mode 100644 index 0000000..0d15107 --- /dev/null +++ b/cross_validation/parse_data.py @@ -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-A,36张牌) + 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 diff --git a/poker/__init__.py b/poker/__init__.py index 35e10e2..56aacdd 100644 --- a/poker/__init__.py +++ b/poker/__init__.py @@ -2,8 +2,8 @@ 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_evaluator import HandEvaluator -__all__ = ['Card', 'Suit', 'Rank', 'HandRanking', 'HandType', 'HandEvaluator'] \ No newline at end of file +__all__ = ['Card', 'Suit', 'Rank', 'HandRanking', 'HandType', 'HandEvaluator', 'ShortDeckRank'] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6cb15bf..d4ff993 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.13" dependencies = [ "pytest>=8.4.2", + "scipy>=1.16.2", ] [tool.pytest.ini_options] diff --git a/shortdeck/__init__.py b/shortdeck/__init__.py index 0715453..e28aa3a 100644 --- a/shortdeck/__init__.py +++ b/shortdeck/__init__.py @@ -1,10 +1,12 @@ from poker.card import ShortDeckRank from .hand_evaluator import ShortDeckHandEvaluator from .hand_ranking import ShortDeckHandType, ShortDeckHandRanking +from .gen_hist import ShortDeckHistGenerator __all__ = [ 'ShortDeckRank', 'ShortDeckHandEvaluator', 'ShortDeckHandType', - 'ShortDeckHandRanking' + 'ShortDeckHandRanking', + 'ShortDeckHistGenerator' ] \ No newline at end of file diff --git a/shortdeck/gen_hist.py b/shortdeck/gen_hist.py new file mode 100644 index 0000000..e8a2574 --- /dev/null +++ b/shortdeck/gen_hist.py @@ -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) diff --git a/shortdeck_main.py b/task4_main.py similarity index 100% rename from shortdeck_main.py rename to task4_main.py diff --git a/task5_main.py b/task5_main.py new file mode 100644 index 0000000..99b9f6a --- /dev/null +++ b/task5_main.py @@ -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()) \ No newline at end of file diff --git a/task5_readme.md b/task5_readme.md new file mode 100644 index 0000000..da96dfc --- /dev/null +++ b/task5_readme.md @@ -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/river(player_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, # 公共牌ID(5张牌的编码) + 'player': int64, # 玩家手牌ID(2张牌的编码) + 'ehs': float64 # EHS值(0.0-1.0之间的浮点数) +} + +# turn_hist.npy +{ + 'board': int64, # 公共牌ID(4张牌的编码) + 'player': int64, # 玩家手牌ID(2张牌的编码) + 'bins': ndarray # 30维直方图数组 +} + +# flop_hist.npy +{ + 'board': int64, # 公共牌ID(3张牌的编码) + 'player': int64, # 玩家手牌ID(2张牌的编码) + '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. **反向验证对比** + diff --git a/uv.lock b/uv.lock index a3a03f1..b1f38c3 100644 --- a/uv.lock +++ b/uv.lock @@ -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" }, ] +[[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]] name = "packaging" version = "25.0" @@ -105,6 +157,7 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "pytest" }, + { name = "scipy" }, ] [package.dev-dependencies] @@ -114,7 +167,10 @@ dev = [ ] [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] dev = [ @@ -160,3 +216,54 @@ sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b 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" }, ] + +[[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" }, +]