Tutorials
TutorialIntermediate2026-07-06

# Construire un outil RCCP assisté par AI pour l'équilibrage de capacité à moyen terme ## Vue d'ensemble Le Rough Cut Capacity Planning (RCCP) est un processus critique du S&OP qui valide si les plans de production agrégés peuvent être exécutés avec les ressources disponibles. Un outil RCCP assisté par AI améliore ce processus en automatisant les calculs de charge, en identifiant les goulots d'étranglement et en recommandant des actions d'équilibrage pour un horizon de 3 à 18 mois. ## Composants principaux du système ### 1. Moteur de calcul de charge **Données d'entrée :** - Plan de production agrégé par famille de produits - Profils de routage et temps standard par famille - Calendriers de ressources et disponibilité - Définitions de groupes de capacité (postes de charge, lignes, départements) **Logique de calcul :** ```python import pandas as pd import numpy as np class LoadCalculationEngine: def __init__(self, resource_calendar, routing_profiles): self.resource_calendar = resource_calendar self.routing_profiles = routing_profiles def calculate_capacity_load(self, production_plan, time_horizon): """ Calcule les besoins en capacité par ressource et période """ load_by_resource = {} for period in time_horizon: for product_family, quantity in production_plan[period].items(): routing = self.routing_profiles[product_family] for operation in routing: resource = operation['resource_group'] std_hours = operation['standard_hours'] setup_hours = operation['setup_hours'] # Calcul de la charge total_hours = (quantity * std_hours) + setup_hours if resource not in load_by_resource: load_by_resource[resource] = {} if period not in load_by_resource[resource]: load_by_resource[resource][period] = 0 load_by_resource[resource][period] += total_hours return self.create_load_profile(load_by_resource) def create_load_profile(self, load_data): """ Crée un profil de charge structuré avec disponibilité et utilisation """ profile = [] for resource, periods in load_data.items(): for period, load_hours in periods.items(): available_hours = self.resource_calendar.get_capacity( resource, period ) utilization = (load_hours / available_hours * 100) if available_hours > 0 else 0 profile.append({ 'resource': resource, 'period': period, 'required_hours': load_hours, 'available_hours': available_hours, 'utilization_pct': utilization, 'gap_hours': load_hours - available_hours }) return pd.DataFrame(profile) ``` ### 2. Module de détection des goulots d'étranglement **Identification des contraintes de capacité :** ```python class BottleneckDetector: def __init__(self, utilization_threshold=85, criticality_window=3): self.utilization_threshold = utilization_threshold self.criticality_window = criticality_window def identify_bottlenecks(self, load_profile): """ Identifie les goulots d'étranglement basés sur l'utilisation et la persistance """ bottlenecks = [] # Grouper par ressource for resource in load_profile['resource'].unique(): resource_data = load_profile[ load_profile['resource'] == resource ].sort_values('period') # Identifier les périodes de surcharge overload_periods = resource_data[ resource_data['utilization_pct'] > self.utilization_threshold ] if len(overload_periods) > 0: # Analyser la sévérité severity = self._calculate_severity(overload_periods) # Analyser la criticité (périodes consécutives) criticality = self._analyze_criticality( resource_data, overload_periods ) bottlenecks.append({ 'resource': resource, 'affected_periods': len(overload_periods), 'avg_utilization': overload_periods['utilization_pct'].mean(), 'max_utilization': overload_periods['utilization_pct'].max(), 'total_gap_hours': overload_periods['gap_hours'].sum(), 'severity_score': severity, 'criticality_score': criticality, 'periods': overload_periods['period'].tolist() }) # Classer par criticité bottlenecks_df = pd.DataFrame(bottlenecks) if not bottlenecks_df.empty: bottlenecks_df = bottlenecks_df.sort_values( ['criticality_score', 'severity_score'], ascending=False ) return bottlenecks_df def _calculate_severity(self, overload_periods): """ Calcule le score de sévérité basé sur l'ampleur et la durée """ avg_excess = (overload_periods['utilization_pct'] - 100).mean() duration = len(overload_periods) severity = (avg_excess * 0.6) + (duration * 10 * 0.4) return severity def _analyze_criticality(self, resource_data, overload_periods): """ Analyse les périodes consécutives de surcharge """ overload_indices = overload_periods.index.tolist() all_indices = resource_data.index.tolist() max_consecutive = 0 current_consecutive = 0 for idx in all_indices: if idx in overload_indices: current_consecutive += 1 max_consecutive = max(max_consecutive, current_consecutive) else: current_consecutive = 0 return max_consecutive * 20 # Score de criticité ``` ### 3. Moteur de recommandations AI **Génération de stratégies d'équilibrage :** ```python from sklearn.ensemble import RandomForestClassifier import itertools class AIBalancingRecommender: def __init__(self, historical_actions): self.historical_actions = historical_actions self.model = self._train_action_model() def _train_action_model(self): """ Entraîne un modèle pour prédire l'efficacité des actions """ if self.historical_actions.empty: return None # Features : caractéristiques du goulot d'étranglement X = self.historical_actions[[ 'utilization_pct', 'gap_hours', 'affected_periods', 'resource_type', 'lead_time_weeks' ]] # Target : efficacité de l'action (résolu, partiel, échec) y = self.historical_actions['action_effectiveness'] model = RandomForestClassifier(n_estimators=100, random_state=42) # Encoder les variables catégorielles X_encoded = pd.get_dummies(X, columns=['resource_type']) model.fit(X_encoded, y) return model def generate_recommendations(self, bottlenecks, load_profile, production_plan, constraints): """ Génère des recommandations priorisées pour résoudre les goulots """ recommendations = [] for _, bottleneck in bottlenecks.iterrows(): resource = bottleneck['resource'] periods = bottleneck['periods'] gap_hours = bottleneck['total_gap_hours'] # Générer plusieurs options options = self._generate_balancing_options( resource, periods, gap_hours, load_profile, production_plan, constraints ) # Scorer chaque option for option in options: score = self._score_option(option, bottleneck) recommendations.append({ 'resource': resource, 'action_type': option['type'], 'description': option['description'], 'estimated_capacity_gain': option['capacity_gain'], 'cost_estimate': option['cost'], 'implementation_time': option['lead_time'], 'feasibility_score': score, 'periods_affected': periods, 'details': option['details'] }) recommendations_df = pd.DataFrame(recommendations) if not recommendations_df.empty: recommendations_df = recommendations_df.sort_values( 'feasibility_score', ascending=False ) return recommendations_df def _generate_balancing_options(self, resource, periods, gap_hours, load_profile, production_plan, constraints): """ Génère différentes options d'équilibrage """ options = [] # Option 1 : Overtime if constraints.get('allow_overtime', True): overtime_capacity = gap_hours overtime_cost = overtime_capacity * constraints.get('overtime_rate', 1.5) options.append({ 'type': 'overtime', 'description': f'Ajouter {overtime_capacity:.0f} heures supplémentaires', 'capacity_gain': overtime_capacity, 'cost': overtime_cost, 'lead_time': 0, 'details': {'hours': overtime_capacity, 'periods': periods} }) # Option 2 : Shift additionnel if constraints.get('allow_additional_shifts', True): shift_capacity = constraints.get('hours_per_shift', 160) * len(periods) shift_cost = shift_capacity * constraints.get('shift_cost_multiplier', 1.3) if shift_capacity >= gap_hours * 0.8: options.append({ 'type': 'additional_shift', 'description': 'Ajouter un shift (équipe supplémentaire)', 'capacity_gain': shift_capacity, 'cost': shift_cost, 'lead_time': 4, # semaines 'details': {'shifts': 1, 'periods': periods} }) # Option 3 : Sous-traitance if constraints.get('allow_subcontracting', True): subcontract_capacity = gap_hours subcontract_cost = subcontract_capacity * constraints.get('subcontract_rate', 2.0) options.append({ 'type': 'subcontracting', 'description': f'Sous-traiter {subcontract_capacity:.0f} heures', 'capacity_gain': subcontract_capacity, 'cost': subcontract_cost, 'lead_time': 2, 'details': {'hours': subcontract_capacity, 'periods': periods} }) # Option 4 : Nivellement de charge (load leveling) leveling_options = self._analyze_load_leveling( resource, periods, load_profile, production_plan ) if leveling_options: options.extend(leveling_options) # Option 5 : Investissement capacitaire if gap_hours > constraints.get('investment_threshold', 500): equipment_cost = constraints.get('equipment_investment', 100000) capacity_increase = constraints.get('equipment_capacity', 2000) options.append({ 'type': 'capital_investment', 'description': 'Acquérir équipement/ligne supplémentaire', 'capacity_gain': capacity_increase, 'cost': equipment_cost, 'lead_time': 12, 'details': {'type': 'equipment', 'capacity': capacity_increase} }) return options def _analyze_load_leveling(self, resource, overload_periods, load_profile, production_plan): """ Analyse les opportunités de nivellement de charge """ options = [] # Identifier les périodes sous-utilisées resource_profile = load_profile[load_profile['resource'] == resource] underload_periods = resource_profile[ resource_profile['utilization_pct'] < 70 ] if len(underload_periods) > 0: # Calculer la capacité disponible available_capacity = ( underload_periods['available_hours'] - underload_periods['required_hours'] ).sum() if available_capacity > 0: options.append({ 'type': 'load_leveling', 'description': 'Déplacer production vers périodes creuses', 'capacity_gain': available_capacity, 'cost': available_capacity * 0.1, # Coût de nivellement minimal 'lead_time': 0, 'details': { 'from_periods': overload_periods, 'to_periods': underload_periods['period'].tolist(), 'capacity_available': available_capacity } }) return options def _score_option(self, option, bottleneck): """ Score une option basée sur efficacité, coût et faisabilité """ # Efficacité : l'option résout-elle le problème ? coverage = min(option['capacity_gain'] / bottleneck['total_gap_hours'], 1.0) # Coût normalisé (inversé pour que moins cher = meilleur score) cost_normalized = 1 / (1 + option['cost'] / 10000) # Lead time (plus court = meilleur) time_score = 1 / (1 + option['lead_time'] / 12) # Score composite score = (coverage * 0.4) + (cost_normalized * 0.3) + (time_score * 0.3) return score * 100 ``` ### 4. Module de simulation de scénarios **Test des stratégies d'équilibrage :** ```python class CapacityScenarioSimulator: def __init__(self, load_calculator, bottleneck_detector): self.load_calculator = load_calculator self.bottleneck_detector = bottleneck_detector def simulate_scenario(self, production_plan, actions, time_horizon): """ Simule l'impact des actions d'équilibrage """ # Calcul de charge de base baseline_load = self.load_calculator.calculate_capacity_load( production_plan, time_horizon ) # Appliquer les actions adjusted_load = self._apply_actions(baseline_load, actions) # Détection des goulots après actions remaining_bottlenecks = self.bottleneck_detector.identify_bottlenecks( adjusted_load ) # Métriques de comparaison comparison = self._compare_scenarios( baseline_load, adjusted_load, remaining_bottlenecks ) return { 'baseline': baseline_load, 'adjusted': adjusted_load, 'remaining_bottlenecks': remaining_bottlenecks, 'comparison': comparison, 'actions_applied': actions } def _apply_actions(self, load_profile, actions): """ Applique les actions au profil de charge """ adjusted = load_profile.copy() for action in actions: resource = action['resource'] action_type = action['action_type'] details = action['details'] if action_type == 'overtime': # Ajouter capacité overtime mask = (adjusted['resource'] == resource) & \ (adjusted['period'].isin(details['periods'])) adjusted.loc[mask, 'available_hours'] += \ details['hours'] / len(details['periods']) elif action_type == 'additional_shift': # Ajouter un shift complet mask = (adjusted['resource'] == resource) & \ (adjusted['period'].isin(details['periods'])) adjusted.loc[mask, 'available_hours'] *= 1.5 # Approximation elif action_type == 'subcontracting': # Réduire la charge requise mask = (adjusted['resource'] == resource) & \ (adjusted['period'].isin(details['periods'])) adjusted.loc[mask, 'required_hours'] -= \ details['hours'] / len(details['periods']) elif action_type == 'load_leveling': # Déplacer charge entre périodes adjusted = self._apply_load_leveling( adjusted, resource, details ) elif action_type == 'capital_investment': # Augmentation permanente de capacité mask = adjusted['resource'] == resource adjusted.loc[mask, 'available_hours'] += details['capacity'] / 12 # Recalculer utilisation et gap adjusted['utilization_pct'] = ( adjusted['required_hours'] / adjusted['available_hours'] * 100 ) adjusted['gap_hours'] = ( adjusted['required_hours'] - adjusted['available_hours'] ) return adjusted def _apply_load_leveling(self, load_profile, resource, details): """ Applique le nivellement de charge entre périodes """ adjusted = load_profile.copy() from_periods = details['from_periods'] to_periods = details['to_periods'] # Logique de déplacement simplifée # Dans un vrai système, cela nécessiterait une optimisation plus sophistiquée return adjusted def _compare_scenarios(self, baseline, adjusted, remaining_bottlenecks): """ Compare les scénarios baseline et ajusté """ baseline_overload = len(baseline[baseline['utilization_pct'] > 100]) adjusted_overload = len(adjusted[adjusted['utilization_pct'] > 100]) baseline_avg_util = baseline['utilization_pct'].mean() adjusted_avg_util = adjusted['utilization_pct'].mean() return { 'overloaded_periods_before': baseline_overload, 'overloaded_periods_after': adjusted_overload, 'improvement_pct': ( (baseline_overload - adjusted_overload) / baseline_overload * 100 if baseline_overload > 0 else 0 ), 'avg_utilization_before': baseline_avg_util, 'avg_utilization_after': adjusted_avg_util, 'bottlenecks_resolved': len(baseline) - len(remaining_bottlenecks), 'bottlenecks_remaining': len(remaining_bottlenecks) } ``` ## Interface utilisateur et visualisations ### Tableau de bord RCCP ```python import plotly.graph_objects as go from plotly.subplots import make_subplots class RCCPDashboard: def create_capacity_heatmap(self, load_profile): """ Crée une heatmap d'utilisation de capacité """ # Pivoter les données pour heatmap pivot_data = load_profile.pivot( index='resource', columns='period', values='utilization_pct' ) fig = go.Figure(data=go.Heatmap( z=pivot_data.values, x=pivot_data.columns, y=pivot_data.index, colorscale=[ [0, 'green'], [0.7, 'yellow'], [0.85, 'orange'], [1, 'red'] ], colorbar=dict(title='Utilisation %'), text=pivot_data.values, texttemplate='%{text:.1f}%', textfont={"size": 10} )) fig.update_layout( title='Profil d\'utilisation de capacité par ressource', xaxis_title='Période', yaxis_title='Ressource', height=600 ) return fig def create_bottleneck_chart(self, bottlenecks): """ Visualise les goulots d'étranglement """ fig = go.Figure() fig.add_trace(go.Bar( x=bottlenecks['resource'], y=bottlenecks['total_gap_hours'], name='Heures de déficit', marker_color='red' )) fig.update_layout( title='Déficits de capacité par ressource', xaxis_title='Ressource', yaxis_title='Heures de déficit', showlegend=True, height=400 ) return fig def create_load_profile_chart(self, load_profile, resource): """ Graphique de profil de charge pour une ressource """ resource_data = load_profile[ load_profile['resource'] == resource ].sort_values('period') fig = go.Figure() # Capacité disponible fig.add_trace(go.Scatter( x=resource_data['period'], y=resource_data['available_hours'], name='Capacité disponible', line=dict(color='green', dash='dash') )) # Charge requise fig.add_trace(go.Scatter( x=resource_data['period'], y=resource_data['required_hours'], name='Charge requise', line=dict(color='blue'), fill='tonexty' )) fig.update_layout( title=f'Profil de charge - {resource}', xaxis_title='Période', yaxis_title='Heures', height=400 ) return fig def create_scenario_comparison(self, comparison_data): """ Compare les scénarios baseline vs ajusté """ metrics = ['overloaded_periods', 'avg_utilization', 'bottlenecks'] baseline_values = [ comparison_data['overloaded_periods_before'], comparison_data['avg_utilization_before'], comparison_data['overloaded_periods_before'] ] adjusted_values = [ comparison_data['overloaded_periods_after'], comparison_data['avg_utilization_after'], comparison_data['bottlenecks_remaining'] ] fig = go.Figure(data=[ go.Bar(name='Baseline', x=metrics, y=baseline_values), go.Bar(name='Ajusté', x=metrics, y=adjusted_values) ]) fig.update_layout( title='Comparaison de scénarios', barmode='group', height=400 ) return fig ``` ## Architecture système complète ```python class RCCPSystem: """ Système RCCP intégré assisté par AI """ def __init__(self, config): self.config = config # Initialiser les composants self.load_calculator = LoadCalculationEngine( config['resource_calendar'], config['routing_profiles'] ) self.bottleneck_detector = BottleneckDetector( utilization_threshold=config.get('utilization_threshold', 85) ) self.recommender = AIBalancingRecommender( config.get('historical_actions', pd.DataFrame()) ) self.simulator = CapacityScenarioSimulator( self.load_calculator, self.bottleneck_detector ) self.dashboard = RCCPDashboard() def run_rccp_analysis(self, production_plan, time_horizon, constraints): """ Exécute l'analyse RCCP complète """ print("Étape 1/5 : Calcul des besoins en capacité...") load_profile = self.load_calculator.calculate_capacity_load( production_plan, time_horizon ) print("Étape 2/5 : Détection des goulots d'étranglement...") bottlenecks = self.bottleneck_detector.identify_bottlenecks(load_profile) print(f"Goulots identifiés : {len(bottlenecks)}") if len(bottlenecks) == 0: return { 'status': 'FAISABLE', 'load_profile': load_profile, 'bottlenecks': bottlenecks, 'recommendations': pd.DataFrame(), 'message': 'Le plan de production est faisable avec la capacité actuelle' } print("Étape 3/5 : Génération de recommandations...") recommendations = self.recommender.generate_recommendations( bottlenecks, load_profile, production_plan, constraints ) print(f"Recommandations générées : {len(recommendations)}") print("Étape 4/5 : Simulation de scénarios...") # Sélectionner les meilleures recommandations top_actions = recommendations.head(5).to_dict('records') scenario_result = self.simulator.simulate_scenario( production_plan, top_actions, time_horizon ) print("Étape 5/5 : Génération des visualisations...") visualizations = { 'heatmap': self.dashboard.create_capacity_heatmap(load_profile), 'bottlenecks': self.dashboard.create_bottleneck_chart(bottlenecks), 'scenario_comparison': self.dashboard.create_scenario_comparison( scenario_result['comparison'] ) } return { 'status': 'CONTRAINTES' if len(bottlenecks) > 0 else 'FAISABLE', 'load_profile': load_profile, 'bottlenecks': bottlenecks, 'recommendations': recommendations, 'scenario_simulation': scenario_result, 'visualizations': visualizations, 'summary': self._generate_summary(bottlenecks, recommendations, scenario_result) } def _generate_summary(self, bottlenecks, recommendations, scenario): """ Génère un résumé exécutif """ summary = { 'total_bottlenecks': len(bottlenecks), 'critical_resources': bottlenecks['resource'].tolist() if not bottlenecks.empty else [], 'total_capacity_gap_hours': bottlenecks['total_gap_hours'].sum() if not bottlenecks.empty else 0, 'top_recommendation': recommendations.iloc[0].to_dict() if not recommendations.empty else None, 'estimated_improvement': scenario['comparison']['improvement_pct'], 'action_required': len(bottlenecks) > 0 } return summary ``` ## Exemple d'utilisation ```python # Configuration config = { 'resource_calendar': resource_calendar_obj, 'routing_profiles': routing_data, 'utilization_threshold': 85, 'historical_actions': historical_df } # Initialiser le système rccp_system = RCCPSystem(config) # Plan de production (exemple) production_plan = { '2024-01': {'ProductFamily_A': 1000, 'ProductFamily_B': 500}, '2024-02': {'ProductFamily_A': 1200, 'ProductFamily_B': 600}, '2024-03': {'ProductFamily_A': 1100, 'ProductFamily_B': 550}, # ... autres périodes } time_horizon = ['2024-01', '2024-02', '2024-03', '2024-04', '2024-05', '2024-06'] constraints = { 'allow_overtime': True, 'allow_additional_shifts': True, 'allow_subcontracting': True, 'overtime_rate': 1.5, 'shift_cost_multiplier': 1.3, 'subcontract_rate': 2.0, 'investment_threshold': 1000, 'equipment_investment': 150000, 'equipment_capacity': 2000, 'hours_per_shift': 160 } # Exécuter l'analyse results = rccp_system.run_rccp_analysis( production_plan, time_horizon, constraints ) # Afficher les résultats print(f"\nStatut : {results['status']}") print(f"Goulots identifiés : {results['summary']['total_bottlenecks']}") print(f"Déficit total : {results['summary']['total_capacity_gap_hours']:.0f} heures") if results['summary']['top_recommendation']: top_rec = results['summary']['top_recommendation'] print(f"\nRecommandation principale :") print(f" - Action : {top_rec['description']}") print(f" - Gain de capacité : {top_rec['estimated_capacity_gain']:.0f} heures") print(f" - Coût estimé : {top_rec['cost_estimate']:.0f} €") print(f" - Délai : {top_rec['implementation_time']} semaines") # Visualisations results['visualizations']['heatmap'].show() results['visualizations']['bottlenecks'].show() ``` ## Intégration avec l'ERP et systèmes existants ```python class ERPIntegration: """ Module d'intégration avec systèmes ERP """ def __init__(self, erp_connector): self.erp = erp_connector def fetch_production_plan(self, planning_version, time_horizon): """ Récupère le plan de production depuis l'ERP """ query = f""" SELECT period, product_family, planned_quantity FROM production_plan WHERE version = '{planning_version}' AND period BETWEEN '{time_horizon[0]}' AND '{time_horizon[-1]}' """ df = self.erp.execute_query(query) # Transformer en structure attendue plan = {} for _, row in df.iterrows(): period = row['period'] if period not in plan: plan[period] = {} plan[period][row['product_family']] = row['planned_quantity'] return plan def fetch_resource_calendar(self, time_horizon): """ Récupère les calendriers de ressources """ query = f""" SELECT resource_id, resource_group, period, available_hours, efficiency_factor FROM resource_calendar WHERE period BETWEEN '{time_horizon[0]}' AND '{time_horizon[-1]}' """ return self.erp.execute_query(query) def fetch_routing_data(self): """ Récupère les données de routage """ query = """ SELECT product_family, operation_seq, resource_group, standard_hours_per_unit, setup_hours FROM routing_master WHERE active = 1 """ return self.erp.execute_query(query) def publish_rccp_results(self, results, version): """ Publie les résultats RCCP dans l'ERP """ # Publier le profil de charge self._publish_load_profile(results['load_profile'], version) # Publier les goulots self._publish_bottlenecks(results['bottlenecks'], version) # Publier les recommandations self._publish_recommendations(results['recommendations'], version) def _publish_load_profile(self, load_profile, version): load_profile['version'] = version load_profile['analysis_date'] = pd.Timestamp.now() self.erp.bulk_insert('rccp_load_profile', load_profile) def _publish_bottlenecks(self, bottlenecks, version): if not bottlenecks.empty: bottlenecks['version'] = version bottlenecks['analysis_date'] = pd.Timestamp.now() self.erp.bulk_insert('rccp_bottlenecks', bottlenecks) def _publish_recommendations(self, recommendations, version): if not recommendations.empty: recommendations['version'] = version recommendations['analysis_date'] = pd.Timestamp.now() self.erp.bulk_insert('rccp_recommendations', recommendations) ``` ## Fonctionnalités avancées ### 1. Apprentissage continu ```python class ContinuousLearning: """ Module d'apprentissage continu à partir des actions passées """ def __init__(self, feedback_database): self.feedback_db = feedback_database def record_action_outcome(self, action_id, outcome_metrics): """ Enregistre l'issue d'une action recommandée """ self.feedback_db.insert({ 'action_id': action_id, 'outcome': outcome_metrics['status'], # 'success', 'partial', 'failed' 'actual_capacity_gain': outcome_metrics['capacity_gain'], 'actual_cost': outcome_metrics['cost'], 'actual_implementation_time': outcome_metrics['implementation_days'], 'timestamp': pd.Timestamp.now() }) def retrain_model(self, recommender): """ Réentraîne le modèle avec les nouvelles données """ updated_data = self.feedback_db.fetch_all() recommender.historical_actions = updated_data recommender.model = recommender._train_action_model() ``` ### 2. Alertes proactives ```python class ProactiveAlerts: """ Système d'alertes pour problèmes de capacité émergents """ def monitor_capacity_trends(self, load_profile, historical_data): """ Surveille les tendances et déclenche des alertes """ alerts = [] for resource in load_profile['resource'].unique(): resource_trend = self._analyze_trend(resource, historical_data) if resource_trend['direction'] == 'increasing' and \ resource_trend['projected_utilization'] > 90: alerts.append({ 'severity': 'HIGH', 'resource': resource, 'message': f'Tendance croissante détectée. Utilisation projetée : {resource_trend["projected_utilization"]:.1f}%', 'recommended_action': 'Planifier augmentation de capacité' }) return alerts def _analyze_trend(self, resource, historical_data): # Analyse de tendance simplifiée resource_history = historical_data[historical_data['resource'] == resource] # Régression linéaire simple from scipy import stats x = range(len(resource_history)) y = resource_history['utilization_pct'].values slope, intercept, r_value, p_value, std_err = stats.linregress(x, y) projected = slope * (len(x) + 3) + intercept # Projection 3 périodes return { 'direction': 'increasing' if slope > 0 else 'decreasing', 'slope': slope, 'projected_utilization': projected, 'confidence': r_value ** 2 } ``` ## Points clés d'implémentation 1. **Granularité appropriée** : Équilibrer entre détail (SKU-ressource) et agrégation (famille-workcentre) 2. **Fenêtre temporelle** : 3-18 mois typique pour RCCP, avec buckets hebdomadaires ou mensuels 3. **Données de qualité** : Temps standards maintenus, calendriers à jour, routages précis 4. **Flexibilité des contraintes** : Permettre ajustement des règles métier par utilisateur 5. **Performance** : Optimiser les calculs pour grands volumes (parallélisation, caching) 6. **Boucle de feedback** : Capturer résultats réels pour amélioration continue du modèle 7. **Intégration S&OP** : Synchroniser avec cycle S&OP mensuel et mise à jour IBP Cet outil RCCP assisté par AI transforme un processus manuel et chronophage en analyse automatisée avec recommandations actionnables, permettant aux planificateurs de se concentrer sur les décisions stratégiques plutôt que sur les calculs.

