weather-impact-analysis

SKILL.md

Weather Impact Analysis

Overview

This skill implements weather data analysis for construction project management. Integrate weather forecasts, historical data, and activity sensitivity to predict delays and optimize scheduling.

Capabilities:

  • Weather forecast integration
  • Activity weather sensitivity mapping
  • Delay prediction and quantification
  • Schedule optimization based on weather
  • Historical weather impact analysis
  • Risk factor calculation

Quick Start

from dataclasses import dataclass
from datetime import date, datetime, timedelta
from typing import List, Dict, Optional
from enum import Enum
import requests

class WeatherCondition(Enum):
    CLEAR = "clear"
    CLOUDY = "cloudy"
    RAIN = "rain"
    HEAVY_RAIN = "heavy_rain"
    SNOW = "snow"
    FROST = "frost"
    HIGH_WIND = "high_wind"
    EXTREME_HEAT = "extreme_heat"
    EXTREME_COLD = "extreme_cold"

@dataclass
class WeatherDay:
    date: date
    condition: WeatherCondition
    temp_high: float
    temp_low: float
    precipitation_mm: float
    wind_speed_kmh: float
    humidity_pct: float

@dataclass
class ActivitySensitivity:
    activity_type: str
    min_temp: float
    max_temp: float
    max_wind: float
    max_precipitation: float
    can_work_in_rain: bool

def check_work_day(weather: WeatherDay, activity: ActivitySensitivity) -> Dict:
    """Check if work is possible for given weather and activity"""
    can_work = True
    reasons = []

    if weather.temp_low < activity.min_temp:
        can_work = False
        reasons.append(f"Temperature too low: {weather.temp_low}°C < {activity.min_temp}°C")

    if weather.temp_high > activity.max_temp:
        can_work = False
        reasons.append(f"Temperature too high: {weather.temp_high}°C > {activity.max_temp}°C")

    if weather.wind_speed_kmh > activity.max_wind:
        can_work = False
        reasons.append(f"Wind too strong: {weather.wind_speed_kmh} km/h > {activity.max_wind} km/h")

    if weather.precipitation_mm > activity.max_precipitation and not activity.can_work_in_rain:
        can_work = False
        reasons.append(f"Precipitation: {weather.precipitation_mm}mm")

    return {
        'date': weather.date,
        'can_work': can_work,
        'reasons': reasons,
        'productivity_factor': 1.0 if can_work else 0.0
    }

# Example
concrete_work = ActivitySensitivity(
    activity_type="concrete_placement",
    min_temp=5,
    max_temp=35,
    max_wind=40,
    max_precipitation=2,
    can_work_in_rain=False
)

today_weather = WeatherDay(
    date=date.today(),
    condition=WeatherCondition.RAIN,
    temp_high=15,
    temp_low=8,
    precipitation_mm=10,
    wind_speed_kmh=20,
    humidity_pct=80
)

result = check_work_day(today_weather, concrete_work)
print(f"Can work: {result['can_work']}, Reasons: {result['reasons']}")

Comprehensive Weather Analysis System

Weather Data Integration

from dataclasses import dataclass, field
from datetime import date, datetime, timedelta
from typing import List, Dict, Optional, Tuple
from enum import Enum
import requests
import json

class WeatherSeverity(Enum):
    NORMAL = 1
    CAUTION = 2
    WARNING = 3
    SEVERE = 4
    EXTREME = 5

@dataclass
class HourlyWeather:
    datetime: datetime
    temperature: float
    feels_like: float
    humidity: float
    wind_speed: float
    wind_direction: float
    precipitation: float
    precipitation_probability: float
    condition: WeatherCondition
    visibility: float
    uv_index: float

@dataclass
class DailyForecast:
    date: date
    temp_high: float
    temp_low: float
    sunrise: datetime
    sunset: datetime
    precipitation_total: float
    precipitation_probability: float
    primary_condition: WeatherCondition
    hourly: List[HourlyWeather] = field(default_factory=list)
    severity: WeatherSeverity = WeatherSeverity.NORMAL

