Spaces:
Running
Running
File size: 12,461 Bytes
4d0ffdd f6c65ef 4d0ffdd 6a28f91 4d0ffdd 6a28f91 4d0ffdd 6a28f91 f6c65ef 4d0ffdd f6c65ef 4d0ffdd 6a28f91 4d0ffdd f6c65ef 4d0ffdd 6a28f91 f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd 6a28f91 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd 6a28f91 f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd 6a28f91 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd 6a28f91 4d0ffdd f6c65ef 6a28f91 f6c65ef 4d0ffdd f6c65ef 4d0ffdd 6a28f91 4d0ffdd f6c65ef 4d0ffdd 6a28f91 f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd 6a28f91 f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd 6a28f91 f6c65ef 4d0ffdd 6a28f91 4d0ffdd 6a28f91 4d0ffdd 6a28f91 4d0ffdd |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 |
"""Case entity and lifecycle management.
This module defines the Case class which represents a single court case
progressing through various stages.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import date, datetime
from enum import Enum
from typing import TYPE_CHECKING, List, Optional
from src.data.config import TERMINAL_STAGES
if TYPE_CHECKING:
from src.core.ripeness import RipenessStatus
else:
# Import at runtime
RipenessStatus = None
class CaseStatus(Enum):
"""Status of a case in the system."""
PENDING = "pending" # Filed, awaiting first hearing
ACTIVE = "active" # Has had at least one hearing
ADJOURNED = "adjourned" # Last hearing was adjourned
DISPOSED = "disposed" # Final disposal/settlement reached
@dataclass
class Case:
"""Represents a single court case.
Attributes:
case_id: Unique identifier (like CNR number)
case_type: Type of case (RSA, CRP, RFA, CA, CCC, CP, CMP)
filed_date: Date when case was filed
current_stage: Current stage in lifecycle
status: Current status (PENDING, ACTIVE, ADJOURNED, DISPOSED)
courtroom_id: Assigned courtroom (0-4 for 5 courtrooms)
is_urgent: Whether case is marked urgent
readiness_score: Computed readiness score (0-1)
hearing_count: Number of hearings held
last_hearing_date: Date of most recent hearing
days_since_last_hearing: Days elapsed since last hearing
age_days: Days since filing
disposal_date: Date of disposal (if disposed)
history: List of hearing dates and outcomes
"""
case_id: str
case_type: str
filed_date: date
current_stage: str = "ADMISSION" # Default initial stage
status: CaseStatus = CaseStatus.PENDING
courtroom_id: int | None = None # None = not yet assigned; 0 is invalid
is_urgent: bool = False
readiness_score: float = 0.0
hearing_count: int = 0
last_hearing_date: Optional[date] = None
days_since_last_hearing: int = 0
age_days: int = 0
disposal_date: Optional[date] = None
stage_start_date: Optional[date] = None
days_in_stage: int = 0
history: List[dict] = field(default_factory=list)
# Ripeness tracking (NEW - for bottleneck detection)
ripeness_status: str = "UNKNOWN" # RipenessStatus enum value (stored as string to avoid circular import)
bottleneck_reason: Optional[str] = None
ripeness_updated_at: Optional[datetime] = None
last_hearing_purpose: Optional[str] = (
None # Purpose of last hearing (for classification)
)
# No-case-left-behind tracking (NEW)
last_scheduled_date: Optional[date] = None
days_since_last_scheduled: int = 0
def progress_to_stage(self, new_stage: str, current_date: date) -> None:
"""Progress case to a new stage.
Args:
new_stage: The stage to progress to
current_date: Current simulation date
"""
self.current_stage = new_stage
self.stage_start_date = current_date
self.days_in_stage = 0
# Check if terminal stage (case disposed)
if new_stage in TERMINAL_STAGES:
self.status = CaseStatus.DISPOSED
self.disposal_date = current_date
# Record in history
self.history.append(
{
"date": current_date,
"event": "stage_change",
"stage": new_stage,
}
)
def record_hearing(
self, hearing_date: date, was_heard: bool, outcome: str = ""
) -> None:
"""Record a hearing event.
Args:
hearing_date: Date of the hearing
was_heard: Whether the hearing actually proceeded (not adjourned)
outcome: Outcome description
"""
self.hearing_count += 1
self.last_hearing_date = hearing_date
if was_heard:
self.status = CaseStatus.ACTIVE
else:
self.status = CaseStatus.ADJOURNED
# Record in history
self.history.append(
{
"date": hearing_date,
"event": "hearing",
"was_heard": was_heard,
"outcome": outcome,
"stage": self.current_stage,
}
)
def update_age(self, current_date: date) -> None:
"""Update age and days since last hearing.
Args:
current_date: Current simulation date
"""
self.age_days = (current_date - self.filed_date).days
if self.last_hearing_date:
self.days_since_last_hearing = (current_date - self.last_hearing_date).days
else:
self.days_since_last_hearing = self.age_days
if self.stage_start_date:
self.days_in_stage = (current_date - self.stage_start_date).days
else:
self.days_in_stage = self.age_days
# Update days since last scheduled (for no-case-left-behind tracking)
if self.last_scheduled_date:
self.days_since_last_scheduled = (
current_date - self.last_scheduled_date
).days
else:
self.days_since_last_scheduled = self.age_days
def compute_readiness_score(self) -> float:
"""Compute readiness score based on hearings, gaps, and stage.
Formula (from EDA):
READINESS = (hearings_capped/50) * 0.4 +
(100/gap_clamped) * 0.3 +
(stage_advanced) * 0.3
Returns:
Readiness score (0-1, higher = more ready)
"""
# Cap hearings at 50
hearings_capped = min(self.hearing_count, 50)
hearings_component = (hearings_capped / 50) * 0.4
# Gap component (inverse of days since last hearing)
gap_clamped = min(max(self.days_since_last_hearing, 1), 100)
gap_component = (100 / gap_clamped) * 0.3
# Stage component (advanced stages get higher score)
advanced_stages = ["ARGUMENTS", "EVIDENCE", "ORDERS / JUDGMENT"]
stage_component = 0.3 if self.current_stage in advanced_stages else 0.1
readiness = hearings_component + gap_component + stage_component
self.readiness_score = min(1.0, max(0.0, readiness))
return self.readiness_score
def is_ready_for_scheduling(self, min_gap_days: int = 7) -> bool:
"""Check if case is ready to be scheduled.
Args:
min_gap_days: Minimum days required since last hearing
Returns:
True if case can be scheduled
"""
if self.status == CaseStatus.DISPOSED:
return False
if self.last_hearing_date is None:
return True # First hearing, always ready
return self.days_since_last_hearing >= min_gap_days
def needs_alert(self, max_gap_days: int = 90) -> bool:
"""Check if case needs alert due to long gap.
Args:
max_gap_days: Maximum allowed gap before alert
Returns:
True if alert should be triggered
"""
if self.status == CaseStatus.DISPOSED:
return False
return self.days_since_last_hearing > max_gap_days
def get_priority_score(self) -> float:
"""Get overall priority score for scheduling.
Combines age, readiness, urgency, and adjournment boost into single score.
Formula:
priority = age*0.35 + readiness*0.25 + urgency*0.25 + adjournment_boost*0.15
Adjournment boost: Recently adjourned cases get priority to avoid indefinite postponement.
The boost decays exponentially: strongest immediately after adjournment, weaker over time.
Returns:
Priority score (higher = higher priority)
"""
# Age component (normalize to 0-1, assuming max age ~2000 days)
age_component = min(self.age_days / 2000, 1.0) * 0.35
# Readiness component
readiness_component = self.readiness_score * 0.25
# Urgency component
urgency_component = 1.0 if self.is_urgent else 0.0
urgency_component *= 0.25
# Adjournment boost (NEW - prevents cases from being repeatedly postponed)
adjournment_boost = 0.0
if self.status == CaseStatus.ADJOURNED and self.hearing_count > 0:
# Boost starts at 1.0 immediately after adjournment, decays exponentially
# Formula: boost = exp(-days_since_hearing / 21)
# At 7 days: ~0.71 (strong boost)
# At 14 days: ~0.50 (moderate boost)
# At 21 days: ~0.37 (weak boost)
# At 28 days: ~0.26 (very weak boost)
import math
decay_factor = 21 # Half-life of boost
adjournment_boost = math.exp(-self.days_since_last_hearing / decay_factor)
adjournment_boost *= 0.15
return (
age_component + readiness_component + urgency_component + adjournment_boost
)
def mark_unripe(self, status, reason: str, current_date: datetime) -> None:
"""Mark case as unripe with bottleneck reason.
Args:
status: Ripeness status (UNRIPE_SUMMONS, UNRIPE_PARTY, etc.) - RipenessStatus enum
reason: Human-readable reason for unripeness
current_date: Current simulation date
"""
# Store as string to avoid circular import
self.ripeness_status = status.value if hasattr(status, "value") else str(status)
self.bottleneck_reason = reason
self.ripeness_updated_at = current_date
# Record in history
self.history.append(
{
"date": current_date,
"event": "ripeness_change",
"status": self.ripeness_status,
"reason": reason,
}
)
def mark_ripe(self, current_date: datetime) -> None:
"""Mark case as ripe (ready for hearing).
Args:
current_date: Current simulation date
"""
self.ripeness_status = "RIPE"
self.bottleneck_reason = None
self.ripeness_updated_at = current_date
# Record in history
self.history.append(
{
"date": current_date,
"event": "ripeness_change",
"status": "RIPE",
"reason": "Case became ripe",
}
)
def mark_scheduled(self, scheduled_date: date) -> None:
"""Mark case as scheduled for a hearing.
Used for no-case-left-behind tracking.
Args:
scheduled_date: Date case was scheduled
"""
self.last_scheduled_date = scheduled_date
self.days_since_last_scheduled = 0
@property
def is_disposed(self) -> bool:
"""Check if case is disposed."""
return self.status == CaseStatus.DISPOSED
def __repr__(self) -> str:
return (
f"Case(id={self.case_id}, type={self.case_type}, "
f"stage={self.current_stage}, status={self.status.value}, "
f"hearings={self.hearing_count})"
)
def to_dict(self) -> dict:
"""Convert case to dictionary for serialization."""
return {
"case_id": self.case_id,
"case_type": self.case_type,
"filed_date": self.filed_date.isoformat(),
"current_stage": self.current_stage,
"status": self.status.value,
"courtroom_id": self.courtroom_id,
"is_urgent": self.is_urgent,
"readiness_score": self.readiness_score,
"hearing_count": self.hearing_count,
"last_hearing_date": self.last_hearing_date.isoformat()
if self.last_hearing_date
else None,
"days_since_last_hearing": self.days_since_last_hearing,
"age_days": self.age_days,
"disposal_date": self.disposal_date.isoformat()
if self.disposal_date
else None,
"ripeness_status": self.ripeness_status,
"bottleneck_reason": self.bottleneck_reason,
"last_hearing_purpose": self.last_hearing_purpose,
"last_scheduled_date": self.last_scheduled_date.isoformat()
if self.last_scheduled_date
else None,
"days_since_last_scheduled": self.days_since_last_scheduled,
"history": self.history,
}
|