Un tutoriel pratique pour construire une application de décision centrée sur le planificateur avec Python, Streamlit et une couche d'explication AI.

# Construire un outil RCCP assisté par AI pour l'équilibrage de capacité à moyen terme

## Vue d'ensemble

Le Rough Cut Capacity Planning (RCCP) est un processus critique du S&OP qui valide si les plans de production agrégés peuvent être exécutés avec les ressources disponibles. Un outil RCCP assisté par AI améliore ce processus en automatisant les calculs de charge, en identifiant les goulots d'étranglement et en recommandant des actions d'équilibrage pour un horizon de 3 à 18 mois.

## Composants principaux du système

### 1. Moteur de calcul de charge

**Données d'entrée :**
- Plan de production agrégé par famille de produits
- Profils de routage et temps standard par famille
- Calendriers de ressources et disponibilité
- Définitions de groupes de capacité (postes de charge, lignes, départements)

**Logique de calcul :**

```python
import pandas as pd
import numpy as np

class LoadCalculationEngine:
    def __init__(self, resource_calendar, routing_profiles):
        self.resource_calendar = resource_calendar
        self.routing_profiles = routing_profiles
    
    def calculate_capacity_load(self, production_plan, time_horizon):
        """
        Calcule les besoins en capacité par ressource et période
        """
        load_by_resource = {}
        
        for period in time_horizon:
            for product_family, quantity in production_plan[period].items():
                routing = self.routing_profiles[product_family]
                
                for operation in routing:
                    resource = operation['resource_group']
                    std_hours = operation['standard_hours']
                    setup_hours = operation['setup_hours']
                    
                    # Calcul de la charge
                    total_hours = (quantity * std_hours) + setup_hours
                    
                    if resource not in load_by_resource:
                        load_by_resource[resource] = {}
                    
                    if period not in load_by_resource[resource]:
                        load_by_resource[resource][period] = 0
                    
                    load_by_resource[resource][period] += total_hours
        
        return self.create_load_profile(load_by_resource)
    
    def create_load_profile(self, load_data):
        """
        Crée un profil de charge structuré avec disponibilité et utilisation
        """
        profile = []
        
        for resource, periods in load_data.items():
            for period, load_hours in periods.items():
                available_hours = self.resource_calendar.get_capacity(
                    resource, period
                )
                
                utilization = (load_hours / available_hours * 100) if available_hours > 0 else 0
                
                profile.append({
                    'resource': resource,
                    'period': period,
                    'required_hours': load_hours,
                    'available_hours': available_hours,
                    'utilization_pct': utilization,
                    'gap_hours': load_hours - available_hours
                })
        
        return pd.DataFrame(profile)
```

