diff --git a/.vscode/settings.json b/.vscode/settings.json index e2ce41d..0967ef4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,152 +1 @@ -{ - "editor.tabSize": 4, - "editor.rulers": [ - 120 - ], - "editor.renderWhitespace": "trailing", - "editor.suggestSelection": "first", - "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.stickyScroll.enabled": false, - "editor.bracketPairColorization.enabled": false, - "editor.cursorSmoothCaretAnimation": "on", - "editor.suggest.preview": true, - "terminal.integrated.defaultProfile.windows": "Command Prompt", - "debug.onTaskErrors": "debugAnyway", - "explorer.compactFolders": false, - "explorer.confirmDragAndDrop": false, - "explorer.confirmDelete": false, - "explorer.copyRelativePathSeparator": "/", - "files.autoSave": "onFocusChange", - "files.exclude": { - "node_modules/**/*": true, - "**/.classpath": true, - "**/.project": true, - "**/.settings": true, - "**/.factorypath": true - }, - "files.associations": { - "*.pyx": "cython", - ".clang*": "yaml", - "*.gpj": "jsonc", - "*.gvw": "jsonc", - "*.hpp.in": "cpp" - }, - "files.insertFinalNewline": true, - "files.trimFinalNewlines": true, - "files.trimTrailingWhitespace": true, - "workbench.startupEditor": "none", - "workbench.editorAssociations": { - "*.ipynb": "jupyter-notebook", - "*.md": "vscode.markdown.preview.editor", - "*.svg": "svgPreviewer.customEditor" - }, - "workbench.colorTheme": "Dracula Theme Soft", - "git.enableSmartCommit": true, - "git.autofetch": true, - "git.confirmSync": false, - "git.openRepositoryInParentFolders": "always", - "partialDiff.enableTelemetry": false, - "prettier.tabWidth": 4, - "prettier.singleQuote": true, - "prettier.jsxSingleQuote": true, - "prettier.trailingComma": "all", - "prettier.useEditorConfig": true, - "prettier.bracketSpacing": false, - "markdown.validate.enabled": true, - "[markdown]": { - "files.trimTrailingWhitespace": false, - "editor.formatOnSave": false, - "editor.defaultFormatter": "yzhang.markdown-all-in-one", - "editor.wordWrap": "wordWrapColumn", - "editor.wordWrapColumn": 80 - }, - "[yaml]": { - "editor.formatOnSave": false, - "editor.defaultFormatter": "redhat.vscode-yaml", - "editor.wordWrap": "wordWrapColumn", - "editor.wordWrapColumn": 80 - }, - "[json]": { - "editor.formatOnSave": false, - "editor.defaultFormatter": "vscode.json-language-features" - }, - "[jsonc]": { - "editor.formatOnSave": false - }, - "[plaintext]": { - "editor.wordWrap": "wordWrapColumn", - "editor.wordWrapColumn": 120 - }, - "[toml]": { - "editor.wordWrap": "wordWrapColumn", - "editor.wordWrapColumn": 80, - "editor.defaultFormatter": "tamasfe.even-better-toml", - "editor.formatOnSave": true - }, - "better-comments.tags": [ - { - "tag": "XXX", - "color": "#F8C471" - }, - { - "tag": "WARN", - "color": "#FF6961" - }, - { - "tag": "NOTE", - "color": "#3498DB" - }, - { - "tag": "TODO", - "color": "#77C3EC" - } - ], - "vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue", - "codesnap.showWindowControls": false, - "codesnap.shutterAction": "copy", - "Workspace_Formatter.excludePattern": [ - "**/build", - "**/.*", - "**/.vscode", - "**/html" - ], - "svg.preview.autoOpen": true, - "remote.WSL.fileWatcher.polling": true, - "errorLens.delay": 1000, - "errorLens.enabledDiagnosticLevels": [ - "error", - "warning" - ], - "errorLens.enabled": false, - "[python]": { - "editor.formatOnSave": false, - "editor.defaultFormatter": "mikoz.black-py", - "editor.formatOnType": false - }, - "python.languageServer": "Jedi", - "python.analysis.addImport.exactMatchOnly": true, - "python.analysis.autoImportCompletions": false, - "python.analysis.completeFunctionParens": false, - "python.analysis.autoFormatStrings": true, - "python.analysis.logLevel": "Error", - "python.createEnvironment.contentButton": "show", - "python.missingPackage.severity": "Error", - "mypy-type-checker.importStrategy": "fromEnvironment", - "black-formatter.importStrategy": "fromEnvironment", - "isort.check": true, - "isort.importStrategy": "fromEnvironment", - "ruff.organizeImports": false, - "ruff.fixAll": false, - "autoDocstring.generateDocstringOnEnter": true, - "autoDocstring.quoteStyle": "'''", - "jupyter.interactiveWindow.creationMode": "perFile", - "jupyter.askForKernelRestart": false, - "jupyter.themeMatplotlibPlots": true, - "jupyter.logging.level": "error", - "notebook.formatOnSave.enabled": false, - "notebook.output.textLineLimit": 20, - "notebook.compactView": false, - "notebook.diff.ignoreMetadata": true, - "notebook.diff.ignoreOutputs": true -} +{} diff --git a/src/calculation_target_reliability/router.py b/src/calculation_target_reliability/router.py index 8be4da5..ba5822a 100644 --- a/src/calculation_target_reliability/router.py +++ b/src/calculation_target_reliability/router.py @@ -3,12 +3,12 @@ from typing import Dict, List, Optional from fastapi import APIRouter, HTTPException, status from fastapi.params import Query -from src.database.core import DbSession +from src.database.core import DbSession, CollectorDbSession from src.auth.service import Token from src.models import StandardResponse -from .service import get_eaf_timeline, run_rbd_simulation, get_simulation_results - +from .service import run_rbd_simulation, get_simulation_results, identify_worst_eaf_contributors +from .schema import OptimizationResult router = APIRouter() @@ -29,13 +29,14 @@ router = APIRouter() # ) -@router.get("", response_model=StandardResponse[dict]) +@router.get("", response_model=StandardResponse[OptimizationResult]) async def get_target_reliability( db_session: DbSession, token: Token, + collector_db: CollectorDbSession, oh_session_id: Optional[str] = Query(None), - eaf_input: float = Query(0.5), - duration: int = Query(8000), + eaf_input: float = Query(99.8), + duration: int = Query(8760), ): """Get all scope pagination.""" if not oh_session_id: @@ -57,12 +58,20 @@ async def get_target_reliability( ) results = await get_simulation_results( - simulation_id=simulation_id['data'], + simulation_id=simulation_id["data"], token=token ) + optimize_result = await identify_worst_eaf_contributors( + simulation_result=results, + target_eaf=eaf_input, + db_session=db_session, + oh_session_id=oh_session_id, + collector_db=collector_db + ) + return StandardResponse( - data=results, + data=optimize_result, message="Data retrieved successfully", ) diff --git a/src/calculation_target_reliability/schema.py b/src/calculation_target_reliability/schema.py index bb90d76..525dbd0 100644 --- a/src/calculation_target_reliability/schema.py +++ b/src/calculation_target_reliability/schema.py @@ -31,6 +31,31 @@ class OverhaulRead(OverhaulBase): schedules: List[Dict[str, Any]] systemComponents: Dict[str, Any] +class AssetWeight(OverhaulBase): + node: dict + capacity_weight: float + eaf_impact: float + eaf: float + num_of_failures: int + downtime_hours: float + plot_data: dict + +class MaintenanceScenario(OverhaulBase): + location_tag: str + current_eaf: float + projected_eaf_improvement: float + priority_score: float + plant_level_benefit: float = 0.0 + capacity_weight : float + + +class OptimizationResult(OverhaulBase): + current_plant_eaf: float + target_plant_eaf: float + eaf_gap: float + asset_contributions: List[AssetWeight] + optimization_success: bool = False + # { # "overview": { diff --git a/src/calculation_target_reliability/service.py b/src/calculation_target_reliability/service.py index dd935a0..47734b0 100644 --- a/src/calculation_target_reliability/service.py +++ b/src/calculation_target_reliability/service.py @@ -1,27 +1,25 @@ -from typing import Optional - +from typing import Optional, List +from dataclasses import dataclass from sqlalchemy import Delete, Select import httpx from src.auth.service import CurrentUser -from src.database.core import DbSession -# from src.scope_equipment.model import ScopeEquipment -# from src.scope_equipment.service import get_by_scope_name -# from src.scope_equipment_job.service import get_equipment_level_by_no +from src.database.core import DbSession, CollectorDbSession from datetime import datetime, timedelta import random -from typing import List from .utils import generate_down_periods from src.overhaul_scope.service import get as get_overhaul from bisect import bisect_left from collections import defaultdict - import asyncio +from .schema import AssetWeight,MaintenanceScenario,OptimizationResult + +from src.overhaul_activity.service import get_standard_scope_by_session_id RBD_SERVICE_API = "http://192.168.1.82:8000/rbd" client = httpx.AsyncClient(timeout=300.0) -# return results + async def run_rbd_simulation(*, sim_hours: int, token): sim_data = { "SimulationName": "Simulation OH Reliability Target", @@ -51,129 +49,248 @@ async def get_simulation_results(*, simulation_id: str, token: str): calc_result_url = f"{RBD_SERVICE_API}/aeros/simulation/result/calc/{simulation_id}?nodetype=RegularNode" plot_result_url = f"{RBD_SERVICE_API}/aeros/simulation/result/plot/{simulation_id}?nodetype=RegularNode" + calc_plant_result = f"{RBD_SERVICE_API}/aeros/simulation/result/calc/{simulation_id}/plant" async with httpx.AsyncClient(timeout=300.0) as client: calc_task = client.get(calc_result_url, headers=headers) plot_task = client.get(plot_result_url, headers=headers) + plant_task = client.get(calc_plant_result, headers=headers) - # Run both requests concurrently - calc_response, plot_response = await asyncio.gather(calc_task, plot_task) + # Run all three requests concurrently + calc_response, plot_response, plant_response = await asyncio.gather(calc_task, plot_task, plant_task) calc_response.raise_for_status() plot_response.raise_for_status() + plant_response.raise_for_status() calc_data = calc_response.json()["data"] - plot_data = plot_response.json()["data"] + plot_data = plot_response.json()["data"]["plot_results"] + plant_data = plant_response.json()["data"] return { "calc_result": calc_data, - "plot_result": plot_data + "plot_result": plot_data, + "plant_result": plant_data } -async def get_eaf_timeline(*, db_session, eaf_input: float, oh_session_id: str, oh_duration = 8000) -> List[dict]: +def calculate_asset_weights(plant_result, eq_results): """ - Generate a timeline of EAF values based on input parameters. - Optimized version with reduced time complexity. + Calculate each asset's contribution weight to plant EAF + Based on production capacity and criticality + """ + plant_ideal_production = plant_result.get('ideal_production', 0) + results = [] - Args: - eaf_input (float): EAF value to check against thresholds - oh_session_id (str): OH session identifier - oh_duration (int): Duration in hours + for asset in eq_results: + # Weight based on production capacity + capacity_weight = asset.get('ideal_production', 0) / plant_ideal_production if plant_ideal_production > 0 else 0 - Returns: - List[dict]: List of dictionaries containing dates and their EAF values - """ - MIN_EAF = 30 - MAX_EAF = 80 - - oh_session = await get_overhaul(db_session=db_session, overhaul_session_id=oh_session_id) - oh_session_start = datetime.fromisoformat(oh_session.start_date.isoformat()) - - # Determine date range - if MIN_EAF <= eaf_input <= MAX_EAF: - end_date = oh_session_start + timedelta(hours=oh_duration) - elif eaf_input < MIN_EAF: - end_date = oh_session_start + timedelta(hours=oh_duration, days=360) - else: # eaf_input > MAX_EAF - end_date = oh_session_start + timedelta(hours=oh_duration) - timedelta(days=180) - - # Default EAF values when system is up - default_values = { - 'eaf1_value': 1.0, - } + # Get asset EAF + asset_eaf = asset.get('eaf', 0) - # EAF values during downtime - now using three states - downtime_values = { - 'eaf1_warning': 0.6, - 'eaf1_down': 0.0, - } + # Calculate EAF impact (how much this asset affects plant EAF) + eaf_impact = (100 - asset_eaf) * capacity_weight - # Generate down periods for all EAF scenarios at once - all_down_periods = {} - for eaf_key in ['eaf1']: - # Generate warning periods (0.6) - warning_periods = generate_down_periods( - oh_session_start, - end_date, - 3, # Less frequent warnings - min_duration=24, - max_duration=48 + asset_weight = AssetWeight( + node=asset.get('aeros_node'), + capacity_weight=capacity_weight, + eaf_impact=eaf_impact, + eaf=asset_eaf, + num_of_failures=asset.get('num_events', 0), + ideal_production=asset.get('ideal_production', 0) ) + results.append(asset_weight) - # Generate full downtime periods (0.0) - down_periods = generate_down_periods( - oh_session_start, - end_date, - 2, # Less frequent full downtimes - min_duration=36, - max_duration=72 - ) + return results - # Store both types of periods with their state - all_down_periods[f'{eaf_key}_warning'] = [(start, end, 'warning') for start, end in warning_periods] - all_down_periods[f'{eaf_key}_down'] = [(start, end, 'down') for start, end in down_periods] +def calculate_asset_eaf_contributions(plant_result, eq_results, plot_result): + """ + Calculate each asset's negative contribution to plant EAF + Higher contribution = more impact on reducing plant EAF + """ + plant_ideal_production = plant_result.get('ideal_production', 0) + results = [] - # Create a list of all state change times - state_changes = defaultdict(dict) - # Process both warning and down periods - for eaf_type, periods in all_down_periods.items(): - eaf_key = eaf_type.split('_')[0] # Extract base key (eaf1) - state_type = eaf_type.split('_')[1] # Extract state type (warning/down) + for asset in eq_results: + # Weight based on production capacity + capacity_weight = asset.get('ideal_production', 0) / plant_ideal_production if plant_ideal_production > 0 else 0 + plot_data = next((item for item in plot_result if item['aeros_node']['node_name'] == asset['aeros_node']['node_name']), None) - for start, end, state in periods: - # Record state changes at period boundaries - if state == 'warning': - state_changes[start][f'{eaf_key}_value'] = downtime_values[f'{eaf_key}_warning'] - else: # state == 'down' - state_changes[start][f'{eaf_key}_value'] = downtime_values[f'{eaf_key}_down'] + # Get asset EAF and downtime + asset_eaf = asset.get('eaf', 0) + asset_derating_pct = 100 - asset_eaf - # Reset to normal at the end of period - state_changes[end + timedelta(hours=1)][f'{eaf_key}_value'] = default_values[f'{eaf_key}_value'] + # Calculate this asset's contribution to plant EAF reduction + # This is how much this asset alone reduces the overall plant EAF + eaf_contribution = asset_derating_pct * capacity_weight - # Convert state_changes to sorted list of times - change_times = sorted(state_changes.keys()) + # Calculate actual downtime hours (if simulation hours available) + sim_duration = plant_result.get('sim_duration', 8760) # Default to 1 year + downtime_hours = (asset_derating_pct / 100) * sim_duration - results = [] - current_values = default_values.copy() + contribution = AssetWeight( + node=asset.get('aeros_node'), + eaf=asset_eaf, + capacity_weight=capacity_weight, + eaf_impact=eaf_contribution, + downtime_hours=downtime_hours, + num_of_failures=asset.get('num_events', 0), + plot_data=plot_data + ) + results.append(contribution) - # Process changes between state change points - current_time = oh_session_start - idx = 0 + # Sort by contribution (worst contributors first) + results.sort(key=lambda x: x.eaf_impact, reverse=True) + return results - while current_time <= end_date: - # Update values if we've hit a state change point - while idx < len(change_times) and current_time >= change_times[idx]: - changes = state_changes[change_times[idx]] - for key, value in changes.items(): - current_values[key] = value - idx += 1 +def project_eaf_improvement(asset: AssetWeight, improvement_factor: float = 0.3) -> float: + """ + Project EAF improvement after maintenance + This is a simplified model - you should replace with your actual prediction logic + """ + current_downtime_pct = 100 - asset.eaf + # Assume maintenance reduces downtime by improvement_factor + improved_downtime_pct = current_downtime_pct * (1 - improvement_factor) + projected_eaf = 100 - improved_downtime_pct + return min(projected_eaf, 99.9) # Cap at 99.9% - results.append({ - 'date': current_time.strftime('%Y-%m-%d %H:00:00'), - **current_values - }) +async def identify_worst_eaf_contributors(*, simulation_result, target_eaf: float, db_session: DbSession, oh_session_id: str, collector_db: CollectorDbSession): + """ + Identify equipment that contributes most to plant EAF reduction - current_time += timedelta(hours=1) + Args: + simulation_result: Dictionary containing calc_result and plant_result + target_eaf: Target plant EAF percentage (for gap calculation) - return results + Returns: + OptimizationResult with asset contributions ranked by impact + """ + # Calculate current plant EAF and asset contributions + calc_result = simulation_result['calc_result'] + plant_result = simulation_result['plant_result'] + plot_result = simulation_result['plot_result'] + + # Get equipment results from calc_result + eq_results = calc_result if isinstance(calc_result, list) else [calc_result] + + asset_contributions = calculate_asset_eaf_contributions(plant_result, eq_results, plot_result) + current_plant_eaf = plant_result.get("eaf", 0) + eaf_gap = target_eaf - current_plant_eaf + + # Verify our calculation by summing contributions + total_calculated_downtime = sum(contrib.eaf_impact for contrib in asset_contributions) + calculated_plant_eaf = 100 - total_calculated_downtime + + standard_scope = await get_standard_scope_by_session_id( + db_session=db_session, + overhaul_session_id=oh_session_id, + collector_db=collector_db + ) + + standard_scope_location_tags = [tag.location_tag for tag in standard_scope] + + # Select only standard Scope(array) + selected_asset = [asset for asset in asset_contributions if asset.node['node_name'] in standard_scope_location_tags] + + print(f"Plant EAF from API: {current_plant_eaf:.2f}%") + print(f"Plant EAF calculated: {calculated_plant_eaf:.2f}%") + print(f"Target EAF: {target_eaf:.2f}%") + print(f"EAF Gap: {eaf_gap:.2f}%") + + optimization_success = current_plant_eaf >= target_eaf + + return OptimizationResult( + current_plant_eaf=current_plant_eaf, + target_plant_eaf=target_eaf, + eaf_gap=eaf_gap, + asset_contributions=selected_asset, + optimization_success=optimization_success + ) + +# def optimize_maintenance_priority(*, simulation_result, target_eaf: float): +# """ +# Optimize maintenance priorities to achieve target plant EAF + +# Args: +# simulation_result: Dictionary containing calc_result and plant_result +# target_eaf: Target plant EAF percentage + +# Returns: +# OptimizationResult with recommendations +# """ +# # Calculate current plant EAF and asset weights +# calc_result = simulation_result['calc_result'] +# plant_result = simulation_result['plant_result'] + +# # Get equipment results from calc_result +# eq_results = calc_result if isinstance(calc_result, list) else [calc_result] + +# equipment_eaf_weights = calculate_asset_weights(plant_result, eq_results) +# current_plant_eaf = plant_result.get("eaf", 0) + +# if current_plant_eaf >= target_eaf: +# return OptimizationResult( +# current_plant_eaf=current_plant_eaf, +# target_plant_eaf=target_eaf, +# eaf_gap=0.0, +# recommended_assets=[], +# projected_plant_eaf=current_plant_eaf, +# optimization_success=True +# ) + +# # Create maintenance scenarios for all assets +# scenarios = [] +# for asset in equipment_eaf_weights: +# if asset.eaf < 99.9: # Only consider assets with improvement potential +# projected_eaf = project_eaf_improvement(asset) +# eaf_improvement = projected_eaf - asset.eaf + +# # Calculate plant-level benefit (how much this asset improvement affects plant EAF) +# plant_benefit = eaf_improvement * asset.capacity_weight + +# # Priority score combines multiple factors +# priority_score = ( +# plant_benefit * 0.8 + # Plant impact (50%) +# (100 - asset.eaf) * asset.capacity_weight * 0.2 # Current performance gap (30%) +# # asset.num_of_failures * asset.capacity_weight * 0.1 # Failure frequency (20%) +# ) + +# scenario = MaintenanceScenario( +# location_tag=asset.node['node_name'], # Placeholder - replace with actual name +# current_eaf=asset.eaf, +# projected_eaf_improvement=eaf_improvement, +# priority_score=priority_score, +# plant_level_benefit=plant_benefit, +# capacity_weight=asset.capacity_weight +# ) +# scenarios.append(scenario) + +# # Sort by priority score (highest first) +# scenarios.sort(key=lambda x: x.priority_score, reverse=True) + +# # Select assets for maintenance to achieve target EAF +# selected_scenarios = [] +# cumulative_benefit = 0.0 + +# eaf_gap = target_eaf - current_plant_eaf + +# for scenario in scenarios: +# if cumulative_benefit < eaf_gap: +# selected_scenarios.append(scenario) +# cumulative_benefit += scenario.plant_level_benefit + +# if cumulative_benefit >= eaf_gap: +# break + +# projected_plant_eaf = current_plant_eaf + cumulative_benefit +# optimization_success = projected_plant_eaf >= target_eaf + +# return OptimizationResult( +# current_plant_eaf=current_plant_eaf, +# target_plant_eaf=target_eaf, +# eaf_gap=eaf_gap, +# recommended_assets=selected_scenarios, +# projected_plant_eaf=projected_plant_eaf, +# optimization_success=optimization_success +# )