class WeatherDataService:
    """Weather data integration service"""

    def __init__(self, api_key: str = None, provider: str = "openweathermap"):
        self.api_key = api_key
        self.provider = provider
        self.cache: Dict[str, Dict] = {}
        self.cache_duration = timedelta(hours=1)

    def get_forecast(self, latitude: float, longitude: float,
                    days: int = 14) -> List[DailyForecast]:
        """Get weather forecast for location"""
        cache_key = f"{latitude},{longitude}"

        if cache_key in self.cache:
            cached = self.cache[cache_key]
            if datetime.now() - cached['timestamp'] < self.cache_duration:
                return cached['data']

        if self.provider == "openweathermap":
            forecast = self._fetch_openweathermap(latitude, longitude, days)
        else:
            forecast = self._generate_sample_forecast(days)

        self.cache[cache_key] = {
            'timestamp': datetime.now(),
            'data': forecast
        }

        return forecast

    def _fetch_openweathermap(self, lat: float, lon: float,
                              days: int) -> List[DailyForecast]:
        """Fetch from OpenWeatherMap API"""
        url = f"https://api.openweathermap.org/data/2.5/forecast"
        params = {
            'lat': lat,
            'lon': lon,
            'appid': self.api_key,
            'units': 'metric'
        }

        try:
            response = requests.get(url, params=params)
            data = response.json()
            return self._parse_openweathermap(data)
        except Exception as e:
            print(f"Weather API error: {e}")
            return self._generate_sample_forecast(days)

    def _parse_openweathermap(self, data: Dict) -> List[DailyForecast]:
        """Parse OpenWeatherMap response"""
        forecasts = []
        daily_data = {}

        for item in data.get('list', []):
            dt = datetime.fromtimestamp(item['dt'])
            day = dt.date()

            if day not in daily_data:
                daily_data[day] = {
                    'temps': [],
                    'precipitation': 0,
                    'conditions': [],
                    'hourly': []
                }

            daily_data[day]['temps'].append(item['main']['temp'])
            daily_data[day]['precipitation'] += item.get('rain', {}).get('3h', 0)

            condition = self._map_condition(item['weather'][0]['main'])
            daily_data[day]['conditions'].append(condition)

            daily_data[day]['hourly'].append(HourlyWeather(
                datetime=dt,
                temperature=item['main']['temp'],
                feels_like=item['main']['feels_like'],
                humidity=item['main']['humidity'],
                wind_speed=item['wind']['speed'] * 3.6,  # m/s to km/h
                wind_direction=item['wind'].get('deg', 0),
                precipitation=item.get('rain', {}).get('3h', 0),
                precipitation_probability=item.get('pop', 0) * 100,
                condition=condition,
                visibility=item.get('visibility', 10000) / 1000,
                uv_index=0
            ))

        for day, data in daily_data.items():
            primary_condition = max(set(data['conditions']), key=data['conditions'].count)

            forecasts.append(DailyForecast(
                date=day,
                temp_high=max(data['temps']),
                temp_low=min(data['temps']),
                sunrise=datetime.combine(day, datetime.min.time().replace(hour=6)),
                sunset=datetime.combine(day, datetime.min.time().replace(hour=18)),
                precipitation_total=data['precipitation'],
                precipitation_probability=max(h.precipitation_probability for h in data['hourly']),
                primary_condition=primary_condition,
                hourly=data['hourly'],
                severity=self._calculate_severity(primary_condition, data)
            ))

        return sorted(forecasts, key=lambda x: x.date)

    def _map_condition(self, condition_str: str) -> WeatherCondition:
        """Map API condition to enum"""
        mapping = {
            'Clear': WeatherCondition.CLEAR,
            'Clouds': WeatherCondition.CLOUDY,
            'Rain': WeatherCondition.RAIN,
            'Drizzle': WeatherCondition.RAIN,
            'Thunderstorm': WeatherCondition.HEAVY_RAIN,
            'Snow': WeatherCondition.SNOW,
            'Mist': WeatherCondition.CLOUDY,
            'Fog': WeatherCondition.CLOUDY
        }
        return mapping.get(condition_str, WeatherCondition.CLEAR)

    def _calculate_severity(self, condition: WeatherCondition,
                           data: Dict) -> WeatherSeverity:
        """Calculate weather severity"""
        max_temp = max(data['temps'])
        min_temp = min(data['temps'])
        precip = data['precipitation']

        if condition in [WeatherCondition.HEAVY_RAIN, WeatherCondition.SNOW]:
            if precip > 50:
                return WeatherSeverity.EXTREME
            elif precip > 25:
                return WeatherSeverity.SEVERE

        if max_temp > 40 or min_temp < -15:
            return WeatherSeverity.SEVERE

        if max_temp > 35 or min_temp < -5:
            return WeatherSeverity.WARNING

        if condition == WeatherCondition.RAIN:
            return WeatherSeverity.CAUTION

        return WeatherSeverity.NORMAL

    def _generate_sample_forecast(self, days: int) -> List[DailyForecast]:
        """Generate sample forecast for testing"""
        import random
        forecasts = []

        for i in range(days):
            day = date.today() + timedelta(days=i)
            temp_base = 15 + random.uniform(-5, 10)
            condition = random.choice(list(WeatherCondition))

            forecasts.append(DailyForecast(
                date=day,
                temp_high=temp_base + random.uniform(3, 8),
                temp_low=temp_base - random.uniform(3, 8),
                sunrise=datetime.combine(day, datetime.min.time().replace(hour=6)),
                sunset=datetime.combine(day, datetime.min.time().replace(hour=18)),
                precipitation_total=random.uniform(0, 20) if condition == WeatherCondition.RAIN else 0,
                precipitation_probability=random.uniform(0, 100) if condition == WeatherCondition.RAIN else 10,
                primary_condition=condition,
                severity=WeatherSeverity.NORMAL
            ))

        return forecasts