### 2. Module de détection des goulots d'étranglement

**Identification des contraintes de capacité :**

```python
class BottleneckDetector:
    def __init__(self, utilization_threshold=85, criticality_window=3):
        self.utilization_threshold = utilization_threshold
        self.criticality_window = criticality_window
    
    def identify_bottlenecks(self, load_profile):
        """
        Identifie les goulots d'étranglement basés sur l'utilisation et la persistance
        """
        bottlenecks = []
        
        # Grouper par ressource
        for resource in load_profile['resource'].unique():
            resource_data = load_profile[
                load_profile['resource'] == resource
            ].sort_values('period')
            
            # Identifier les périodes de surcharge
            overload_periods = resource_data[
                resource_data['utilization_pct'] > self.utilization_threshold
            ]
            
            if len(overload_periods) > 0:
                # Analyser la sévérité
                severity = self._calculate_severity(overload_periods)
                
                # Analyser la criticité (périodes consécutives)
                criticality = self._analyze_criticality(
                    resource_data, overload_periods
                )
                
                bottlenecks.append({
                    'resource': resource,
                    'affected_periods': len(overload_periods),
                    'avg_utilization': overload_periods['utilization_pct'].mean(),
                    'max_utilization': overload_periods['utilization_pct'].max(),
                    'total_gap_hours': overload_periods['gap_hours'].sum(),
                    'severity_score': severity,
                    'criticality_score': criticality,
                    'periods': overload_periods['period'].tolist()
                })
        
        # Classer par criticité
        bottlenecks_df = pd.DataFrame(bottlenecks)
        if not bottlenecks_df.empty:
            bottlenecks_df = bottlenecks_df.sort_values(
                ['criticality_score', 'severity_score'], 
                ascending=False
            )
        
        return bottlenecks_df
    
    def _calculate_severity(self, overload_periods):
        """
        Calcule le score de sévérité basé sur l'ampleur et la durée
        """
        avg_excess = (overload_periods['utilization_pct'] - 100).mean()
        duration = len(overload_periods)
        
        severity = (avg_excess * 0.6) + (duration * 10 * 0.4)
        return severity
    
    def _analyze_criticality(self, resource_data, overload_periods):
        """
        Analyse les périodes consécutives de surcharge
        """
        overload_indices = overload_periods.index.tolist()
        all_indices = resource_data.index.tolist()
        
        max_consecutive = 0
        current_consecutive = 0
        
        for idx in all_indices:
            if idx in overload_indices:
                current_consecutive += 1
                max_consecutive = max(max_consecutive, current_consecutive)
            else:
                current_consecutive = 0
        
        return max_consecutive * 20  # Score de criticité
```

