From d5d2e7f8db90710ad98b880910913eb82c7ad1d3 Mon Sep 17 00:00:00 2001 From: Matthew Vu Date: Mon, 10 Nov 2025 18:57:47 -0500 Subject: [PATCH] fix: just fixed --- main/__pycache__/dashboard.cpython-310.pyc | Bin 0 -> 13016 bytes .../power_grid_env.cpython-310.pyc | Bin 0 -> 13911 bytes .../power_grid_env.cpython-311.pyc | Bin 0 -> 28577 bytes main/power_grid_env.py | 580 ++++++++++++++++++ readme.md | 129 ---- 5 files changed, 580 insertions(+), 129 deletions(-) create mode 100644 main/__pycache__/dashboard.cpython-310.pyc create mode 100644 main/__pycache__/power_grid_env.cpython-310.pyc create mode 100644 main/__pycache__/power_grid_env.cpython-311.pyc create mode 100644 main/power_grid_env.py diff --git a/main/__pycache__/dashboard.cpython-310.pyc b/main/__pycache__/dashboard.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..20f54f02c56b61c2df94152da14ae95e89d71c6d GIT binary patch literal 13016 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 F`M&|+Y-Iod literal 0 HcmV?d00001 diff --git a/main/__pycache__/power_grid_env.cpython-311.pyc b/main/__pycache__/power_grid_env.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ba31ffd04d34162cb265a943c7bd6d5adf2134fa GIT binary patch literal 28577 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 H>(KuXyW>ci literal 0 HcmV?d00001 diff --git a/main/power_grid_env.py b/main/power_grid_env.py new file mode 100644 index 0000000..b3f7293 --- /dev/null +++ b/main/power_grid_env.py @@ -0,0 +1,580 @@ +""" +Multi-Agent Power Grid Environment for Reinforcement Learning + +This environment simulates a 68-bus power grid with 20 agents: +- 5 batteries (ramp rate: 50 MW/min) +- 8 gas plants (ramp rate: 10 MW/min) +- 7 demand response units (ramp rate: 5 MW/min) + +State space: 140-dimensional (bus frequencies, generator outputs, loads) +Each agent observes: 15-dimensional local observation +Actions: 20 continuous power changes ΔP^i within ramp rate limits + +Key Features (Aligned with Proposal): +- Correct swing equation: df/dt = P_imbalance / (2*H*S_base) +- Total system load: 2000-5000 MW (distributed across 68 buses) +- Communication delay: 2 seconds (SCADA delay) +- Graduated response: exponential penalties + lenient termination (±1.5 Hz) +- No artificial frequency clamping - realistic physics +""" + +import gymnasium as gym +from gymnasium import spaces +import torch +import numpy as np +from typing import Dict, List, Tuple, Any, Optional +import math +import random + + +class PowerGridEnv(gym.Env): + """Multi-agent power grid environment using gymnasium interface.""" + + def __init__(self, + n_buses: int = 68, + n_agents: int = 20, + dt: float = 2.0/60.0, # time step in minutes (2 seconds) + frequency_bounds: Tuple[float, float] = (59.5, 60.5), + load_range: Tuple[float, float] = (2000.0, 5000.0), + contingency_prob: float = 0.001, + device: str = 'cpu'): + """ + Initialize the power grid environment. + + Args: + n_buses: Number of buses in the power grid (68) + n_agents: Number of agents (20: 5 batteries + 8 gas + 7 DR) + dt: Time step in minutes + frequency_bounds: Frequency bounds in Hz [59.5, 60.5] + load_range: Load range in MW [2000, 5000] + contingency_prob: Probability of N-1 contingency per step + device: PyTorch device ('cpu' or 'cuda') + """ + super().__init__() + + self.device = torch.device(device) + self.n_buses = n_buses + self.n_agents = n_agents + self.dt = dt + self.frequency_bounds = frequency_bounds + self.load_range = load_range + self.contingency_prob = contingency_prob + + # Agent configuration: [batteries, gas plants, demand response] + self.agent_types = ['battery'] * 5 + ['gas'] * 8 + ['dr'] * 7 + self.ramp_rates = torch.tensor([50.0] * 5 + [10.0] * 8 + [5.0] * 7, device=self.device) # MW/min + + # Agent capacity constraints [P_min, P_max] in MW + self.power_min = torch.tensor([0.0] * 5 + [50.0] * 8 + [-200.0] * 7, device=self.device) + self.power_max = torch.tensor([100.0] * 5 + [500.0] * 8 + [0.0] * 7, device=self.device) + + # Agent-specific cost coefficients ($/MW) + self.cost_coefficients = torch.tensor([5.0] * 5 + [50.0] * 8 + [20.0] * 7, device=self.device) + + # Wear-and-tear coefficients for different agent types + self.wear_coefficients = torch.tensor([0.1] * 5 + [0.05] * 8 + [0.2] * 7, device=self.device) + + # Power system parameters + self.nominal_frequency = 60.0 # Hz + self.base_power = 100.0 # MVA base + self.inertia_constants = torch.rand(n_buses, device=self.device) * 5.0 + 2.0 # H in seconds + + # Grid topology - simplified admittance matrix (68x68) + self._initialize_grid_topology() + + # State space: [frequencies (68), generator outputs (20), renewables (14), loads (30), time features (8)] = 140 + self.state_dim = 140 + self.obs_dim = 15 # Local observation dimension per agent as specified in proposal + + # Action space: continuous power changes for each agent + self.action_space = spaces.Box( + low=-1.0, high=1.0, shape=(self.n_agents,), dtype=np.float32 + ) + + # Observation space: each agent gets local 15-dim observation + self.observation_space = spaces.Box( + low=-np.inf, high=np.inf, shape=(self.n_agents, self.obs_dim), dtype=np.float32 + ) + + # Communication delay buffer (2-second SCADA delay) + self.delay_steps = 1 # 1 time step × 2 seconds = 2 second delay + self.observation_buffer = [] + + # Renewable forecast parameters + self.forecast_horizon = 5 # 5 time steps ahead + self.renewable_forecasts = torch.zeros(14, self.forecast_horizon, device=self.device) + + # Initialize state variables + self.reset() + + def _initialize_grid_topology(self): + """Initialize the power grid topology and admittance matrix.""" + # Simplified 68-bus system admittance matrix + # In practice, this would be based on actual grid topology + self.admittance_matrix = torch.zeros(self.n_buses, self.n_buses, device=self.device) + + # Create a connected graph structure + for i in range(self.n_buses): + # Self admittance (diagonal elements) + self.admittance_matrix[i, i] = (torch.rand(1, device=self.device) * 10.0 + 5.0).item() + + # Connect to nearby buses (simplified ring + radial structure) + if i < self.n_buses - 1: + admittance_val = (torch.rand(1, device=self.device) * 2.0 + 1.0).item() + self.admittance_matrix[i, i+1] = -admittance_val + self.admittance_matrix[i+1, i] = -admittance_val + self.admittance_matrix[i, i] += admittance_val + self.admittance_matrix[i+1, i+1] += admittance_val + + # Add some additional connections for robustness + for _ in range(self.n_buses // 4): + i, j = torch.randint(0, self.n_buses, (2,)).tolist() + if i != j: + admittance_val = (torch.rand(1, device=self.device) * 1.0 + 0.5).item() + self.admittance_matrix[i, j] = -admittance_val + self.admittance_matrix[j, i] = -admittance_val + self.admittance_matrix[i, i] += admittance_val + self.admittance_matrix[j, j] += admittance_val + + # Agent-to-bus mapping (which buses have controllable agents) + self.agent_bus_mapping = torch.randint(0, self.n_buses, (self.n_agents,), device=self.device) + + def reset(self, seed: Optional[int] = None, options: Optional[Dict] = None) -> Tuple[np.ndarray, Dict]: + """ + Reset the environment to initial state. + + Returns: + observations: Array of shape (n_agents, obs_dim) + info: Dictionary with additional information + """ + if seed is not None: + torch.manual_seed(seed) + np.random.seed(seed) + random.seed(seed) + + # Initialize bus frequencies around nominal (60 Hz) + self.frequencies = torch.ones(self.n_buses, device=self.device) * self.nominal_frequency + self.frequencies += torch.randn(self.n_buses, device=self.device) * 0.01 # Small initial deviation + + # Initialize generator outputs (MW) within capacity bounds + self.generator_outputs = torch.zeros(self.n_agents, device=self.device) + # Set initial outputs to minimum capacity for gas plants and mid-range for batteries + self.generator_outputs[:5] = 50.0 # Batteries at 50% capacity + self.generator_outputs[5:13] = 100.0 # Gas plants at minimum + self.generator_outputs[13:] = -50.0 # DR at 25% load reduction + + # Initialize loads (MW) - distribute total system load across buses + # Total system load should be 2000-5000 MW as per proposal + load_min, load_max = self.load_range + total_system_load = torch.rand(1, device=self.device).item() * (load_max - load_min) + load_min + load_distribution = torch.rand(self.n_buses, device=self.device) + self.loads = (load_distribution / load_distribution.sum()) * total_system_load + + # Initialize renewable generation (14 buses with renewables) + self.renewable_buses = torch.randint(0, self.n_buses, (14,), device=self.device) + self.renewable_generation = torch.rand(14, device=self.device) * 500.0 # 0-500 MW + + # Initialize voltage angles (radians) + self.voltage_angles = torch.zeros(self.n_buses, device=self.device) + + # Reset contingency state + self.contingency_active = False + self.contingency_bus = None + + # Time step counter and time features + self.time_step = 0 + self.current_hour = 12.0 # Start at noon + self.current_day = 1.0 # Monday + + # Initialize observation buffer for communication delay + initial_obs = self._get_observations_immediate() + self.observation_buffer = [initial_obs.clone() for _ in range(self.delay_steps + 1)] + + # Initialize renewable forecasts + self._update_renewable_forecasts() + + # Get initial observations (with delay) + observations = self._get_observations() + info = self._get_info() + + return observations.cpu().numpy(), info + + def step(self, actions: np.ndarray) -> Tuple[np.ndarray, float, bool, bool, Dict]: + """ + Execute one time step of the environment. + + Args: + actions: Array of shape (n_agents,) with continuous actions in [-1, 1] + + Returns: + observations: Next state observations + reward: Shared reward for all agents + terminated: Whether episode is terminated + truncated: Whether episode is truncated + info: Additional information + """ + actions = torch.tensor(actions, device=self.device, dtype=torch.float32) + + # Convert normalized actions to actual power changes (MW) + power_changes = actions * self.ramp_rates * self.dt # Scale by ramp rate and time step + + # Apply action constraints - check capacity limits before updating + feasible_changes = torch.zeros_like(power_changes) + for i in range(self.n_agents): + current_power = self.generator_outputs[i] + desired_change = power_changes[i] + + # Calculate feasible change respecting capacity bounds + max_increase = self.power_max[i] - current_power + max_decrease = self.power_min[i] - current_power + + feasible_changes[i] = torch.clamp(desired_change, max_decrease, max_increase) + + # Apply feasible power changes to generator outputs + self.generator_outputs += feasible_changes + + # Ensure outputs stay within bounds (safety check) + self.generator_outputs = torch.clamp(self.generator_outputs, self.power_min, self.power_max) + + # Store action for wear calculation + self.last_actions = feasible_changes + + # Update time features + self._update_time_features() + + # Update stochastic loads and renewable generation + self._update_stochastic_components() + + # Update renewable forecasts + self._update_renewable_forecasts() + + # Check for N-1 contingencies + self._check_contingencies() + + # Solve power flow and update frequencies using swing equation + self._update_system_dynamics() + + # Calculate reward + reward = self._calculate_reward() + + # Check termination conditions + terminated, truncated = self._check_termination() + + # Update observation buffer and get delayed observations + current_obs = self._get_observations_immediate() + self.observation_buffer.append(current_obs) + if len(self.observation_buffer) > self.delay_steps + 1: + self.observation_buffer.pop(0) + + # Get delayed observations + observations = self._get_observations() + + # Update time step + self.time_step += 1 + + info = self._get_info() + + return observations.cpu().numpy(), reward, terminated, truncated, info + + def _update_stochastic_components(self): + """Update stochastic load and renewable generation.""" + # Add random variations to loads while maintaining total system load in [2000, 5000] MW + # Add ±5% variation to individual bus loads + load_variation = torch.randn(self.n_buses, device=self.device) * 0.05 + self.loads *= (1.0 + load_variation) + + # Rescale to maintain total system load within bounds + current_total = torch.sum(self.loads) + load_min, load_max = self.load_range + if current_total < load_min: + self.loads *= (load_min / current_total) + elif current_total > load_max: + self.loads *= (load_max / current_total) + + # Ensure no negative loads + self.loads = torch.clamp(self.loads, min=0.0) + + # Update renewable generation with stochastic variations + renewable_variation = torch.randn(14, device=self.device) * 0.1 + self.renewable_generation *= (1.0 + renewable_variation) + self.renewable_generation = torch.clamp(self.renewable_generation, 0.0, 1000.0) + + def _check_contingencies(self): + """Check for N-1 contingency events.""" + if torch.rand(1).item() < self.contingency_prob: + if not self.contingency_active: + # Activate contingency - disconnect a random bus + self.contingency_active = True + self.contingency_bus = torch.randint(0, self.n_buses, (1,)).item() + # Reduce load at contingency bus + self.loads[self.contingency_bus] *= 0.1 + else: + # Recover from contingency + if self.contingency_active: + self.contingency_active = False + if self.contingency_bus is not None: + # Restore load + load_min, load_max = self.load_range + self.loads[self.contingency_bus] = torch.rand(1, device=self.device) * (load_max - load_min) + load_min + self.contingency_bus = None + + def _update_system_dynamics(self): + """Update system dynamics using swing equation.""" + # Calculate power imbalance at each bus + power_injection = torch.zeros(self.n_buses, device=self.device) + + # Add generator outputs to their respective buses + for i, bus_idx in enumerate(self.agent_bus_mapping): + power_injection[bus_idx] += self.generator_outputs[i] + + # Add renewable generation + for i, bus_idx in enumerate(self.renewable_buses): + power_injection[bus_idx] += self.renewable_generation[i] + + # Subtract loads + power_injection -= self.loads + + # Swing equation from proposal: df/dt = (P_gen - P_load - P_losses) / (2 * H * S_base) + # where S_base is the base power (MVA), not nominal frequency + + # Calculate electrical power flow (simplified) + frequency_deviations = self.frequencies - self.nominal_frequency + electrical_power = torch.matmul(self.admittance_matrix, frequency_deviations) + + # Power imbalance + power_imbalance = power_injection - electrical_power + + # Update frequencies using correct swing equation (use base_power, not nominal_frequency) + frequency_derivative = power_imbalance / (2.0 * self.inertia_constants * self.base_power) + self.frequencies += frequency_derivative * self.dt * 60.0 # Convert minutes to seconds + + # NO CLAMPING - let physics run its course for realistic dynamics + + def _calculate_reward(self): + """ + Calculate the shared reward with graduated response. + Uses exponential penalties to create soft boundaries before hard violations. + """ + frequency_deviations = self.frequencies - self.nominal_frequency + + # 1. Base frequency penalty from proposal: -1000 * sum of squared deviations + frequency_penalty = 1000.0 * torch.sum(frequency_deviations ** 2) + + # 2. Exponential penalty as frequencies approach operational bounds [59.5, 60.5] + # Creates a "soft boundary" that strongly discourages approaching limits + operational_margin = 0.5 # Hz (operational bounds from proposal) + beyond_operational = torch.abs(frequency_deviations) - operational_margin + beyond_operational = torch.clamp(beyond_operational, min=0.0) # Only penalize if beyond + exponential_penalty = 5000.0 * torch.sum(torch.exp(5.0 * beyond_operational) - 1.0) + + # 3. Agent-specific costs: C_i per MW adjusted (from proposal equation 2) + if hasattr(self, 'last_actions'): + agent_costs = torch.sum(self.cost_coefficients * torch.abs(self.last_actions)) + else: + agent_costs = 0.0 + + # 4. Wear-and-tear functions: 0.1 * W_i(|ΔP^i|) (from proposal equation 2) + # Note: Using quadratic wear W_i * (ΔP^i)^2 for smoother gradients + if hasattr(self, 'last_actions'): + wear_costs = 0.1 * torch.sum(self.wear_coefficients * (self.last_actions ** 2)) + else: + wear_costs = 0.0 + + # 5. Hard safety constraint violations: 10,000 per bus violating operational bounds + freq_violations = torch.sum((torch.abs(frequency_deviations) > operational_margin)) + safety_violations = freq_violations * 10000.0 + + # Total reward (negative because we minimize costs) + reward = -(frequency_penalty + exponential_penalty + agent_costs + wear_costs + safety_violations) + + return reward.item() + + def _get_observations(self): + """Get delayed observations for each agent (2-second SCADA delay).""" + if len(self.observation_buffer) >= self.delay_steps + 1: + return self.observation_buffer[-(self.delay_steps + 1)] # Return delayed observation + else: + return self.observation_buffer[0] # Return most recent if buffer not full + + def _get_observations_immediate(self): + """Get immediate (non-delayed) observations for each agent.""" + observations = torch.zeros(self.n_agents, self.obs_dim, device=self.device) + + # Calculate system frequency deviation (key coordination signal) + system_freq_deviation = torch.mean(self.frequencies - self.nominal_frequency) + + for i in range(self.n_agents): + bus_idx = self.agent_bus_mapping[i] + obs_idx = 0 + + # Local bus frequency (1) + observations[i, obs_idx] = self.frequencies[bus_idx] + obs_idx += 1 + + # Local bus load (1) + observations[i, obs_idx] = self.loads[bus_idx] + obs_idx += 1 + + # Own generator output (1) + observations[i, obs_idx] = self.generator_outputs[i] + obs_idx += 1 + + # System frequency deviation Δf_sys = (1/68)Σ(f_k - 60) (1) + observations[i, obs_idx] = system_freq_deviation + obs_idx += 1 + + # Nearby bus frequencies (5 nearest buses) + distances = torch.abs(torch.arange(self.n_buses, device=self.device) - bus_idx) + _, nearest_indices = torch.topk(distances, k=6, largest=False) # 6 to exclude self + nearest_indices = nearest_indices[1:] # Remove self + observations[i, obs_idx:obs_idx+5] = self.frequencies[nearest_indices] + obs_idx += 5 + + # Renewable generation forecasts - next 3 time steps (3) + # Use actual forecast values for next 3 time steps, averaged across all renewable sources + if self.renewable_forecasts.shape[1] >= 3: + # Average forecast across all renewable sources for next 3 time steps + observations[i, obs_idx:obs_idx+3] = torch.mean(self.renewable_forecasts[:, :3], dim=0) + else: + # Fallback if not enough forecast steps + observations[i, obs_idx:obs_idx+3] = torch.mean(self.renewable_generation) + obs_idx += 3 + + # Time features: hour of day, day of week (2) + observations[i, obs_idx] = self.current_hour / 24.0 # Normalized hour + observations[i, obs_idx+1] = self.current_day / 7.0 # Normalized day + + # Total: 1+1+1+1+5+3+2 = 14, need 1 more for 15 + # Add own capacity utilization (1) + capacity_range = self.power_max[i] - self.power_min[i] + if capacity_range > 0: + utilization = (self.generator_outputs[i] - self.power_min[i]) / capacity_range + else: + utilization = 0.0 + observations[i, 14] = utilization + + return observations + + def _check_termination(self): + """ + Check if episode should be terminated or truncated. + Uses graduated termination criteria - only catastrophic failures terminate. + """ + # Critical violations: ±1.0 Hz from nominal (proposal bounds) + critical_violations = torch.sum((self.frequencies < 59.0) | (self.frequencies > 61.0)) + + # Catastrophic violations: ±1.5 Hz from nominal (blackout conditions) + catastrophic_violations = torch.sum((self.frequencies < 58.5) | (self.frequencies > 61.5)) + + # Terminate only on: + # 1. Catastrophic frequency deviations (±1.5 Hz) + # 2. OR >10% of buses in critical state (±1.0 Hz) + terminated = (catastrophic_violations > 0) or (critical_violations > 0.1 * self.n_buses) + + # Truncate after maximum time steps (1000 steps with dt=2s ≈ 33 minutes) + truncated = self.time_step >= 1000 + + return terminated, truncated + + def _update_time_features(self): + """Update time-based features (hour of day, day of week).""" + # Advance time by dt minutes + self.current_hour += self.dt / 60.0 + if self.current_hour >= 24.0: + self.current_hour -= 24.0 + self.current_day += 1.0 + if self.current_day > 7.0: + self.current_day = 1.0 + + def _update_renewable_forecasts(self): + """Update renewable generation forecasts for next 5-15 minutes.""" + # Simple forecast model: current + trend + noise + for i in range(14): + current_gen = self.renewable_generation[i] + + # Add trend (seasonal pattern) + hour_tensor = torch.tensor(self.current_hour, device=self.device) + trend = 10.0 * torch.sin(2 * math.pi * hour_tensor / 24.0) # Daily pattern + + # Add noise + noise = torch.randn(self.forecast_horizon, device=self.device) * 20.0 + + # Generate forecast + for t in range(self.forecast_horizon): + forecast = current_gen + trend * (t + 1) + noise[t] + self.renewable_forecasts[i, t] = torch.clamp(forecast, 0.0, 1000.0) + + def get_full_state(self): + """Get the complete 140-dimensional state vector for centralized critic.""" + # State components: frequencies (68) + generator outputs (20) + renewables (14) + loads (30) + time features (8) = 140 + state = torch.zeros(self.state_dim, device=self.device) + + idx = 0 + # Bus frequencies (68) + state[idx:idx+68] = self.frequencies + idx += 68 + + # Generator outputs (20) + state[idx:idx+20] = self.generator_outputs + idx += 20 + + # Renewable generation (14) + state[idx:idx+14] = self.renewable_generation + idx += 14 + + # Loads (first 30 buses - most critical/largest loads) + state[idx:idx+30] = self.loads[:30] + idx += 30 + + # Time features (8): hour, day, hour_sin, hour_cos, day_sin, day_cos, load_pattern, renewable_pattern + state[idx] = self.current_hour / 24.0 # Normalized hour + state[idx+1] = self.current_day / 7.0 # Normalized day + + # Convert to tensors for torch trig functions + hour_tensor = torch.tensor(self.current_hour, device=self.device) + day_tensor = torch.tensor(self.current_day, device=self.device) + + state[idx+2] = torch.sin(2 * math.pi * hour_tensor / 24.0) # Hour sine + state[idx+3] = torch.cos(2 * math.pi * hour_tensor / 24.0) # Hour cosine + state[idx+4] = torch.sin(2 * math.pi * day_tensor / 7.0) # Day sine + state[idx+5] = torch.cos(2 * math.pi * day_tensor / 7.0) # Day cosine + state[idx+6] = torch.mean(self.loads) # Average load pattern + state[idx+7] = torch.mean(self.renewable_generation) # Average renewable pattern + + return state + + def _get_info(self): + """Get additional information about the environment state.""" + system_freq_deviation = torch.mean(self.frequencies - self.nominal_frequency).item() + + return { + 'time_step': self.time_step, + 'mean_frequency': torch.mean(self.frequencies).item(), + 'frequency_std': torch.std(self.frequencies).item(), + 'system_freq_deviation': system_freq_deviation, + 'total_generation': torch.sum(self.generator_outputs).item(), + 'total_load': torch.sum(self.loads).item(), + 'contingency_active': self.contingency_active, + 'safety_violations': torch.sum((self.frequencies < self.frequency_bounds[0]) | + (self.frequencies > self.frequency_bounds[1])).item(), + 'current_hour': self.current_hour, + 'current_day': self.current_day, + 'agent_capacity_utilization': [(self.generator_outputs[i] - self.power_min[i]) / + (self.power_max[i] - self.power_min[i]) + for i in range(self.n_agents)] + } + + def render(self, mode='human'): + """Render the environment (optional).""" + if mode == 'human': + print(f"Step: {self.time_step}") + print(f"Mean frequency: {torch.mean(self.frequencies):.3f} Hz") + print(f"Frequency range: [{torch.min(self.frequencies):.3f}, {torch.max(self.frequencies):.3f}] Hz") + print(f"Total generation: {torch.sum(self.generator_outputs):.1f} MW") + print(f"Total load: {torch.sum(self.loads):.1f} MW") + print(f"Contingency active: {self.contingency_active}") + print("-" * 50) + + def close(self): + """Clean up resources.""" + pass diff --git a/readme.md b/readme.md index 1524cc5..e69de29 100644 --- a/readme.md +++ b/readme.md @@ -1,129 +0,0 @@ -# Course Project LaTeX Template - -A minimal, opinionated template for course-project writeups. Works on Overleaf and locally. - ---- - -## Quick Start - -1. **Copy the template** (clone or download). -2. **Edit metadata** in `main.tex` (title, authors, date). -3. **Write content** by adding `.tex` files under `sections/` and `\input{...}` them from `main.tex`. -4. **Add figures** to the `figure/` folder and include them with `\includegraphics`. -5. **Manage references** in `refs.bib` and cite with `\cite{...}`. -6. **Compile** on Overleaf or locally (see below). - ---- - -## Compile Options - -- **Online:** Overleaf — upload the repo and set `main.tex` as the root document. -- **Local:** VS Code with LaTeX or any editor. - ---- - -## Project Structure - -``` -. -├── main.tex # Entry point: metadata, packages, inputs, bibliography -├── preamble_packages.tex # Package imports (comment out what you don't need) -├── preamble_symbols.tex # Common symbols and math operators -├── shortcuts.tex # Project-specific commands/macros -├── refs.bib # BibTeX database -├── sections/ # Source for individual sections -│ ├── intro.tex -│ ├── related_work.tex -│ └── ... -├── figure/ # Images and plots -│ ├── system_diagram.pdf -│ └── ... -└── .gitignore # Files to exclude from version control -``` - - ---- - -## How to Use Each File - -- **`main.tex`** - - Sets the document class, title, authors, packages, and bibliography. - - Includes content, e.g.: - ```tex - \input{preamble_packages} - \input{preamble_symbols} - \input{shortcuts} - - \title{Project Title} - \author{Alice Smith \and Bob Jones} - \date{\today} - - \begin{document} - \maketitle - - \input{sections/intro} - \input{sections/related_work} - \input{sections/method} - \input{sections/experiments} - \input{sections/conclusion} - - \bibliographystyle{abbrvnat} - \bibliography{refs} - \end{document} - ``` - -- **`preamble_packages.tex`** - - Curated package list. Comment out lines you don’t need to keep the build lean. - -- **`preamble_symbols.tex`** - - Common math symbols/operators (e.g., `\R`, `\E`, `\argmin`). Extend as needed. - -- **`shortcuts.tex`** - - Project-specific macros: - ```tex - \newcommand{\method}{\textsc{OurMethod}\xspace} - ``` - -- **`refs.bib`** - - Add BibTeX entries and cite them: - ```tex - As shown by \cite{mnih2015dqn}, ... - ``` - -- **`sections/`** - - Split the paper into maintainable pieces: - - `intro.tex`, `related_work.tex`, `method.tex`, `experiments.tex`, `conclusion.tex`, etc. - - Include with `\input{sections/}` (no `.tex` extension required). - -- **`figure/`** - - Store figures/plots and include them: - ```tex - \begin{figure}[t] - \centering - \includegraphics[width=\linewidth]{figure/system_diagram} - \caption{System overview.} - \label{fig:system} - \end{figure} - ``` - -- **`.gitignore`** - - Keeps build artifacts and OS/editor files out of Git (e.g., `*.aux`, `*.log`, `*.out`, `*.synctex.gz`). - ---- - -## Best Practices - -- **Labels & refs:** `\label{sec:method}` then `\S\ref{sec:method}`; figures with `\ref{fig:system}`; equations with `\eqref{eq:loss}`. -- **Tables/Figures:** Prefer vector formats (`.pdf`, `.eps`) for diagrams; use high-resolution `.png` for raster images. -- **Keep it lean:** Only load packages you need; define macros once in `shortcuts.tex`. - ---- - -## Troubleshooting - -- **Missing references/citations:** Run LaTeX → BibTeX → LaTeX → LaTeX, or just use `latexmk -pdf`. -- **Undefined control sequence:** A macro may live in `shortcuts.tex` or a package is missing—ensure it’s included. - ---- - -Happy writing!