Activity Weather Sensitivity

@dataclass
class WeatherThresholds:
    min_temp: float = -10
    max_temp: float = 45
    max_wind: float = 50
    max_precipitation: float = 50
    max_snow_depth: float = 20
    min_visibility: float = 0.5  # km

@dataclass
class ConstructionActivity:
    activity_id: str
    activity_name: str
    category: str
    thresholds: WeatherThresholds
    indoor: bool = False
    rain_sensitive: bool = True
    frost_sensitive: bool = False
    productivity_factors: Dict[WeatherCondition, float] = field(default_factory=dict)

    def __post_init__(self):
        if not self.productivity_factors:
            self.productivity_factors = {
                WeatherCondition.CLEAR: 1.0,
                WeatherCondition.CLOUDY: 0.95,
                WeatherCondition.RAIN: 0.3 if self.rain_sensitive else 0.8,
                WeatherCondition.HEAVY_RAIN: 0.0 if self.rain_sensitive else 0.5,
                WeatherCondition.SNOW: 0.2,
                WeatherCondition.FROST: 0.5 if self.frost_sensitive else 0.8,
                WeatherCondition.HIGH_WIND: 0.3,
                WeatherCondition.EXTREME_HEAT: 0.6,
                WeatherCondition.EXTREME_COLD: 0.4
            }