### 3. Moteur de recommandations AI

**Génération de stratégies d'équilibrage :**

```python
from sklearn.ensemble import RandomForestClassifier
import itertools

class AIBalancingRecommender:
    def __init__(self, historical_actions):
        self.historical_actions = historical_actions
        self.model = self._train_action_model()
        
    def _train_action_model(self):
        """
        Entraîne un modèle pour prédire l'efficacité des actions
        """
        if self.historical_actions.empty:
            return None
        
        # Features : caractéristiques du goulot d'étranglement
        X = self.historical_actions[[
            'utilization_pct', 'gap_hours', 'affected_periods',
            'resource_type', 'lead_time_weeks'
        ]]
        
        # Target : efficacité de l'action (résolu, partiel, échec)
        y = self.historical_actions['action_effectiveness']
        
        model = RandomForestClassifier(n_estimators=100, random_state=42)
        
        # Encoder les variables catégorielles
        X_encoded = pd.get_dummies(X, columns=['resource_type'])
        model.fit(X_encoded, y)
        
        return model
    
    def generate_recommendations(self, bottlenecks, load_profile, 
                                production_plan, constraints):
        """
        Génère des recommandations priorisées pour résoudre les goulots
        """
        recommendations = []
        
        for _, bottleneck in bottlenecks.iterrows():
            resource = bottleneck['resource']
            periods = bottleneck['periods']
            gap_hours = bottleneck['total_gap_hours']
            
            # Générer plusieurs options
            options = self._generate_balancing_options(
                resource, periods, gap_hours, load_profile, 
                production_plan, constraints
            )
            
            # Scorer chaque option
            for option in options:
                score = self._score_option(option, bottleneck)
                
                recommendations.append({
                    'resource': resource,
                    'action_type': option['type'],
                    'description': option['description'],
                    'estimated_capacity_gain': option['capacity_gain'],
                    'cost_estimate': option['cost'],
                    'implementation_time': option['lead_time'],
                    'feasibility_score': score,
                    'periods_affected': periods,
                    'details': option['details']
                })
        
        recommendations_df = pd.DataFrame(recommendations)
        if not recommendations_df.empty:
            recommendations_df = recommendations_df.sort_values(
                'feasibility_score', ascending=False
            )
        
        return recommendations_df
    
    def _generate_balancing_options(self, resource, periods, gap_hours,
                                   load_profile, production_plan, constraints):
        """
        Génère différentes options d'équilibrage
        """
        options = []
        
        # Option 1 : Overtime
        if constraints.get('allow_overtime', True):
            overtime_capacity = gap_hours
            overtime_cost = overtime_capacity * constraints.get('overtime_rate', 1.5)
            
            options.append({
                'type': 'overtime',
                'description': f'Ajouter {overtime_capacity:.0f} heures supplémentaires',
                'capacity_gain': overtime_capacity,
                'cost': overtime_cost,
                'lead_time': 0,
                'details': {'hours': overtime_capacity, 'periods': periods}
            })
        
        # Option 2 : Shift additionnel
        if constraints.get('allow_additional_shifts', True):
            shift_capacity = constraints.get('hours_per_shift', 160) * len(periods)
            shift_cost = shift_capacity * constraints.get('shift_cost_multiplier', 1.3)
            
            if shift_capacity >= gap_hours * 0.8:
                options.append({
                    'type': 'additional_shift',
                    'description': 'Ajouter un shift (équipe supplémentaire)',
                    'capacity_gain': shift_capacity,
                    'cost': shift_cost,
                    'lead_time': 4,  # semaines
                    'details': {'shifts': 1, 'periods': periods}
                })
        
        # Option 3 : Sous-traitance
        if constraints.get('allow_subcontracting', True):
            subcontract_capacity = gap_hours
            subcontract_cost = subcontract_capacity * constraints.get('subcontract_rate', 2.0)
            
            options.append({
                'type': 'subcontracting',
                'description': f'Sous-traiter {subcontract_capacity:.0f} heures',
                'capacity_gain': subcontract_capacity,
                'cost': subcontract_cost,
                'lead_time': 2,
                'details': {'hours': subcontract_capacity, 'periods': periods}
            })
        
        # Option 4 : Nivellement de charge (load leveling)
        leveling_options = self._analyze_load_leveling(
            resource, periods, load_profile, production_plan
        )
        if leveling_options:
            options.extend(leveling_options)
        
        # Option 5 : Investissement capacitaire
        if gap_hours > constraints.get('investment_threshold', 500):
            equipment_cost = constraints.get('equipment_investment', 100000)
            capacity_increase = constraints.get('equipment_capacity', 2000)
            
            options.append({
                'type': 'capital_investment',
                'description': 'Acquérir équipement/ligne supplémentaire',
                'capacity_gain': capacity_increase,
                'cost': equipment_cost,
                'lead_time': 12,
                'details': {'type': 'equipment', 'capacity': capacity_increase}
            })
        
        return options
    
    def _analyze_load_leveling(self, resource, overload_periods, 
                              load_profile, production_plan):
        """
        Analyse les opportunités de nivellement de charge
        """
        options = []
        
        # Identifier les périodes sous-utilisées
        resource_profile = load_profile[load_profile['resource'] == resource]
        underload_periods = resource_profile[
            resource_profile['utilization_pct'] < 70
        ]
        
        if len(underload_periods) > 0:
            # Calculer la capacité disponible
            available_capacity = (
                underload_periods['available_hours'] - 
                underload_periods['required_hours']
            ).sum()
            
            if available_capacity > 0:
                options.append({
                    'type': 'load_leveling',
                    'description': 'Déplacer production vers périodes creuses',
                    'capacity_gain': available_capacity,
                    'cost': available_capacity * 0.1,  # Coût de nivellement minimal
                    'lead_time': 0,
                    'details': {
                        'from_periods': overload_periods,
                        'to_periods': underload_periods['period'].tolist(),
                        'capacity_available': available_capacity
                    }
                })
        
        return options
    
    def _score_option(self, option, bottleneck):
        """
        Score une option basée sur efficacité, coût et faisabilité
        """
        # Efficacité : l'option résout-elle le problème ?
        coverage = min(option['capacity_gain'] / bottleneck['total_gap_hours'], 1.0)
        
        # Coût normalisé (inversé pour que moins cher = meilleur score)
        cost_normalized = 1 / (1 + option['cost'] / 10000)
        
        # Lead time (plus court = meilleur)
        time_score = 1 / (1 + option['lead_time'] / 12)
        
        # Score composite
        score = (coverage * 0.4) + (cost_normalized * 0.3) + (time_score * 0.3)
        
        return score * 100
```

