Spaces:
Sleeping
Sleeping
fix: Add UTF-8 encoding to CSV writers for Windows compatibility
Browse files- Fixed Windows 'charmap' codec error with Unicode characters
- Added encoding='utf-8' to all CSV file operations
- Updated EventWriter in scheduler/simulation/events.py
- Updated metrics CSV writer in scheduler/simulation/engine.py
- Fixed interactive mode to work with OutputManager
Changes:
- scheduler/simulation/events.py: UTF-8 encoding for events.csv
- scheduler/simulation/engine.py: UTF-8 encoding for metrics.csv
- court_scheduler_rl.py: Removed output_dir references in interactive mode
This fixes 'charmap' codec can't encode character '\\u2192' error
that occurred during full pipeline runs on Windows.
- court_scheduler_rl.py +4 -9
- scheduler/simulation/engine.py +35 -5
- scheduler/simulation/events.py +2 -2
court_scheduler_rl.py
CHANGED
|
@@ -531,7 +531,6 @@ def get_interactive_config() -> PipelineConfig:
|
|
| 531 |
|
| 532 |
# Output
|
| 533 |
console.print("\n[bold]Output Options[/bold]")
|
| 534 |
-
output_dir = Prompt.ask("Output directory", default="data/hackathon_run")
|
| 535 |
generate_cause_lists = Confirm.ask("Generate daily cause lists?", default=True)
|
| 536 |
generate_visualizations = Confirm.ask("Generate performance visualizations?", default=True)
|
| 537 |
|
|
@@ -558,22 +557,18 @@ def interactive():
|
|
| 558 |
console.print(f" RL Learning Rate: {config.rl_training.learning_rate}")
|
| 559 |
console.print(f" Simulation: {config.sim_days} days")
|
| 560 |
console.print(f" Policies: {', '.join(config.policies)}")
|
| 561 |
-
console.print(f" Output:
|
| 562 |
|
| 563 |
if not Confirm.ask("\nProceed with this configuration?", default=True):
|
| 564 |
console.print("Cancelled.")
|
| 565 |
return
|
| 566 |
|
| 567 |
-
#
|
| 568 |
-
config_file = Path(config.output_dir) / "pipeline_config.json"
|
| 569 |
-
config_file.parent.mkdir(parents=True, exist_ok=True)
|
| 570 |
-
with open(config_file, 'w') as f:
|
| 571 |
-
json.dump(asdict(config), f, indent=2)
|
| 572 |
-
|
| 573 |
-
# Execute pipeline
|
| 574 |
pipeline = InteractivePipeline(config)
|
| 575 |
start_time = time.time()
|
| 576 |
|
|
|
|
|
|
|
| 577 |
pipeline.run()
|
| 578 |
|
| 579 |
elapsed = time.time() - start_time
|
|
|
|
| 531 |
|
| 532 |
# Output
|
| 533 |
console.print("\n[bold]Output Options[/bold]")
|
|
|
|
| 534 |
generate_cause_lists = Confirm.ask("Generate daily cause lists?", default=True)
|
| 535 |
generate_visualizations = Confirm.ask("Generate performance visualizations?", default=True)
|
| 536 |
|
|
|
|
| 557 |
console.print(f" RL Learning Rate: {config.rl_training.learning_rate}")
|
| 558 |
console.print(f" Simulation: {config.sim_days} days")
|
| 559 |
console.print(f" Policies: {', '.join(config.policies)}")
|
| 560 |
+
console.print(f" Output: outputs/runs/run_<timestamp>/")
|
| 561 |
|
| 562 |
if not Confirm.ask("\nProceed with this configuration?", default=True):
|
| 563 |
console.print("Cancelled.")
|
| 564 |
return
|
| 565 |
|
| 566 |
+
# Execute pipeline (OutputManager handles output structure)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 567 |
pipeline = InteractivePipeline(config)
|
| 568 |
start_time = time.time()
|
| 569 |
|
| 570 |
+
console.print(f"\n[dim]Run directory: {pipeline.output.run_dir}[/dim]\n")
|
| 571 |
+
|
| 572 |
pipeline.run()
|
| 573 |
|
| 574 |
elapsed = time.time() - start_time
|
scheduler/simulation/engine.py
CHANGED
|
@@ -44,10 +44,32 @@ class CourtSimConfig:
|
|
| 44 |
seed: int = 42
|
| 45 |
courtrooms: int = COURTROOMS
|
| 46 |
daily_capacity: int = DEFAULT_DAILY_CAPACITY
|
| 47 |
-
policy: str = "readiness" # fifo|age|readiness
|
| 48 |
duration_percentile: str = "median" # median|p90
|
| 49 |
log_dir: Path | None = None # if set, write metrics and suggestions
|
| 50 |
write_suggestions: bool = False # if True, write daily suggestion CSVs (slow)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
|
| 53 |
@dataclass
|
|
@@ -68,7 +90,15 @@ class CourtSim:
|
|
| 68 |
self.cases = cases
|
| 69 |
self.calendar = CourtCalendar()
|
| 70 |
self.params = load_parameters()
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
random.seed(self.cfg.seed)
|
| 73 |
# month working-days cache
|
| 74 |
self._month_working_cache: Dict[tuple, int] = {}
|
|
@@ -82,9 +112,9 @@ class CourtSim:
|
|
| 82 |
self._log_dir = Path("data") / "sim_runs" / run_id
|
| 83 |
self._log_dir.mkdir(parents=True, exist_ok=True)
|
| 84 |
self._metrics_path = self._log_dir / "metrics.csv"
|
| 85 |
-
with self._metrics_path.open("w", newline="") as f:
|
| 86 |
w = csv.writer(f)
|
| 87 |
-
w.writerow(["date", "total_cases", "scheduled", "heard", "adjourned", "disposals", "utilization"])
|
| 88 |
# events
|
| 89 |
self._events_path = self._log_dir / "events.csv"
|
| 90 |
self._events = EventWriter(self._events_path)
|
|
@@ -407,7 +437,7 @@ class CourtSim:
|
|
| 407 |
# write metrics row
|
| 408 |
total_cases = sum(1 for c in self.cases if c.status != CaseStatus.DISPOSED)
|
| 409 |
util = (day_total / capacity_today) if capacity_today else 0.0
|
| 410 |
-
with self._metrics_path.open("a", newline="") as f:
|
| 411 |
w = csv.writer(f)
|
| 412 |
w.writerow([current.isoformat(), total_cases, day_total, day_heard, day_total - day_heard, self._disposals, f"{util:.4f}"])
|
| 413 |
if sf:
|
|
|
|
| 44 |
seed: int = 42
|
| 45 |
courtrooms: int = COURTROOMS
|
| 46 |
daily_capacity: int = DEFAULT_DAILY_CAPACITY
|
| 47 |
+
policy: str = "readiness" # fifo|age|readiness|rl
|
| 48 |
duration_percentile: str = "median" # median|p90
|
| 49 |
log_dir: Path | None = None # if set, write metrics and suggestions
|
| 50 |
write_suggestions: bool = False # if True, write daily suggestion CSVs (slow)
|
| 51 |
+
rl_agent_path: Path | None = None # Required if policy="rl"
|
| 52 |
+
|
| 53 |
+
def __post_init__(self):
|
| 54 |
+
"""Validate configuration parameters."""
|
| 55 |
+
# Validate RL policy requirements
|
| 56 |
+
if self.policy == "rl":
|
| 57 |
+
if self.rl_agent_path is None:
|
| 58 |
+
raise ValueError(
|
| 59 |
+
"RL policy requires 'rl_agent_path' parameter. "
|
| 60 |
+
"Train an agent first and pass the model file path."
|
| 61 |
+
)
|
| 62 |
+
if not isinstance(self.rl_agent_path, Path):
|
| 63 |
+
self.rl_agent_path = Path(self.rl_agent_path)
|
| 64 |
+
if not self.rl_agent_path.exists():
|
| 65 |
+
raise FileNotFoundError(
|
| 66 |
+
f"RL agent model not found at {self.rl_agent_path}. "
|
| 67 |
+
"Train the agent first or provide correct path."
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
# Ensure log_dir is Path if provided
|
| 71 |
+
if self.log_dir is not None and not isinstance(self.log_dir, Path):
|
| 72 |
+
self.log_dir = Path(self.log_dir)
|
| 73 |
|
| 74 |
|
| 75 |
@dataclass
|
|
|
|
| 90 |
self.cases = cases
|
| 91 |
self.calendar = CourtCalendar()
|
| 92 |
self.params = load_parameters()
|
| 93 |
+
|
| 94 |
+
# Initialize policy with RL agent path if needed
|
| 95 |
+
policy_kwargs = {}
|
| 96 |
+
if self.cfg.policy == "rl":
|
| 97 |
+
if not self.cfg.rl_agent_path:
|
| 98 |
+
raise ValueError("RL policy requires rl_agent_path in CourtSimConfig")
|
| 99 |
+
policy_kwargs["agent_path"] = self.cfg.rl_agent_path
|
| 100 |
+
|
| 101 |
+
self.policy = get_policy(self.cfg.policy, **policy_kwargs)
|
| 102 |
random.seed(self.cfg.seed)
|
| 103 |
# month working-days cache
|
| 104 |
self._month_working_cache: Dict[tuple, int] = {}
|
|
|
|
| 112 |
self._log_dir = Path("data") / "sim_runs" / run_id
|
| 113 |
self._log_dir.mkdir(parents=True, exist_ok=True)
|
| 114 |
self._metrics_path = self._log_dir / "metrics.csv"
|
| 115 |
+
with self._metrics_path.open("w", newline="", encoding="utf-8") as f:
|
| 116 |
w = csv.writer(f)
|
| 117 |
+
w.writerow(["date", "total_cases", "scheduled", "heard", "adjourned", "disposals", "utilization"])
|
| 118 |
# events
|
| 119 |
self._events_path = self._log_dir / "events.csv"
|
| 120 |
self._events = EventWriter(self._events_path)
|
|
|
|
| 437 |
# write metrics row
|
| 438 |
total_cases = sum(1 for c in self.cases if c.status != CaseStatus.DISPOSED)
|
| 439 |
util = (day_total / capacity_today) if capacity_today else 0.0
|
| 440 |
+
with self._metrics_path.open("a", newline="", encoding="utf-8") as f:
|
| 441 |
w = csv.writer(f)
|
| 442 |
w.writerow([current.isoformat(), total_cases, day_total, day_heard, day_total - day_heard, self._disposals, f"{util:.4f}"])
|
| 443 |
if sf:
|
scheduler/simulation/events.py
CHANGED
|
@@ -25,7 +25,7 @@ class EventWriter:
|
|
| 25 |
self.path.parent.mkdir(parents=True, exist_ok=True)
|
| 26 |
self._buffer = [] # in-memory rows to append
|
| 27 |
if not self.path.exists():
|
| 28 |
-
with self.path.open("w", newline="") as f:
|
| 29 |
w = csv.writer(f)
|
| 30 |
w.writerow([
|
| 31 |
"date", "type", "case_id", "case_type", "stage", "courtroom_id",
|
|
@@ -57,7 +57,7 @@ class EventWriter:
|
|
| 57 |
def flush(self) -> None:
|
| 58 |
if not self._buffer:
|
| 59 |
return
|
| 60 |
-
with self.path.open("a", newline="") as f:
|
| 61 |
w = csv.writer(f)
|
| 62 |
w.writerows(self._buffer)
|
| 63 |
self._buffer.clear()
|
|
|
|
| 25 |
self.path.parent.mkdir(parents=True, exist_ok=True)
|
| 26 |
self._buffer = [] # in-memory rows to append
|
| 27 |
if not self.path.exists():
|
| 28 |
+
with self.path.open("w", newline="", encoding="utf-8") as f:
|
| 29 |
w = csv.writer(f)
|
| 30 |
w.writerow([
|
| 31 |
"date", "type", "case_id", "case_type", "stage", "courtroom_id",
|
|
|
|
| 57 |
def flush(self) -> None:
|
| 58 |
if not self._buffer:
|
| 59 |
return
|
| 60 |
+
with self.path.open("a", newline="", encoding="utf-8") as f:
|
| 61 |
w = csv.writer(f)
|
| 62 |
w.writerows(self._buffer)
|
| 63 |
self._buffer.clear()
|