class ActivityWeatherAnalyzer:
    """Analyze weather impact on construction activities"""

    # Default activity definitions
    ACTIVITY_TEMPLATES = {
        'concrete_placement': ConstructionActivity(
            activity_id='ACT-001',
            activity_name='Concrete Placement',
            category='structural',
            thresholds=WeatherThresholds(min_temp=5, max_temp=35, max_precipitation=2, max_wind=40),
            rain_sensitive=True,
            frost_sensitive=True
        ),
        'steel_erection': ConstructionActivity(
            activity_id='ACT-002',
            activity_name='Steel Erection',
            category='structural',
            thresholds=WeatherThresholds(max_wind=35, max_precipitation=10),
            rain_sensitive=False
        ),
        'roofing': ConstructionActivity(
            activity_id='ACT-003',
            activity_name='Roofing',
            category='envelope',
            thresholds=WeatherThresholds(min_temp=0, max_precipitation=0, max_wind=30),
            rain_sensitive=True
        ),
        'excavation': ConstructionActivity(
            activity_id='ACT-004',
            activity_name='Excavation',
            category='earthwork',
            thresholds=WeatherThresholds(min_temp=-5, max_precipitation=25),
            rain_sensitive=True,
            frost_sensitive=True
        ),
        'painting_exterior': ConstructionActivity(
            activity_id='ACT-005',
            activity_name='Exterior Painting',
            category='finishing',
            thresholds=WeatherThresholds(min_temp=10, max_temp=35, max_precipitation=0, max_wind=25),
            rain_sensitive=True
        ),
        'masonry': ConstructionActivity(
            activity_id='ACT-006',
            activity_name='Masonry Work',
            category='structural',
            thresholds=WeatherThresholds(min_temp=5, max_temp=32, max_precipitation=5),
            rain_sensitive=True,
            frost_sensitive=True
        ),
        'crane_operations': ConstructionActivity(
            activity_id='ACT-007',
            activity_name='Crane Operations',
            category='equipment',
            thresholds=WeatherThresholds(max_wind=30, min_visibility=1.0),
            rain_sensitive=False
        ),
        'electrical_exterior': ConstructionActivity(
            activity_id='ACT-008',
            activity_name='Exterior Electrical',
            category='MEP',
            thresholds=WeatherThresholds(max_precipitation=0),
            rain_sensitive=True
        ),
        'interior_work': ConstructionActivity(
            activity_id='ACT-009',
            activity_name='Interior Work',
            category='finishing',
            thresholds=WeatherThresholds(),
            indoor=True,
            rain_sensitive=False
        )
    }

    def __init__(self):
        self.activities = dict(self.ACTIVITY_TEMPLATES)

    def add_activity(self, activity: ConstructionActivity):
        """Add custom activity"""
        self.activities[activity.activity_id] = activity

    def analyze_day(self, weather: DailyForecast,
                   activities: List[str]) -> Dict[str, Dict]:
        """Analyze weather impact for specific day and activities"""
        results = {}

        for activity_id in activities:
            activity = self.activities.get(activity_id)
            if not activity:
                continue

            impact = self._calculate_impact(weather, activity)
            results[activity_id] = impact

        return results

    def _calculate_impact(self, weather: DailyForecast,
                         activity: ConstructionActivity) -> Dict:
        """Calculate weather impact on activity"""
        if activity.indoor:
            return {
                'can_work': True,
                'productivity': 1.0,
                'issues': [],
                'recommendations': []
            }

        issues = []
        productivity = 1.0

        # Temperature check
        if weather.temp_low < activity.thresholds.min_temp:
            issues.append(f"Low temperature: {weather.temp_low}°C")
            if activity.frost_sensitive:
                productivity *= 0.0
            else:
                productivity *= 0.5

        if weather.temp_high > activity.thresholds.max_temp:
            issues.append(f"High temperature: {weather.temp_high}°C")
            productivity *= 0.6

        # Precipitation check
        if weather.precipitation_total > activity.thresholds.max_precipitation:
            issues.append(f"Precipitation: {weather.precipitation_total}mm")
            if activity.rain_sensitive:
                productivity *= 0.0
            else:
                productivity *= 0.7

        # Condition-based productivity
        condition_factor = activity.productivity_factors.get(
            weather.primary_condition, 1.0
        )
        productivity *= condition_factor

        # Generate recommendations
        recommendations = []
        if productivity < 0.5 and productivity > 0:
            recommendations.append("Consider rescheduling to more favorable day")
        if weather.temp_low < activity.thresholds.min_temp + 5:
            recommendations.append("Plan for cold weather precautions")
        if weather.precipitation_probability > 50:
            recommendations.append("Have rain contingency plan ready")

        can_work = productivity > 0

        return {
            'activity_name': activity.activity_name,
            'can_work': can_work,
            'productivity': round(productivity, 2),
            'issues': issues,
            'recommendations': recommendations,
            'weather_condition': weather.primary_condition.value,
            'temperature_range': f"{weather.temp_low}°C - {weather.temp_high}°C"
        }

    def find_optimal_days(self, forecast: List[DailyForecast],
                         activity_id: str,
                         min_productivity: float = 0.8) -> List[date]:
        """Find optimal days for an activity"""
        activity = self.activities.get(activity_id)
        if not activity:
            return []

        optimal = []
        for day in forecast:
            impact = self._calculate_impact(day, activity)
            if impact['productivity'] >= min_productivity:
                optimal.append(day.date)

        return optimal

Schedule Weather Integration

from datetime import date, timedelta
from typing import List, Dict
import pandas as pd

@dataclass
class ScheduledActivity:
    activity_id: str
    activity_name: str
    activity_type: str  # Maps to ACTIVITY_TEMPLATES
    planned_start: date
    planned_end: date
    duration_days: int
    is_critical: bool = False