### 4. Module de simulation de scénarios

**Test des stratégies d'équilibrage :**

```python
class CapacityScenarioSimulator:
    def __init__(self, load_calculator, bottleneck_detector):
        self.load_calculator = load_calculator
        self.bottleneck_detector = bottleneck_detector
    
    def simulate_scenario(self, production_plan, actions, time_horizon):
        """
        Simule l'impact des actions d'équilibrage
        """
        # Calcul de charge de base
        baseline_load = self.load_calculator.calculate_capacity_load(
            production_plan, time_horizon
        )
        
        # Appliquer les actions
        adjusted_load = self._apply_actions(baseline_load, actions)
        
        # Détection des goulots après actions
        remaining_bottlenecks = self.bottleneck_detector.identify_bottlenecks(
            adjusted_load
        )
        
        # Métriques de comparaison
        comparison = self._compare_scenarios(
            baseline_load, adjusted_load, remaining_bottlenecks
        )
        
        return {
            'baseline': baseline_load,
            'adjusted': adjusted_load,
            'remaining_bottlenecks': remaining_bottlenecks,
            'comparison': comparison,
            'actions_applied': actions
        }
    
    def _apply_actions(self, load_profile, actions):
        """
        Applique les actions au profil de charge
        """
        adjusted = load_profile.copy()
        
        for action in actions:
            resource = action['resource']
            action_type = action['action_type']
            details = action['details']
            
            if action_type == 'overtime':
                # Ajouter capacité overtime
                mask = (adjusted['resource'] == resource) & \
                       (adjusted['period'].isin(details['periods']))
                adjusted.loc[mask, 'available_hours'] += \
                    details['hours'] / len(details['periods'])
            
            elif action_type == 'additional_shift':
                # Ajouter un shift complet
                mask = (adjusted['resource'] == resource) & \
                       (adjusted['period'].isin(details['periods']))
                adjusted.loc[mask, 'available_hours'] *= 1.5  # Approximation
            
            elif action_type == 'subcontracting':
                # Réduire la charge requise
                mask = (adjusted['resource'] == resource) & \
                       (adjusted['period'].isin(details['periods']))
                adjusted.loc[mask, 'required_hours'] -= \
                    details['hours'] / len(details['periods'])
            
            elif action_type == 'load_leveling':
                # Déplacer charge entre périodes
                adjusted = self._apply_load_leveling(
                    adjusted, resource, details
                )
            
            elif action_type == 'capital_investment':
                # Augmentation permanente de capacité
                mask = adjusted['resource'] == resource
                adjusted.loc[mask, 'available_hours'] += details['capacity'] / 12
        
        # Recalculer utilisation et gap
        adjusted['utilization_pct'] = (
            adjusted['required_hours'] / adjusted['available_hours'] * 100
        )
        adjusted['gap_hours'] = (
            adjusted['required_hours'] - adjusted['available_hours']
        )
        
        return adjusted
    
    def _apply_load_leveling(self, load_profile, resource, details):
        """
        Applique le nivellement de charge entre périodes
        """
        adjusted = load_profile.copy()
        
        from_periods = details['from_periods']
        to_periods = details['to_periods']
        
        # Logique de déplacement simplifée
        # Dans un vrai système, cela nécessiterait une optimisation plus sophistiquée
        
        return adjusted
    
    def _compare_scenarios(self, baseline, adjusted, remaining_bottlenecks):
        """
        Compare les scénarios baseline et ajusté
        """
        baseline_overload = len(baseline[baseline['utilization_pct'] > 100])
        adjusted_overload = len(adjusted[adjusted['utilization_pct'] > 100])
        
        baseline_avg_util = baseline['utilization_pct'].mean()
        adjusted_avg_util = adjusted['utilization_pct'].mean()
        
        return {
            'overloaded_periods_before': baseline_overload,
            'overloaded_periods_after': adjusted_overload,
            'improvement_pct': (
                (baseline_overload - adjusted_overload) / baseline_overload * 100
                if baseline_overload > 0 else 0
            ),
            'avg_utilization_before': baseline_avg_util,
            'avg_utilization_after': adjusted_avg_util,
            'bottlenecks_resolved': len(baseline) - len(remaining_bottlenecks),
            'bottlenecks_remaining': len(remaining_bottlenecks)
        }
```

