Files
DeepLottery/filter_model_3.py
dsyoon c611b400ae init
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-25 18:32:11 +09:00

95 lines
3.4 KiB
Python

"""
filter_model_3.py
OR-composed BallFilter:
- A candidate ball is ACCEPTED if it passes EITHER filter_model_1 OR filter_model_2.
- A candidate ball is REJECTED only if it fails BOTH.
This keeps the same public interface used across the project:
BallFilter(lottoHistoryFileName, ruleset_path=..., ruleset=...)
.filter(ball, no, until_end=False, df=None, filter_ball=None) -> set[str]
.extract_final_candidates(ball, no=None, until_end=False, df=None) -> set[str]
Notes:
- The underlying filters return a non-empty set of failure reasons when rejected.
- Callers treat "len(result) == 0" as PASS.
"""
from __future__ import annotations
from typing import Any, Dict, Optional
import filter_model_1 as fm1
import filter_model_2 as fm2
class BallFilter:
"""
OR composition of filter_model_1.BallFilter and filter_model_2.BallFilter.
- If model1 PASSES OR model2 PASSES -> return empty set()
- If both FAIL -> return union of reasons (prefixed for debugging)
"""
def __init__(
self,
lottoHistoryFileName: Optional[str] = None,
# Backward compatible single ruleset knobs (applied to both if specific ones not provided)
ruleset_path: Optional[str] = None,
ruleset: Optional[Dict[str, Any]] = None,
# Optional per-model overrides
ruleset_path_1: Optional[str] = None,
ruleset_path_2: Optional[str] = None,
ruleset_1: Optional[Dict[str, Any]] = None,
ruleset_2: Optional[Dict[str, Any]] = None,
):
rp1 = ruleset_path_1 if ruleset_path_1 is not None else ruleset_path
rp2 = ruleset_path_2 if ruleset_path_2 is not None else ruleset_path
r1 = ruleset_1 if ruleset_1 is not None else ruleset
r2 = ruleset_2 if ruleset_2 is not None else ruleset
self.m1 = fm1.BallFilter(lottoHistoryFileName, ruleset_path=rp1, ruleset=r1)
self.m2 = fm2.BallFilter(lottoHistoryFileName, ruleset_path=rp2, ruleset=r2)
#
# Delegate common helper methods (both models expose the same API)
#
def getBall(self, no):
return self.m1.getBall(no)
def getLastNo(self, YMD):
return self.m1.getLastNo(YMD)
def getNextNo(self, YMD):
return self.m1.getNextNo(YMD)
def getYMD(self, no):
return self.m1.getYMD(no)
def _prefixed(self, prefix: str, reasons: set) -> set:
# keep stable, readable debug strings
return {f"{prefix}{r}" for r in reasons}
def extract_final_candidates(self, ball, no=None, until_end: bool = False, df=None):
"""
OR-pass semantics:
- If either model returns empty set -> PASS (return empty set)
- Else -> FAIL (return union of reasons)
"""
r1 = self.m1.extract_final_candidates(ball=ball, no=no, until_end=until_end, df=df)
if len(r1) == 0:
return set()
r2 = self.m2.extract_final_candidates(ball=ball, no=no, until_end=until_end, df=df)
if len(r2) == 0:
return set()
# both failed
return self._prefixed("m1:", set(r1)) | self._prefixed("m2:", set(r2))
def filter(self, ball, no, until_end: bool = False, df=None, filter_ball=None):
"""
Keep signature compatible with existing callers.
- filter_ball is ignored here (callers typically pre-filter before calling .filter()).
"""
return self.extract_final_candidates(ball=ball, no=no, until_end=until_end, df=df)