class ScheduleWeatherOptimizer:
    """Optimize construction schedule based on weather"""

    def __init__(self, weather_service: WeatherDataService,
                 activity_analyzer: ActivityWeatherAnalyzer):
        self.weather = weather_service
        self.analyzer = activity_analyzer

    def analyze_schedule(self, schedule: List[ScheduledActivity],
                        location: Tuple[float, float]) -> Dict:
        """Analyze schedule against weather forecast"""
        forecast = self.weather.get_forecast(location[0], location[1])
        forecast_dict = {f.date: f for f in forecast}

        analysis = {
            'activities': [],
            'weather_delays': 0,
            'risk_days': [],
            'recommendations': []
        }

        for activity in schedule:
            activity_analysis = self._analyze_activity(
                activity, forecast_dict
            )
            analysis['activities'].append(activity_analysis)

            if activity_analysis['expected_delay'] > 0:
                analysis['weather_delays'] += activity_analysis['expected_delay']

            analysis['risk_days'].extend(activity_analysis['risk_days'])

        # Generate overall recommendations
        if analysis['weather_delays'] > 5:
            analysis['recommendations'].append(
                f"Schedule shows {analysis['weather_delays']} potential weather delay days. "
                "Consider buffer time or alternative scheduling."
            )

        return analysis

    def _analyze_activity(self, activity: ScheduledActivity,
                         forecast: Dict[date, DailyForecast]) -> Dict:
        """Analyze single activity against weather"""
        result = {
            'activity_id': activity.activity_id,
            'activity_name': activity.activity_name,
            'planned_start': activity.planned_start,
            'planned_end': activity.planned_end,
            'day_analysis': [],
            'risk_days': [],
            'expected_delay': 0,
            'avg_productivity': 1.0
        }

        current_date = activity.planned_start
        productivities = []
        delay_days = 0

        while current_date <= activity.planned_end:
            if current_date in forecast:
                weather = forecast[current_date]
                impact = self.analyzer._calculate_impact(
                    weather,
                    self.analyzer.activities.get(activity.activity_type,
                        self.analyzer.ACTIVITY_TEMPLATES.get('interior_work'))
                )

                productivities.append(impact['productivity'])

                day_info = {
                    'date': current_date,
                    'can_work': impact['can_work'],
                    'productivity': impact['productivity'],
                    'weather': weather.primary_condition.value
                }
                result['day_analysis'].append(day_info)

                if not impact['can_work']:
                    delay_days += 1
                    result['risk_days'].append({
                        'date': current_date,
                        'activity': activity.activity_name,
                        'reason': weather.primary_condition.value
                    })
                elif impact['productivity'] < 0.7:
                    delay_days += (1 - impact['productivity'])

            current_date += timedelta(days=1)

        result['expected_delay'] = round(delay_days)
        result['avg_productivity'] = sum(productivities) / len(productivities) if productivities else 1.0

        return result

    def suggest_reschedule(self, activity: ScheduledActivity,
                          location: Tuple[float, float],
                          flexibility_days: int = 7) -> Optional[date]:
        """Suggest better start date for activity"""
        forecast = self.weather.get_forecast(location[0], location[1])

        best_start = None
        best_avg_productivity = 0

        for offset in range(-flexibility_days, flexibility_days + 1):
            test_start = activity.planned_start + timedelta(days=offset)
            test_end = test_start + timedelta(days=activity.duration_days - 1)

            productivities = []
            for f in forecast:
                if test_start <= f.date <= test_end:
                    act_template = self.analyzer.activities.get(activity.activity_type)
                    if act_template:
                        impact = self.analyzer._calculate_impact(f, act_template)
                        productivities.append(impact['productivity'])

            if productivities:
                avg = sum(productivities) / len(productivities)
                if avg > best_avg_productivity:
                    best_avg_productivity = avg
                    best_start = test_start

        if best_start and best_start != activity.planned_start:
            return best_start
        return None

    def generate_weather_report(self, schedule: List[ScheduledActivity],
                               location: Tuple[float, float],
                               output_path: str) -> str:
        """Generate weather impact report"""
        analysis = self.analyze_schedule(schedule, location)

        with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
            # Summary
            summary = pd.DataFrame([{
                'Total Activities': len(schedule),
                'Weather Delay Days': analysis['weather_delays'],
                'High Risk Days': len(analysis['risk_days']),
                'Recommendations': len(analysis['recommendations'])
            }])
            summary.to_excel(writer, sheet_name='Summary', index=False)

            # Activity details
            activity_data = []
            for act in analysis['activities']:
                activity_data.append({
                    'Activity': act['activity_name'],
                    'Start': act['planned_start'],
                    'End': act['planned_end'],
                    'Avg Productivity': f"{act['avg_productivity']:.0%}",
                    'Expected Delay': f"{act['expected_delay']} days"
                })
            pd.DataFrame(activity_data).to_excel(writer, sheet_name='Activities', index=False)

            # Risk days
            if analysis['risk_days']:
                pd.DataFrame(analysis['risk_days']).to_excel(
                    writer, sheet_name='Risk_Days', index=False
                )

        return output_path

Quick Reference

Activity Type Min Temp Max Precip Max Wind Rain Sensitive
Concrete 5°C 2mm 40 km/h Yes
Steel Erection -10°C 10mm 35 km/h No
Roofing 0°C 0mm 30 km/h Yes
Excavation -5°C 25mm 50 km/h Partial
Exterior Painting 10°C 0mm 25 km/h Yes
Masonry 5°C 5mm 40 km/h Yes
Crane Operations -15°C 20mm 30 km/h No

Resources

Next Steps

  • See 4d-simulation for schedule visualization
  • See risk-assessment-ml for weather risk prediction
  • See site-logistics-optimization for delivery scheduling
Weekly Installs
8
GitHub Stars
55
First Seen
11 days ago
Installed on
opencode8
gemini-cli8
github-copilot8
codex8
kimi-cli8
amp8