## Interface utilisateur et visualisations

### Tableau de bord RCCP

```python
import plotly.graph_objects as go
from plotly.subplots import make_subplots

class RCCPDashboard:
    def create_capacity_heatmap(self, load_profile):
        """
        Crée une heatmap d'utilisation de capacité
        """
        # Pivoter les données pour heatmap
        pivot_data = load_profile.pivot(
            index='resource', 
            columns='period', 
            values='utilization_pct'
        )
        
        fig = go.Figure(data=go.Heatmap(
            z=pivot_data.values,
            x=pivot_data.columns,
            y=pivot_data.index,
            colorscale=[
                [0, 'green'],
                [0.7, 'yellow'],
                [0.85, 'orange'],
                [1, 'red']
            ],
            colorbar=dict(title='Utilisation %'),
            text=pivot_data.values,
            texttemplate='%{text:.1f}%',
            textfont={"size": 10}
        ))
        
        fig.update_layout(
            title='Profil d\'utilisation de capacité par ressource',
            xaxis_title='Période',
            yaxis_title='Ressource',
            height=600
        )
        
        return fig
    
    def create_bottleneck_chart(self, bottlenecks):
        """
        Visualise les goulots d'étranglement
        """
        fig = go.Figure()
        
        fig.add_trace(go.Bar(
            x=bottlenecks['resource'],
            y=bottlenecks['total_gap_hours'],
            name='Heures de déficit',
            marker_color='red'
        ))
        
        fig.update_layout(
            title='Déficits de capacité par ressource',
            xaxis_title='Ressource',
            yaxis_title='Heures de déficit',
            showlegend=True,
            height=400
        )
        
        return fig
    
    def create_load_profile_chart(self, load_profile, resource):
        """
        Graphique de profil de charge pour une ressource
        """
        resource_data = load_profile[
            load_profile['resource'] == resource
        ].sort_values('period')
        
        fig = go.Figure()
        
        # Capacité disponible
        fig.add_trace(go.Scatter(
            x=resource_data['period'],
            y=resource_data['available_hours'],
            name='Capacité disponible',
            line=dict(color='green', dash='dash')
        ))
        
        # Charge requise
        fig.add_trace(go.Scatter(
            x=resource_data['period'],
            y=resource_data['required_hours'],
            name='Charge requise',
            line=dict(color='blue'),
            fill='tonexty'
        ))
        
        fig.update_layout(
            title=f'Profil de charge - {resource}',
            xaxis_title='Période',
            yaxis_title='Heures',
            height=400
        )
        
        return fig
    
    def create_scenario_comparison(self, comparison_data):
        """
        Compare les scénarios baseline vs ajusté
        """
        metrics = ['overloaded_periods', 'avg_utilization', 'bottlenecks']
        baseline_values = [
            comparison_data['overloaded_periods_before'],
            comparison_data['avg_utilization_before'],
            comparison_data['overloaded_periods_before']
        ]
        adjusted_values = [
            comparison_data['overloaded_periods_after'],
            comparison_data['avg_utilization_after'],
            comparison_data['bottlenecks_remaining']
        ]
        
        fig = go.Figure(data=[
            go.Bar(name='Baseline', x=metrics, y=baseline_values),
            go.Bar(name='Ajusté', x=metrics, y=adjusted_values)
        ])
        
        fig.update_layout(
            title='Comparaison de scénarios',
            barmode='group',
            height=400
        )
        
        return fig
```

## Architecture système complète

```python
class RCCPSystem:
    """
    Système RCCP intégré assisté par AI
    """
    def __init__(self, config):
        self.config = config
        
        # Initialiser les composants
        self.load_calculator = LoadCalculationEngine(
            config['resource_calendar'],
            config['routing_profiles']
        )
        
        self.bottleneck_detector = BottleneckDetector(
            utilization_threshold=config.get('utilization_threshold', 85)
        )
        
        self.recommender = AIBalancingRecommender(
            config.get('historical_actions', pd.DataFrame())
        )
        
        self.simulator = CapacityScenarioSimulator(
            self.load_calculator,
            self.bottleneck_detector
        )
        
        self.dashboard = RCCPDashboard()
    
    def run_rccp_analysis(self, production_plan, time_horizon, constraints):
        """
        Exécute l'analyse RCCP complète
        """
        print("Étape 1/5 : Calcul des besoins en capacité...")
        load_profile = self.load_calculator.calculate_capacity_load(
            production_plan, time_horizon
        )
        
        print("Étape 2/5 : Détection des goulots d'étranglement...")
        bottlenecks = self.bottleneck_detector.identify_bottlenecks(load_profile)
        
        print(f"Goulots identifiés : {len(bottlenecks)}")
        
        if len(bottlenecks) == 0:
            return {
                'status': 'FAISABLE',
                'load_profile': load_profile,
                'bottlenecks': bottlenecks,
                'recommendations': pd.DataFrame(),
                'message': 'Le plan de production est faisable avec la capacité actuelle'
            }
        
        print("Étape 3/5 : Génération de recommandations...")
        recommendations = self.recommender.generate_recommendations(
            bottlenecks, load_profile, production_plan, constraints
        )
        
        print(f"Recommandations générées : {len(recommendations)}")
        
        print("Étape 4/5 : Simulation de scénarios...")
        # Sélectionner les meilleures recommandations
        top_actions = recommendations.head(5).to_dict('records')
        
        scenario_result = self.simulator.simulate_scenario(
            production_plan, top_actions, time_horizon
        )
        
        print("Étape 5/5 : Génération des visualisations...")
        visualizations = {
            'heatmap': self.dashboard.create_capacity_heatmap(load_profile),
            'bottlenecks': self.dashboard.create_bottleneck_chart(bottlenecks),
            'scenario_comparison': self.dashboard.create_scenario_comparison(
                scenario_result['comparison']
            )
        }
        
        return {
            'status': 'CONTRAINTES' if len(bottlenecks) > 0 else 'FAISABLE',
            'load_profile': load_profile,
            'bottlenecks': bottlenecks,
            'recommendations': recommendations,
            'scenario_simulation': scenario_result,
            'visualizations': visualizations,
            'summary': self._generate_summary(bottlenecks, recommendations, 
                                             scenario_result)
        }
    
    def _generate_summary(self, bottlenecks, recommendations, scenario):
        """
        Génère un résumé exécutif
        """
        summary = {
            'total_bottlenecks': len(bottlenecks),
            'critical_resources': bottlenecks['resource'].tolist() if not bottlenecks.empty else [],
            'total_capacity_gap_hours': bottlenecks['total_gap_hours'].sum() if not bottlenecks.empty else 0,
            'top_recommendation': recommendations.iloc[0].to_dict() if not recommendations.empty else None,
            'estimated_improvement': scenario['comparison']['improvement_pct'],
            'action_required': len(bottlenecks) > 0
        }
        
        return summary
```

## Exemple d'utilisation

```python
# Configuration
config = {
    'resource_calendar': resource_calendar_obj,
    'routing_profiles': routing_data,
    'utilization_threshold': 85,
    'historical_actions': historical_df
}

# Initialiser le système
rccp_system = RCCPSystem(config)

# Plan de production (exemple)
production_plan = {
    '2024-01': {'ProductFamily_A': 1000, 'ProductFamily_B': 500},
    '2024-02': {'ProductFamily_A': 1200, 'ProductFamily_B': 600},
    '2024-03': {'ProductFamily_A': 1100, 'ProductFamily_B': 550},
    # ... autres périodes
}

time_horizon = ['2024-01', '2024-02', '2024-03', '2024-04', '2024-05', '2024-06']

constraints = {
    'allow_overtime': True,
    'allow_additional_shifts': True,
    'allow_subcontracting': True,
    'overtime_rate': 1.5,
    'shift_cost_multiplier': 1.3,
    'subcontract_rate': 2.0,
    'investment_threshold': 1000,
    'equipment_investment': 150000,
    'equipment_capacity': 2000,
    'hours_per_shift': 160
}

# Exécuter l'analyse
results = rccp_system.run_rccp_analysis(
    production_plan, 
    time_horizon, 
    constraints
)

# Afficher les résultats
print(f"\nStatut : {results['status']}")
print(f"Goulots identifiés : {results['summary']['total_bottlenecks']}")
print(f"Déficit total : {results['summary']['total_capacity_gap_hours']:.0f} heures")

if results['summary']['top_recommendation']:
    top_rec = results['summary']['top_recommendation']
    print(f"\nRecommandation principale :")
    print(f"  - Action : {top_rec['description']}")
    print(f"  - Gain de capacité : {top_rec['estimated_capacity_gain']:.0f} heures")
    print(f"  - Coût estimé : {top_rec['cost_estimate']:.0f} €")
    print(f"  - Délai : {top_rec['implementation_time']} semaines")

# Visualisations
results['visualizations']['heatmap'].show()
results['visualizations']['bottlenecks'].show()
```

## Intégration avec l'ERP et systèmes existants

```python
class ERPIntegration:
    """
    Module d'intégration avec systèmes ERP
    """
    def __init__(self, erp_connector):
        self.erp = erp_connector
    
    def fetch_production_plan(self, planning_version, time_horizon):
        """
        Récupère le plan de production depuis l'ERP
        """
        query = f"""
        SELECT 
            period,
            product_family,
            planned_quantity
        FROM production_plan
        WHERE version = '{planning_version}'
        AND period BETWEEN '{time_horizon[0]}' AND '{time_horizon[-1]}'
        """
        
        df = self.erp.execute_query(query)
        
        # Transformer en structure attendue
        plan = {}
        for _, row in df.iterrows():
            period = row['period']
            if period not in plan:
                plan[period] = {}
            plan[period][row['product_family']] = row['planned_quantity']
        
        return plan
    
    def fetch_resource_calendar(self, time_horizon):
        """
        Récupère les calendriers de ressources
        """
        query = f"""
        SELECT 
            resource_id,
            resource_group,
            period,
            available_hours,
            efficiency_factor
        FROM resource_calendar
        WHERE period BETWEEN '{time_horizon[0]}' AND '{time_horizon[-1]}'
        """
        
        return self.erp.execute_query(query)
    
    def fetch_routing_data(self):
        """
        Récupère les données de routage
        """
        query = """
        SELECT 
            product_family,
            operation_seq,
            resource_group,
            standard_hours_per_unit,
            setup_hours
        FROM routing_master
        WHERE active = 1
        """
        
        return self.erp.execute_query(query)
    
    def publish_rccp_results(self, results, version):
        """
        Publie les résultats RCCP dans l'ERP
        """
        # Publier le profil de charge
        self._publish_load_profile(results['load_profile'], version)
        
        # Publier les goulots
        self._publish_bottlenecks(results['bottlenecks'], version)
        
        # Publier les recommandations
        self._publish_recommendations(results['recommendations'], version)
    
    def _publish_load_profile(self, load_profile, version):
        load_profile['version'] = version
        load_profile['analysis_date'] = pd.Timestamp.now()
        
        self.erp.bulk_insert('rccp_load_profile', load_profile)
    
    def _publish_bottlenecks(self, bottlenecks, version):
        if not bottlenecks.empty:
            bottlenecks['version'] = version
            bottlenecks['analysis_date'] = pd.Timestamp.now()
            
            self.erp.bulk_insert('rccp_bottlenecks', bottlenecks)
    
    def _publish_recommendations(self, recommendations, version):
        if not recommendations.empty:
            recommendations['version'] = version
            recommendations['analysis_date'] = pd.Timestamp.now()
            
            self.erp.bulk_insert('rccp_recommendations', recommendations)
```

## Fonctionnalités avancées

### 1. Apprentissage continu

```python
class ContinuousLearning:
    """
    Module d'apprentissage continu à partir des actions passées
    """
    def __init__(self, feedback_database):
        self.feedback_db = feedback_database
    
    def record_action_outcome(self, action_id, outcome_metrics):
        """
        Enregistre l'issue d'une action recommandée
        """
        self.feedback_db.insert({
            'action_id': action_id,
            'outcome': outcome_metrics['status'],  # 'success', 'partial', 'failed'
            'actual_capacity_gain': outcome_metrics['capacity_gain'],
            'actual_cost': outcome_metrics['cost'],
            'actual_implementation_time': outcome_metrics['implementation_days'],
            'timestamp': pd.Timestamp.now()
        })
    
    def retrain_model(self, recommender):
        """
        Réentraîne le modèle avec les nouvelles données
        """
        updated_data = self.feedback_db.fetch_all()
        recommender.historical_actions = updated_data
        recommender.model = recommender._train_action_model()
```

### 2. Alertes proactives

```python
class ProactiveAlerts:
    """
    Système d'alertes pour problèmes de capacité émergents
    """
    def monitor_capacity_trends(self, load_profile, historical_data):
        """
        Surveille les tendances et déclenche des alertes
        """
        alerts = []
        
        for resource in load_profile['resource'].unique():
            resource_trend = self._analyze_trend(resource, historical_data)
            
            if resource_trend['direction'] == 'increasing' and \
               resource_trend['projected_utilization'] > 90:
                alerts.append({
                    'severity': 'HIGH',
                    'resource': resource,
                    'message': f'Tendance croissante détectée. Utilisation projetée : {resource_trend["projected_utilization"]:.1f}%',
                    'recommended_action': 'Planifier augmentation de capacité'
                })
        
        return alerts
    
    def _analyze_trend(self, resource, historical_data):
        # Analyse de tendance simplifiée
        resource_history = historical_data[historical_data['resource'] == resource]
        
        # Régression linéaire simple
        from scipy import stats
        x = range(len(resource_history))
        y = resource_history['utilization_pct'].values
        
        slope, intercept, r_value, p_value, std_err = stats.linregress(x, y)
        
        projected = slope * (len(x) + 3) + intercept  # Projection 3 périodes
        
        return {
            'direction': 'increasing' if slope > 0 else 'decreasing',
            'slope': slope,
            'projected_utilization': projected,
            'confidence': r_value ** 2
        }
```

## Points clés d'implémentation

1. **Granularité appropriée** : Équilibrer entre détail (SKU-ressource) et agrégation (famille-workcentre)

2. **Fenêtre temporelle** : 3-18 mois typique pour RCCP, avec buckets hebdomadaires ou mensuels

3. **Données de qualité** : Temps standards maintenus, calendriers à jour, routages précis

4. **Flexibilité des contraintes** : Permettre ajustement des règles métier par utilisateur

5. **Performance** : Optimiser les calculs pour grands volumes (parallélisation, caching)

6. **Boucle de feedback** : Capturer résultats réels pour amélioration continue du modèle

7. **Intégration S&OP** : Synchroniser avec cycle S&OP mensuel et mise à jour IBP

Cet outil RCCP assisté par AI transforme un processus manuel et chronophage en analyse automatisée avec recommandations actionnables, permettant aux planificateurs de se concentrer sur les décisions stratégiques plutôt que sur les calculs.

Construire un outil RCCP assisté par IA pour l'équilibrage de capacité à moyen terme

Ce tutoriel explique comment construire une application RCCP légère assistée par IA pour l'équilibrage de capacité à moyen terme en utilisant Python, pandas, Streamlit et une couche d'explication IA. Il couvre la préparation des données, les calculs de capacité déterministes, la gestion de scénarios, la visualisation, l'interprétation assistée par IA et les pièges courants de mise en œuvre. Le tutoriel souligne que l'IA doit expliquer les résultats de planification plutôt que remplacer la logique de planification déterministe.