From b57049d2f286bb025220b45219a6bb9a12112c58 Mon Sep 17 00:00:00 2001 From: SevenOfNinePE Date: Wed, 12 Mar 2025 09:14:16 +0100 Subject: [PATCH 1/8] Store before merge with main. --- dct/dctmainctl.py | 60 ++++- dct/htmltemplates/StyleSheets/Dummytrafo.png | Bin 0 -> 108481 bytes .../StyleSheets/histogramStyle.css | 23 ++ dct/server_ctl.py | 102 +++++++ dct/summary_eval.py | 250 ++++++++++++++++++ workspace/DabHeatsinkConf.toml | 22 +- 6 files changed, 448 insertions(+), 9 deletions(-) create mode 100644 dct/htmltemplates/StyleSheets/Dummytrafo.png create mode 100644 dct/htmltemplates/StyleSheets/histogramStyle.css create mode 100644 dct/server_ctl.py create mode 100644 dct/summary_eval.py diff --git a/dct/dctmainctl.py b/dct/dctmainctl.py index 5b0aae2..0daee98 100644 --- a/dct/dctmainctl.py +++ b/dct/dctmainctl.py @@ -2,6 +2,18 @@ # python libraries import os import sys +import base64 +import multiprocessing +import random +import time +import io +import matplotlib.pyplot as plt +import uvicorn +from fastapi import FastAPI, Request +from fastapi.templating import Jinja2Templates +from fastapi.responses import HTMLResponse, JSONResponse, Response +from fastapi.staticfiles import StaticFiles +import threading # 3rd party libraries import toml @@ -12,10 +24,13 @@ import circuit_sim as Elecsimclass # Inductor simulations class import induct_sim as Inductsimclass -# import transf_sim +# Import transf_sim import transf_sim as Transfsimclass -# import heatsink_sim +# Import heatsink_sim import heatsink_sim as Heatsinksimclass +# Import server control class +import server_ctl as Serverctlclass + # logging.basicConfig(format='%(levelname)s,%(asctime)s:%(message)s', encoding='utf-8') # logging.getLogger('pygeckocircuits2').setLevel(logging.DEBUG) @@ -351,6 +366,35 @@ def check_breakpoint(breakpointkey: str, info: str): else: pass + @staticmethod + def start_dct_server(req_stop_server,stop_flag): + """Starts the server to control and supervice simulation. + + :param req_stop_server: Shared memory flag to request server to stop + :type req_stop_server: multiprocessing.Value + :param stop_flag: Shared memory flag which indicates that the server stops the measurment + :type stop_flag: multiprocessing.Value + """ + # Mounten des Stylesheetpfades + app.mount("/StyleSheets", StaticFiles(directory="htmltemplates/StyleSheets"), name="Stylesheets") + + # Start the server process + server_process = multiprocessing.Process(target=srv_ctl.run_server, args=(req_stop_server, stop_flag)) + server_process.start(); + + @staticmethod + def stop_dct_server(req_stop_server): + """Stop the server for the control and supervisuib of the simulation. + + :param req_stop_server: Shared memory flag to request server to stop + :type req_stop_server: multiprocessing.Value + """ + + # Request server to stop + req_stop_server.value = 1 + # Wait for joined server process + server_process.join(5) + @staticmethod def executeProgram(workspace_path: str): """Perform the main programm. @@ -380,6 +424,13 @@ def executeProgram(workspace_path: str): hsim = Heatsinksimclass.Heatsinksim # Flag for available filtered results filtered_resultFlag = False + # Server class to control the workflow + srv_ctl = Serverctlclass + # Shared Memory für das Histogramm und den Status + # histogram_data = multiprocessing.Array('i', [0] * 25) + req_stop_server = multiprocessing.Value('i', 0) + stop_flag = multiprocessing.Value('i', 0) + # Check if workspace path is not provided by argument if workspace_path == "": @@ -493,6 +544,8 @@ def executeProgram(workspace_path: str): # Warning, no data are available # Check, if heatsink optimization is to skip # Warning, no data are available + # -- Start server -------------------------------------------------------------------------------------------- + DctMainCtl.start_dct_server(req_stop_server,stop_flag) # -- Start simulation ---------------------------------------------------------------------------------------- @@ -599,7 +652,8 @@ def executeProgram(workspace_path: str): # Join process if necessary esim.join_process() - + # Shut down server + DctMainCtl.stop_dct_server() pass diff --git a/dct/htmltemplates/StyleSheets/Dummytrafo.png b/dct/htmltemplates/StyleSheets/Dummytrafo.png new file mode 100644 index 0000000000000000000000000000000000000000..c10de0f95d6bb1c0b661e41486fb1c1f5cd68e15 GIT binary patch literal 108481 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ʹo9UEY0WStUUR- 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 Date: Fri, 14 Mar 2025 15:37:22 +0100 Subject: [PATCH 2/8] Add summary workflow (almost complete). Update server for better start and stop. --- dct/dctmainctl.py | 123 +++++------ dct/heatsink_sim.py | 98 ++++----- dct/server_ctl.py | 142 +++++++++--- dct/summary_eval.py | 250 --------------------- dct/summary_processing.py | 384 +++++++++++++++++++++++++++++++++ workspace/DabHeatsinkConf.toml | 11 +- workspace/DabInductorConf.toml | 18 +- workspace/progFlow.toml | 11 +- 8 files changed, 620 insertions(+), 417 deletions(-) delete mode 100644 dct/summary_eval.py create mode 100644 dct/summary_processing.py diff --git a/dct/dctmainctl.py b/dct/dctmainctl.py index 293f400..49bb600 100644 --- a/dct/dctmainctl.py +++ b/dct/dctmainctl.py @@ -2,18 +2,7 @@ # python libraries import os import sys -import base64 import multiprocessing -import random -import time -import io -import matplotlib.pyplot as plt -import uvicorn -from fastapi import FastAPI, Request -from fastapi.templating import Jinja2Templates -from fastapi.responses import HTMLResponse, JSONResponse, Response -from fastapi.staticfiles import StaticFiles -import threading # 3rd party libraries import toml @@ -27,10 +16,10 @@ # Import transf_sim import transf_sim as Transfsimclass # Import heatsink_sim -import heatsink_sim as Heatsinksimclass +from heatsink_sim import HeatSinkSim as Heatsinksimclass +# Import server control class +import summary_processing as SumProcessing # Import server control class -import server_ctl as Serverctlclass - # logging.basicConfig(format='%(levelname)s,%(asctime)s:%(message)s', encoding='utf-8') # logging.getLogger('pygeckocircuits2').setLevel(logging.DEBUG) @@ -295,7 +284,7 @@ def load_transformer_config(act_ginfo: dct.GeneralInformation, act_config_transf act_ginfo, designspace_dict, transformer_data_dict) @staticmethod - def load_heat_sink_config(act_ginfo: dct.GeneralInformation, act_config_heat_sink: dict, act_hsim: Heatsinksimclass.HeatSinkSim) -> bool: + def load_heat_sink_config(act_ginfo: dct.GeneralInformation, act_config_heat_sink: dict, act_hsim: Heatsinksimclass) -> bool: """ Load and initialize the transformer optimization configuration. @@ -308,8 +297,6 @@ def load_heat_sink_config(act_ginfo: dct.GeneralInformation, act_config_heat_sin :return: True, if the configuration is successful :rtype: bool """ - # def init_configuration(act_hct_config_name: str, act_ginfo: dct.GeneralInformation, act_designspace_dict: dict, - # act_hctdimension_dict: dict) -> bool: # Variable initialisation # Get design space path @@ -335,6 +322,34 @@ def load_heat_sink_config(act_ginfo: dct.GeneralInformation, act_config_heat_sin # Initialize inductor optimization and return, if it was successful (true) return act_hsim.init_configuration(act_config_heat_sink["HeatsinkConfigName"]["heatsink_config_name"], act_ginfo, design_space_path, hct_dimension_dict) + @staticmethod # (ginfo, config_heat_sink, spro) + def init_summary_thermal_data(act_ginfo: dct.GeneralInformation, act_config_heat_sink: dict, act_spro: SumProcessing.DctSummmaryProcessing) -> bool: + """ + Initialize thermal data for summary processing. + + :param act_ginfo : General information about the study + :type act_ginfo : dct.GeneralInformation: + :param act_config_heat_sink: actual heat sink configuration information + :type act_config_heat_sink: dict: heat sink with the necessary configuration parameter + :param act_spro: summary processing object reference + :type act_spro: SumProcessing.DctSummmaryProcessing + :return: True, if the configuration is successful + :rtype: bool + """ + # Variable initialisation + + # Get heat sink dimension data + thermal_configuration_dict = { + "transistor_b1_cooling": act_config_heat_sink["ThermalResistanceData"]["transistor_b1_cooling"], + "transistor_b2_cooling": act_config_heat_sink["ThermalResistanceData"]["transistor_b2_cooling"], + "inductor_cooling": act_config_heat_sink["ThermalResistanceData"]["inductor_cooling"], + "transformer_cooling": act_config_heat_sink["ThermalResistanceData"]["transformer_cooling"], + "heat_sink": act_config_heat_sink["ThermalResistanceData"]["heat_sink"], + } + + # Initialize inductor optimization and return, if it was successful (true) + return act_spro.init_thermal_configuration(thermal_configuration_dict) + @staticmethod def check_breakpoint(break_point_key: str, info: str): """ @@ -368,35 +383,6 @@ def check_breakpoint(break_point_key: str, info: str): else: pass - @staticmethod - def start_dct_server(req_stop_server,stop_flag): - """Starts the server to control and supervice simulation. - - :param req_stop_server: Shared memory flag to request server to stop - :type req_stop_server: multiprocessing.Value - :param stop_flag: Shared memory flag which indicates that the server stops the measurment - :type stop_flag: multiprocessing.Value - """ - # Mounten des Stylesheetpfades - app.mount("/StyleSheets", StaticFiles(directory="htmltemplates/StyleSheets"), name="Stylesheets") - - # Start the server process - server_process = multiprocessing.Process(target=srv_ctl.run_server, args=(req_stop_server, stop_flag)) - server_process.start(); - - @staticmethod - def stop_dct_server(req_stop_server): - """Stop the server for the control and supervisuib of the simulation. - - :param req_stop_server: Shared memory flag to request server to stop - :type req_stop_server: multiprocessing.Value - """ - - # Request server to stop - req_stop_server.value = 1 - # Wait for joined server process - server_process.join(5) - @staticmethod def executeProgram(workspace_path: str): """Perform the main program. @@ -422,17 +408,14 @@ def executeProgram(workspace_path: str): isim = Inductsimclass.InductorSim # Transformer simulation tsim = Transfsimclass.Transfsim - # heat sink simulation - hsim = Heatsinksimclass.HeatSinkSim + # Heat sink simulation + hsim = Heatsinksimclass + # Summary processing + spro = SumProcessing.DctSummmaryProcessing # Flag for available filtered results filtered_resultFlag = False - # Server class to control the workflow - srv_ctl = Serverctlclass # Shared Memory für das Histogramm und den Status - # histogram_data = multiprocessing.Array('i', [0] * 25) - req_stop_server = multiprocessing.Value('i', 0) - stop_flag = multiprocessing.Value('i', 0) - + histogram_data = multiprocessing.Array('i', [0] * 25) # Check if workspace path is not provided by argument if workspace_path == "": @@ -541,13 +524,9 @@ def executeProgram(workspace_path: str): if not DctMainCtl.check_study_data(datapath, "heatsink_01"): raise ValueError(f"Study {config_program_flow['general']['StudyName']} in path {datapath} does not exist. No sqlite3-database found!") - # Warning, no data are available - # Check, if transformer optimization is to skip - # Warning, no data are available - # Check, if heat sink optimization is to skip - # Warning, no data are available # -- Start server -------------------------------------------------------------------------------------------- - DctMainCtl.start_dct_server(req_stop_server,stop_flag) + # Debug: Server switched off + # srv_ctl.start_dct_server(histogram_data,False) # -- Start simulation ---------------------------------------------------------------------------------------- @@ -641,21 +620,25 @@ def executeProgram(workspace_path: str): # Check breakpoint DctMainCtl.check_breakpoint(config_program_flow["breakpoints"]["Heatsink"], "Heat sink Pareto front calculated") - # Calculate the combination of components inductor and transformer with same electrical pareto point - # Filter the pareto front data of inductor and transformer - # Create a setup of the three components - # Define the heat sink - # Add this to the summary pareto list (no optimization?) + # Initialisation thermal data + if not DctMainCtl.init_summary_thermal_data(ginfo, config_heat_sink, spro): + raise ValueError("Thermal data configuration not initialized!") + # Create list of inductor and transformer study (ASA: Currently not implemented in configuration files) + inductor_study_names = [config_inductor["InductorConfigName"]["inductor_config_name"]] + stacked_transformer_study_names = [config_transformer["TransformerConfigName"]["transformer_config_name"]] + # Start summary processing by generating the dataframe from calculated simmulation results + s_df = spro.generate_result_database(ginfo, inductor_study_names, stacked_transformer_study_names) + # Select the needed heatsink configuration + spro.select_heatsink_configuration(ginfo, config_heat_sink["HeatsinkConfigName"]["heatsink_config_name"], s_df) - # Check, if electrical optimization is to skip - # Initialize data - # Start calculation - # Filter the pareto front data + # Check breakpoint + DctMainCtl.check_breakpoint(config_program_flow["breakpoints"]["Summary"], "Calculation is complete") # Join process if necessary esim.join_process() # Shut down server - DctMainCtl.stop_dct_server() + # Debug: Server switched off + # srv_ctl.stop_dct_server() pass diff --git a/dct/heatsink_sim.py b/dct/heatsink_sim.py index 754fe3b..092b565 100644 --- a/dct/heatsink_sim.py +++ b/dct/heatsink_sim.py @@ -81,54 +81,7 @@ def init_configuration(act_hct_config_name: str, act_ginfo: dct.GeneralInformati return ret_val - @staticmethod - def calculate_r_th_copper_coin(cooling_area: float, height_pcb: float = 1.55e-3, - height_pcb_heat_sink: float = 3.0e-3) -> tuple[float, float]: - """ - Calculate the thermal resistance of the copper coin. - - Assumptions are made with some geometry factors from a real copper coin for TO263 housing. - :param cooling_area: cooling area in m² - :type cooling_area: float - :param height_pcb: PCB thickness, e.g. 1.55 mm - :type height_pcb: float - :param height_pcb_heat_sink: Distance from PCB to heat sink in m - :type height_pcb_heat_sink: float - :return: r_th_copper_coin, effective_copper_coin_cooling_area - :rtype: tuple[float, float] - """ - factor_pcb_area_copper_coin = 1.42 - factor_bottom_area_copper_coin = 0.39 - thermal_conductivity_copper = 136 # W/(m*K) - - effective_pcb_cooling_area = cooling_area / factor_pcb_area_copper_coin - effective_bottom_cooling_area = effective_pcb_cooling_area / factor_bottom_area_copper_coin - - r_pcb = 1 / thermal_conductivity_copper * height_pcb / effective_pcb_cooling_area - r_bottom = 1 / thermal_conductivity_copper * height_pcb_heat_sink / effective_bottom_cooling_area - - r_copper_coin = r_pcb + r_bottom - - return r_copper_coin, effective_bottom_cooling_area - - @staticmethod - def calculate_r_th_tim(copper_coin_bot_area: float, transistor_cooling: TransistorCooling) -> float: - """ - Calculate the thermal resistance of the thermal interface material (TIM). - - :param copper_coin_bot_area: bottom copper coin area in m² - :type copper_coin_bot_area: float - :param transistor_cooling: Transistor cooling DTO - :type transistor_cooling: TransistorCooling - :return: r_th of TIM material - :rtype: float - """ - r_th_tim = 1 / transistor_cooling.tim_conductivity * transistor_cooling.tim_thickness / copper_coin_bot_area - - return r_th_tim - - # Simulation handler. Later the simulation handler starts a process per list entry. - @staticmethod + @staticmethod # Simulation handler. Later the simulation handler starts a process per list entry. def _simulation(act_hct_config: hopt.OptimizationParameters, act_ginfo: dct.GeneralInformation, target_number_trials: int, re_simulate: bool, debug: bool): """ @@ -188,3 +141,52 @@ def simulation_handler(act_ginfo: dct.GeneralInformation, target_number_trials: if debug: # stop after one circuit run break + +class ThermalCalcSupport: + """Provides functions to calculate the thermal resistance.""" + + @staticmethod + def calculate_r_th_copper_coin(cooling_area: float, height_pcb: float = 1.55e-3, + height_pcb_heat_sink: float = 3.0e-3) -> tuple[float, float]: + """ + Calculate the thermal resistance of the copper coin. + + Assumptions are made with some geometry factors from a real copper coin for TO263 housing. + :param cooling_area: cooling area in m² + :type cooling_area: float + :param height_pcb: PCB thickness, e.g. 1.55 mm + :type height_pcb: float + :param height_pcb_heat_sink: Distance from PCB to heat sink in m + :type height_pcb_heat_sink: float + :return: r_th_copper_coin, effective_copper_coin_cooling_area + :rtype: tuple[float, float] + """ + factor_pcb_area_copper_coin = 1.42 + factor_bottom_area_copper_coin = 0.39 + thermal_conductivity_copper = 136 # W/(m*K) + + effective_pcb_cooling_area = cooling_area / factor_pcb_area_copper_coin + effective_bottom_cooling_area = effective_pcb_cooling_area / factor_bottom_area_copper_coin + + r_pcb = 1 / thermal_conductivity_copper * height_pcb / effective_pcb_cooling_area + r_bottom = 1 / thermal_conductivity_copper * height_pcb_heat_sink / effective_bottom_cooling_area + + r_copper_coin = r_pcb + r_bottom + + return r_copper_coin, effective_bottom_cooling_area + + @staticmethod + def calculate_r_th_tim(copper_coin_bot_area: float, transistor_cooling: TransistorCooling) -> float: + """ + Calculate the thermal resistance of the thermal interface material (TIM). + + :param copper_coin_bot_area: bottom copper coin area in m² + :type copper_coin_bot_area: float + :param transistor_cooling: Transistor cooling DTO + :type transistor_cooling: TransistorCooling + :return: r_th of TIM material + :rtype: float + """ + r_th_tim = 1 / transistor_cooling.tim_conductivity * transistor_cooling.tim_thickness / copper_coin_bot_area + + return r_th_tim diff --git a/dct/server_ctl.py b/dct/server_ctl.py index 7ad6b60..297d6dd 100644 --- a/dct/server_ctl.py +++ b/dct/server_ctl.py @@ -1,16 +1,22 @@ -import base64 -import io +"""Server class implementation.""" +# python libraries +import multiprocessing import threading import time -import matplotlib.pyplot as plt +# 3rd party libraries import uvicorn from fastapi import FastAPI, Request from fastapi.templating import Jinja2Templates -from fastapi.responses import HTMLResponse, JSONResponse, Response +from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles +# own libraries + + class Dct_server: + """server class to supervise the simulation.""" + # Variable declaration # FastAPI-Server definieren app = FastAPI() @@ -19,69 +25,146 @@ class Dct_server: # Serverobject srv_obj = None # Shared memory variable - shared_histogram = None - req_stop = None - stop_flag = None + req_stop = multiprocessing.Value('i', 0) + stop_flag = multiprocessing.Value('i', 0) + # Server process + _server_process = None + # program exit flag + _prog_exit_flag = False + # Server supervision thread + _srv_supervision_thd = None # Mounten des Stylesheetpfades app.mount("/StyleSheets", StaticFiles(directory="htmltemplates/StyleSheets"), name="Stylesheets") @staticmethod - def run_server(req_stop, stop_flag): - """ Startet den FastAPI-Server """ + def start_dct_server(shared_histogram, program_exit_flag: bool): + """Start the server to control and supervise simulation. + + :param shared_histogram: Shared memory flag for histogram information + :type shared_histogram: multiprocessing.Value + :param program_exit_flag: Flag, which indicates if the server (False) or the whole simulation (True) is to stop + :type program_exit_flag: boolean + """ + Dct_server._prog_exit_flag = program_exit_flag + + # Start the server process + Dct_server._server_process = multiprocessing.Process(target=Dct_server._run_server, args=(shared_histogram,)) + Dct_server._server_process.start() + # Check if server process supervision is to start due to program exit requested by server + if Dct_server._prog_exit_flag: + # Create thread for the serversupervision and start it + Dct_server._srv_supervision_thd = threading.Thread(target=Dct_server._supervice_server_stop) + Dct_server._srv_supervision_thd.start() + + @staticmethod + def stop_dct_server(): + """Stop the simulation supervision server.""" + # Set program exit flag to false because program will be exit by themself + Dct_server._prog_exit_flag = False + + # Request server to stop + Dct_server.req_stop.value = 1 + # Debug + print("Process shall join") + # Wait for joined server process + Dct_server._server_process.join(5) + # Stop server supervision if started + if Dct_server._srv_supervision_thd is not None: + Dct_server._srv_supervision_thd.join(5) + # Debug + print("Process has joined") + + @staticmethod + def _supervice_server_stop(): + """Stop the FastAPI-Server.""" + # Supervice if the server is stopped by user request + while True: + # Reduce CPU-supervice load by toggle each second + time.sleep(1) + # Check if server is stopped and requested to stop if the program needs to stop too + if Dct_server.stop_flag.value == 1 and Dct_server._prog_exit_flag: + # Check if the program needs to stop too + print("Program stop is requested") + # Soft kill of process does not work + # sys.exit() + break + + @staticmethod + def _run_server(shared_histogram): + """Start FastAPI-server. + + :param request : Request value + :type request : Request + + :return: Html- page based on html-template + :rtype: _TemplateResponse + """ # Overtake the shared memory variable - # Dct_server.shared_histogram = shared_histogram - Dct_server.req_stop = req_stop - Dct_server.stop_flag = stop_flag - # Initialize server configuration + Dct_server.shared_histogram = shared_histogram + # Start the server (blocking call) config = uvicorn.Config(Dct_server.app, host="127.0.0.1", port=8004, log_level="info") Dct_server.srv_obj = uvicorn.Server(config) # Create thread for the server and start it Dct_server.srv_thread = threading.Thread(target=Dct_server.dct_server_thread) - # Start the server (blocking call) Dct_server.srv_thread.start() # Supervice if the server is stopped by main while True: # Reduce CPU-supervice load by toggle each second time.sleep(1) # Check if server is requested to stop - if Dct_server.stop_flag.value == 1 or Dct_server.req_stop.value == 1: - Dct_server.stop_flag.value = 1 + if Dct_server.req_stop.value == 1: break # Stoppt den Server Dct_server.srv_obj.should_exit = True + # Debug + print("SThread soll joinen") # Wait for thread stop Dct_server.srv_thread.join() + # Debug + print("SThread hat gejoint") + # Set server stop flag to 0 + Dct_server.stop_flag.value = 1 @staticmethod def dct_server_thread(): - """ Startet den FastAPI-Server """ - try: - # Start the server in a blocking call - Dct_server.srv_obj.run() - except Exception as e: - print(f"Fehler beim Starten des Servers: {e}") - + """Start FastAPI-Server in thread.""" + # Start the server in a blocking call + Dct_server.srv_obj.run() @app.get("/", response_class=HTMLResponse) async def main_page(request: Request, action: str = None): + """Provide the answer on client requests. + + :param request : Request value + :type request : Request + :param action : Requested action + :type action : Requested action + :return: Html- page based on html-template + :rtype: _TemplateResponse + """ if action == "continue": Dct_server.status_message = "Weiter ist aktiv" elif action == "pause": Dct_server.status_message = "Pause ist aktiv" elif action == "stop": - Dct_server.status_message = "Stoppt die Simulation und den Server" - Dct_server.stop_flag.value = 1 + Dct_server.status_message = "Stoppt den Server und die Simulation (wenn prog_exit_flag==true)" + Dct_server.req_stop.value = 1 return Dct_server.templates.TemplateResponse("html_main.html", {"request": request, "textvariable": Dct_server.status_message}) - @app.get("/histogram") def get_histogram(request: Request): - """Erstellt das Histogramm als Bild und gibt es als Base64-String zurück""" + """Provide the answer on client histogram request. + :param request : Request value + :type request : Request + + :return: Html- page based on html-template with Histogram information + :rtype: _TemplateResponse + """ """ plt.figure() plt.bar(range(25), Dct_server.shared_histogram[:25], color='blue', edgecolor='black') @@ -95,8 +178,9 @@ def get_histogram(request: Request): buf.seek(0) img_str = base64.b64encode(buf.read()).decode('utf-8') + """ + # Return the html-page with updated image data # return Dct_server.templates.TemplateResponse( "html_histogram.html", {"request": request, "imagedata": img_str}) - """ imagepath = "StyleSheets/Dummytrafo.png" - return Dct_server.templates.TemplateResponse( "html_histogram.html", {"request": request, "image_path": imagepath}) + return Dct_server.templates.TemplateResponse("html_histogram.html", {"request": request, "image_path": imagepath}) diff --git a/dct/summary_eval.py b/dct/summary_eval.py deleted file mode 100644 index 73d6d60..0000000 --- a/dct/summary_eval.py +++ /dev/null @@ -1,250 +0,0 @@ -"""Summary pareto optimization.""" -# python libraries -import os -import pickle - -# 3rd party libraries -import pandas as pd -import numpy as np -from matplotlib import pyplot as plt - -# own libraries -import dct - - -class Dct_result_summary: - - # Variable declaration - - @staticmethod - def generate_result_database(act_ginfo: dct.GeneralInformation): - """Generate a database df by summaries the calculation results. - - The results of circuit-optimization, inductor-optimization and transformer-optimization are to summarize - - # specify input parameters - project_name = "2024-10-04_dab_paper" - circuit_study_name = "circuit_paper_trial_1" - inductor_study_name_list = ["inductor_trial_1"] - stacked_transformer_study_name_list = ["transformer_trial_1"] - heat_sink_study_name = "heat_sink_trial_3_dimensions" - - # load project file paths - filepaths = dct.Optimization.load_filepaths(os.path.abspath(os.path.join(os.curdir, project_name))) - - circuit_filepath_results = os.path.join(filepaths.circuit, circuit_study_name, "filtered_results") - circuit_objects = os.scandir(circuit_filepath_results) - circuit_numbers = [entry.name.split(os.extsep)[0] for entry in circuit_objects] - - """ - # Variable declaration - - - # iterate circuit numbers - for circuit_number in act_ginfo.filtered_list_id: - # Assemble pkl-filename - circuit_filepath_number = os.path.join(act_ginfo.circuit_study_path, "filtered_results",f"{circuit_number}.pkl") - - # Get circuit results - circuit_dto = dct.HandleDabDto.load_from_file(circuit_filepath_number) - - a=act_ginfo.circuit_study_path - - print(f"{circuit_number=}") - - # iterate inductor study - for inductor_study_name in inductor_study_name_list: - inductor_filepath_results = os.path.join(filepaths.inductor, circuit_study_name, circuit_number, - inductor_study_name, "09_circuit_dtos_incl_inductor_losses") - if os.path.exists(inductor_filepath_results): - - inductor_objects = os.scandir(inductor_filepath_results) - inductor_numbers = [entry.name.split(os.extsep)[0] for entry in inductor_objects] - - # iterate inductor numbers - for inductor_number in inductor_numbers: - inductor_filepath_number = os.path.join(inductor_filepath_results, f"{inductor_number}.pkl") - - # Get inductor results - with open(inductor_filepath_number, 'rb') as pickle_file_data: - inductor_dto = pickle.load(pickle_file_data) - - if int(inductor_dto.circuit_trial_number) != int(circuit_number): - raise ValueError(f"{inductor_dto.circuit_trial_number=} != {circuit_number}") - if int(inductor_dto.inductor_trial_number) != int(inductor_number): - raise ValueError(f"{inductor_dto.inductor_trial_number=} != {inductor_number}") - - inductance_loss_matrix = inductor_dto.p_combined_losses - - for stacked_transformer_study_name in stacked_transformer_study_name_list: - stacked_transformer_filepath_results = os.path.join(filepaths.transformer, circuit_study_name, circuit_number, - stacked_transformer_study_name, "09_circuit_dtos_incl_transformer_losses") - - if os.path.exists(stacked_transformer_filepath_results): - - stacked_transformer_objects = os.scandir(stacked_transformer_filepath_results) - stacked_transformer_numbers = [entry.name.split(os.extsep)[0] for entry in stacked_transformer_objects] - - for stacked_transformer_number in stacked_transformer_numbers: - stacked_transformer_filepath_number = os.path.join(stacked_transformer_filepath_results, f"{stacked_transformer_number}.pkl") - - # get transformer results - with open(stacked_transformer_filepath_number, 'rb') as pickle_file_data: - transformer_dto = pickle.load(pickle_file_data) - - if int(transformer_dto.circuit_trial_number) != int(circuit_number): - raise ValueError(f"{transformer_dto.circuit_trial_number=} != {circuit_number}") - if int(transformer_dto.stacked_transformer_trial_number) != int(stacked_transformer_number): - raise ValueError(f"{transformer_dto.stacked_transformer_trial_number=} != {stacked_transformer_number}") - - transformer_loss_matrix = transformer_dto.p_combined_losses - - # get transistor results - total_transistor_cond_loss_matrix = 2 * (circuit_dto.gecko_results.S11_p_cond + circuit_dto.gecko_results.S12_p_cond + \ - circuit_dto.gecko_results.S23_p_cond + circuit_dto.gecko_results.S24_p_cond) - - max_b1_transistor_cond_loss_matrix = circuit_dto.gecko_results.S11_p_cond - max_b1_transistor_cond_loss_matrix[ - np.greater(circuit_dto.gecko_results.S12_p_cond, circuit_dto.gecko_results.S11_p_cond)] = ( - circuit_dto.gecko_results.S12_p_cond)[ - np.greater(circuit_dto.gecko_results.S12_p_cond, circuit_dto.gecko_results.S11_p_cond)] - - max_b2_transistor_cond_loss_matrix = circuit_dto.gecko_results.S23_p_cond - max_b2_transistor_cond_loss_matrix[ - np.greater(circuit_dto.gecko_results.S24_p_cond, circuit_dto.gecko_results.S23_p_cond)] = \ - circuit_dto.gecko_results.S24_p_cond[ - np.greater(circuit_dto.gecko_results.S24_p_cond, circuit_dto.gecko_results.S23_p_cond)] - - total_loss_matrix = (inductor_dto.p_combined_losses + total_transistor_cond_loss_matrix + \ - transformer_dto.p_combined_losses) - - # maximum loss indices - max_loss_all_index = np.unravel_index(total_loss_matrix.argmax(), np.shape(total_loss_matrix)) - max_loss_circuit_1_index = np.unravel_index(max_b1_transistor_cond_loss_matrix.argmax(), - np.shape(max_b1_transistor_cond_loss_matrix)) - max_loss_circuit_2_index = np.unravel_index(max_b2_transistor_cond_loss_matrix.argmax(), - np.shape(max_b2_transistor_cond_loss_matrix)) - max_loss_inductor_index = np.unravel_index(inductance_loss_matrix.argmax(), np.shape(inductance_loss_matrix)) - max_loss_transformer_index = np.unravel_index(transformer_loss_matrix.argmax(), np.shape(transformer_loss_matrix)) - - # get all the losses in a matrix - r_th_copper_coin_1, copper_coin_area_1 = dct.calculate_r_th_copper_coin(circuit_dto.input_config.transistor_dto_1.cooling_area) - r_th_copper_coin_2, copper_coin_area_2 = dct.calculate_r_th_copper_coin(circuit_dto.input_config.transistor_dto_2.cooling_area) - - circuit_r_th_tim_1 = dct.calculate_r_th_tim(copper_coin_area_1, transistor_b1_cooling) - circuit_r_th_tim_2 = dct.calculate_r_th_tim(copper_coin_area_2, transistor_b2_cooling) - - circuit_r_th_1_jhs = circuit_dto.input_config.transistor_dto_1.r_th_jc + r_th_copper_coin_1 + circuit_r_th_tim_1 - circuit_r_th_2_jhs = circuit_dto.input_config.transistor_dto_2.r_th_jc + r_th_copper_coin_2 + circuit_r_th_tim_2 - - circuit_heat_sink_max_1_matrix = ( - circuit_dto.input_config.transistor_dto_1.t_j_max_op - circuit_r_th_1_jhs * max_b1_transistor_cond_loss_matrix) - circuit_heat_sink_max_2_matrix = ( - circuit_dto.input_config.transistor_dto_2.t_j_max_op - circuit_r_th_2_jhs * max_b2_transistor_cond_loss_matrix) - - r_th_ind_heat_sink = 1 / inductor_cooling.tim_conductivity * inductor_cooling.tim_thickness / inductor_dto.area_to_heat_sink - temperature_inductor_heat_sink_max_matrix = 125 - r_th_ind_heat_sink * inductance_loss_matrix - - r_th_xfmr_heat_sink = (1 / transformer_cooling.tim_conductivity * \ - transformer_cooling.tim_thickness / transformer_dto.area_to_heat_sink) - temperature_xfmr_heat_sink_max_matrix = 125 - r_th_xfmr_heat_sink * transformer_loss_matrix - - # maximum heat sink temperatures (minimum of all the maximum temperatures of single components) - t_min_matrix = np.minimum(circuit_heat_sink_max_1_matrix, circuit_heat_sink_max_2_matrix) - t_min_matrix = np.minimum(t_min_matrix, temperature_inductor_heat_sink_max_matrix) - t_min_matrix = np.minimum(t_min_matrix, temperature_xfmr_heat_sink_max_matrix) - t_min_matrix = np.minimum(t_min_matrix, heat_sink.t_hs_max) - - # maximum delta temperature over the heat sink - delta_t_max_heat_sink_matrix = t_min_matrix - heat_sink.t_ambient - - r_th_heat_sink_target_matrix = delta_t_max_heat_sink_matrix / total_loss_matrix - - r_th_target = r_th_heat_sink_target_matrix.min() - - data = { - # circuit - "circuit_number": circuit_number, - "circuit_mean_loss": np.mean(total_transistor_cond_loss_matrix), - "circuit_max_all_loss": total_transistor_cond_loss_matrix[max_loss_all_index], - "circuit_max_circuit_ib_loss": total_transistor_cond_loss_matrix[max_loss_circuit_1_index], - "circuit_max_circuit_ob_loss": total_transistor_cond_loss_matrix[max_loss_circuit_2_index], - "circuit_max_inductor_loss": total_transistor_cond_loss_matrix[max_loss_inductor_index], - "circuit_max_transformer_loss": total_transistor_cond_loss_matrix[max_loss_transformer_index], - "circuit_t_j_max_1": circuit_dto.input_config.transistor_dto_1.t_j_max_op, - "circuit_t_j_max_2": circuit_dto.input_config.transistor_dto_2.t_j_max_op, - "circuit_r_th_ib_jhs_1": circuit_r_th_1_jhs, - "circuit_r_th_ib_jhs_2": circuit_r_th_2_jhs, - "circuit_heat_sink_temperature_max_1": circuit_heat_sink_max_1_matrix[max_loss_circuit_1_index], - "circuit_heat_sink_temperature_max_2": circuit_heat_sink_max_2_matrix[max_loss_circuit_2_index], - "circuit_area": 4 * (copper_coin_area_1 + copper_coin_area_2), - # inductor - "inductor_study_name": inductor_study_name, - "inductor_number": inductor_number, - "inductor_volume": inductor_dto.volume, - "inductor_mean_loss": np.mean(inductance_loss_matrix), - "inductor_max_all_loss": inductance_loss_matrix[max_loss_all_index], - "inductor_max_circuit_ib_loss": inductance_loss_matrix[max_loss_circuit_1_index], - "inductor_max_circuit_ob_loss": inductance_loss_matrix[max_loss_circuit_2_index], - "inductor_max_inductor_loss": inductance_loss_matrix[max_loss_inductor_index], - "inductor_max_transformer_loss": inductance_loss_matrix[max_loss_transformer_index], - "inductor_t_max": 0, - "inductor_heat_sink_temperature_max": temperature_inductor_heat_sink_max_matrix[max_loss_inductor_index], - "inductor_area": inductor_dto.area_to_heat_sink, - # transformer - "transformer_study_name": stacked_transformer_study_name, - "transformer_number": stacked_transformer_number, - "transformer_volume": transformer_dto.volume, - "transformer_mean_loss": np.mean(transformer_dto.p_combined_losses), - "transformer_max_all_loss": transformer_loss_matrix[max_loss_all_index], - "transformer_max_circuit_ib_loss": transformer_loss_matrix[max_loss_circuit_1_index], - "transformer_max_circuit_ob_loss": transformer_loss_matrix[max_loss_circuit_2_index], - "transformer_max_inductor_loss": transformer_loss_matrix[max_loss_inductor_index], - "transformer_max_transformer_loss": transformer_loss_matrix[max_loss_transformer_index], - "transformer_t_max": 0, - "transformer_heat_sink_temperature_max": temperature_xfmr_heat_sink_max_matrix[max_loss_transformer_index], - "transformer_area": transformer_dto.area_to_heat_sink, - - # summary - "total_losses": total_loss_matrix[max_loss_all_index], - - # heat sink - "r_th_heat_sink": r_th_target - } - local_df = pd.DataFrame([data]) - - df = pd.concat([df, local_df], axis=0) - - # Calculate tthe total area as sum of circuit, inductor and transformer area df-comand is like vector sum v1[:]=v2[:]+v3[:]) - df["total_area"] = df["circuit_area"] + df["inductor_area"] + df["transformer_area"] - df["total_mean_loss"] = df["circuit_mean_loss"] + df["inductor_mean_loss"] + df["transformer_mean_loss"] - df["volume_wo_heat_sink"] = df["transformer_volume"] + df["inductor_volume"] - # Save results to file (ASA : later to store only on demand) - df.to_csv(f"{filepaths.heat_sink}/result_df.csv") - - @staticmethod - def select_heatsink_configuration(): - """Select the heatsink configuration from calculated heatsink pareto front. - - Based on the summary results the suitable heatsink configuration is to select from paretofront. - """ - # Variable declaration - - # load heat sink - hs_config_filepath = os.path.join(filepaths.heat_sink, f"{heat_sink_study_name}.pkl") - hs_config = hct.Optimization.load_config(hs_config_filepath) - df_hs = hct.Optimization.study_to_df(hs_config) - - # load summarized results from database (ASA : xTodo: Take it directly form memory) - df_wo_hs = pd.read_csv(f"{filepaths.heat_sink}/result_df.csv") - - # generate full summary as panda database operation - print(df_hs.loc[df_hs["values_1"] < 1]["values_0"].nsmallest(n=1).values[0]) - df_wo_hs["heat_sink_volume"] = df_wo_hs["r_th_heat_sink"].apply( - lambda r_th_max: df_hs.loc[df_hs["values_1"] < r_th_max]["values_0"].nsmallest(n=1).values[0] \ - if df_hs.loc[df_hs["values_1"] < r_th_max]["values_0"].nsmallest(n=1).values else None) - - df_wo_hs["total_volume"] = df_wo_hs["transformer_volume"] + df_wo_hs["inductor_volume"] + df_wo_hs["heat_sink_volume"] - - # save full summary - df_wo_hs.to_csv(f"{filepaths.heat_sink}/df_summary.csv") diff --git a/dct/summary_processing.py b/dct/summary_processing.py new file mode 100644 index 0000000..58cd07f --- /dev/null +++ b/dct/summary_processing.py @@ -0,0 +1,384 @@ +"""Summary pareto optimizations.""" +# python libraries +import os +import pickle + +# 3rd party libraries +import pandas as pd +import numpy as np + +# own libraries +import dct +from heatsink_sim import ThermalCalcSupport as thr_sup +import hct + + +class DctSummmaryProcessing: + """Perform the summary calculation based on optimization results.""" + + # Variable declaration + + # Areas and transistor cooling parameter + copper_coin_area_1 = None + transistor_b1_cooling = None + copper_coin_area_2 = None + transistor_b2_cooling = None + + # Thermal resistance * area + r_th_ind_heat_sink_A = None + r_th_xfmr_heat_sink_A = None + + # Heat sink parameter + heat_sink = None + + @staticmethod + def init_thermal_configuration(act_thermal_configuration_dict: dict) -> bool: + """Initialize the thermal parameter of the connection points for the transistors, inductor and transformer. + + :param act_thermal_configuration_dict : dict with data of the thermal configuration + :type act_thermal_configuration_dict : dict + + :return: True, if the thermal parameter of the connection points was successful initialized + :rtype: bool + """ + # Variable declaration + # Return variable initialized to True + successful_init = True + + # Thermal parameter for bridge transistor 1: List [tim_thickness, tim_conductivity] + DctSummmaryProcessing.transistor_b1_cooling = dct.TransistorCooling( + tim_thickness=act_thermal_configuration_dict["transistor_b1_cooling"][0], + tim_conductivity=act_thermal_configuration_dict["transistor_b1_cooling"][1], + ) + + # Thermal parameter for bridge transistor 2: List [tim_thickness, tim_conductivity] + DctSummmaryProcessing.transistor_b2_cooling = dct.TransistorCooling( + tim_thickness=act_thermal_configuration_dict["transistor_b2_cooling"][0], + tim_conductivity=act_thermal_configuration_dict["transistor_b2_cooling"][1], + ) + + # Thermal parameter for inductor: rth per area: List [tim_thickness, tim_conductivity] + inductor_cooling = dct.InductiveElementCooling( + tim_thickness=act_thermal_configuration_dict["inductor_cooling"][0], + tim_conductivity=act_thermal_configuration_dict["inductor_cooling"][1] + ) + # Check on zero + if inductor_cooling.tim_conductivity > 0: + # Calculate the thermal resistance area product + DctSummmaryProcessing.r_th_ind_heat_sink_A = inductor_cooling.tim_thickness / inductor_cooling.tim_conductivity + else: + print(f"inductor cooling tim conductivity value must be greater zero, but is {inductor_cooling.tim_conductivity}!") + successful_init = False + + # Thermal parameter for inductor: rth per area: List [tim_thickness, tim_conductivity] + # ASA: Rename database class from InductiveElementCooling to MagneticElementCooling + transformer_cooling = dct.InductiveElementCooling( + tim_thickness=act_thermal_configuration_dict["transformer_cooling"][0], + tim_conductivity=act_thermal_configuration_dict["transformer_cooling"][1] + ) + # Check on zero ( ASA: Maybe in general all configurtation files are to check for validity in advanced. In this case the check can be removed.) + if inductor_cooling.tim_conductivity > 0: + # Calculate the thermal resistance area product + DctSummmaryProcessing.r_th_xfmr_heat_sink_A = transformer_cooling.tim_thickness / transformer_cooling.tim_conductivity + else: + print(f"transformer cooling tim conductivity value must be greater zero, but is {transformer_cooling.tim_conductivity}!") + successful_init = False + + # Heat sink parameter: List [t_ambient, t_hs_max] + DctSummmaryProcessing.heat_sink = dct.HeatSink( + t_ambient=act_thermal_configuration_dict["heat_sink"][0], + t_hs_max=act_thermal_configuration_dict["heat_sink"][1] + ) + + return successful_init + + @staticmethod + def _generate_number_list(act_dir_name: str, act_device_numbers: list[str]) -> bool: + """Generate a list of the numbers from filenames. + + :param act_dir_name : Name of the directory containing the files + :type act_dir_name : str + :param act_device_numbers : Reference to the device number list object + :type act_device_numbers : str + + :return: True, if the directory exists and contains minimum one file + :rtype: bool + """ + # Check if target folder 09_circuit_dtos_incl_inductor_losses is created + if os.path.exists(act_dir_name): + # Create list of filespath + file_list = os.listdir(act_dir_name) + # Filter basename without extension + for file_name in file_list: + # Create file path + file_path = os.path.join(act_dir_name, file_name) + # Check if it is a file + if os.path.isfile(file_path): + device_number = os.path.splitext(os.path.basename(file_name))[0] + # Check file type + extension = os.path.splitext(os.path.basename(file_name))[1] + if extension == '.pkl': + act_device_numbers.append(device_number) + else: + print(f"File {device_number}{extension} has no extension '.pkl'!") + else: + print(f"File'{file_path}' does not exists!") + else: + print("Path 'act_dir_name' does not exists!") + + if len(act_device_numbers) > 0: + return True + else: + return False + + @staticmethod + def generate_result_database(act_ginfo: dct.GeneralInformation, act_inductor_study_names: list[str], + act_stacked_transformer_study_names: list[str]) -> pd.DataFrame: + """Generate a database df by summaries the calculation results. + + :param act_ginfo : General information about the study + :type act_ginfo : dct.GeneralInformation: + :param act_inductor_study_names : List of names with inductor studies which are to process + :type act_inductor_study_names : list[str] + :param act_stacked_transformer_study_names : List of names with transformer studies which are to process + :type act_stacked_transformer_study_names : list[str] + + :return: dataframe with result information of the pareto front + :rtype: pd.DataFrame + """ + # Variable declaration + # Result dataframe + df = pd.DataFrame() + + # iterate circuit numbers + for circuit_number in act_ginfo.filtered_list_id: + # Assemble pkl-filename + circuit_filepath_number = os.path.join(act_ginfo.circuit_study_path, "filtered_results", f"{circuit_number}.pkl") + + # Get circuit results + circuit_dto = dct.HandleDabDto.load_from_file(circuit_filepath_number) + + # Calculate the thermal values + + # Begin: ASA: No influence by inductor or transformer ################################ + # get transistor results + total_transistor_cond_loss_matrix \ + = 4 * (circuit_dto.calc_losses.p_m1_conduction + circuit_dto.calc_losses.p_m2_conduction) + + b1_transistor_cond_loss_matrix = circuit_dto.calc_losses.p_m1_conduction + b2_transistor_cond_loss_matrix = circuit_dto.calc_losses.p_m2_conduction + # End: ASA: No influence by inductor or transformer ################################ + # Begin: ASA: No influence by inductor or transformer ################################ + # get all the losses in a matrix + r_th_copper_coin_1, copper_coin_area_1 = thr_sup.calculate_r_th_copper_coin( + circuit_dto.input_config.transistor_dto_1.cooling_area) + r_th_copper_coin_2, copper_coin_area_2 = thr_sup.calculate_r_th_copper_coin( + circuit_dto.input_config.transistor_dto_2.cooling_area) + + circuit_r_th_tim_1 = thr_sup.calculate_r_th_tim(copper_coin_area_1, DctSummmaryProcessing.transistor_b1_cooling) + circuit_r_th_tim_2 = thr_sup.calculate_r_th_tim(copper_coin_area_2, DctSummmaryProcessing.transistor_b2_cooling) + + circuit_r_th_1_jhs = circuit_dto.input_config.transistor_dto_1.r_th_jc + r_th_copper_coin_1 + circuit_r_th_tim_1 + circuit_r_th_2_jhs = circuit_dto.input_config.transistor_dto_2.r_th_jc + r_th_copper_coin_2 + circuit_r_th_tim_2 + + circuit_heat_sink_max_1_matrix = ( + circuit_dto.input_config.transistor_dto_1.t_j_max_op - circuit_r_th_1_jhs * b1_transistor_cond_loss_matrix) + circuit_heat_sink_max_2_matrix = ( + circuit_dto.input_config.transistor_dto_2.t_j_max_op - circuit_r_th_2_jhs * b2_transistor_cond_loss_matrix) + # End: ASA: No influence by inductor or transformer ################################ + + print(f"{circuit_number=}") + + # iterate inductor study + for inductor_study_name in act_inductor_study_names: + + # Create update listfile for inductor and transformer + # Initialise inductor and transformer list + inductor_numbers = [] + stacked_transformer_numbers = [] + + # Assemble directory name for inductor results:.../09_circuit_dtos_incl_inductor_losseslosses + inductor_filepath_results = os.path.join(act_ginfo.inductor_study_path, circuit_number, + inductor_study_name, + "09_circuit_dtos_incl_inductor_losses") + + # Check, if inductor number list cannot be generated + if not DctSummmaryProcessing._generate_number_list(inductor_filepath_results, inductor_numbers): + print(f"Path {inductor_filepath_results} does not exists or does not contains any pkl-files!") + # Next circuit + continue + + # iterate inductor numbers + for inductor_number in inductor_numbers: + inductor_filepath_number = os.path.join(inductor_filepath_results, f"{inductor_number}.pkl") + + # Get inductor results + with open(inductor_filepath_number, 'rb') as pickle_file_data: + inductor_dto = pickle.load(pickle_file_data) + + if int(inductor_dto.circuit_trial_number) != int(circuit_number): + raise ValueError(f"{inductor_dto.circuit_trial_number=} != {circuit_number}") + if int(inductor_dto.inductor_trial_number) != int(inductor_number): + raise ValueError(f"{inductor_dto.inductor_trial_number=} != {inductor_number}") + + inductance_loss_matrix = inductor_dto.p_combined_losses + + # iterate transformer study + for stacked_transformer_study_name in act_stacked_transformer_study_names: + + # Assemble directory name for transformer results:.../09_circuit_dtos_incl_transformer_losseslosses + stacked_transformer_filepath_results = os.path.join(act_ginfo.transformer_study_path, + circuit_number, + stacked_transformer_study_name, + "09_circuit_dtos_incl_transformer_losses") + + # Check, if stacked transformer number list cannot be generated + if not DctSummmaryProcessing._generate_number_list(stacked_transformer_filepath_results, + stacked_transformer_numbers): + print(f"Path {stacked_transformer_filepath_results} does not exists or does not contains any pkl-files!") + # Next circuit + continue + + # iterate transformer numbers + for stacked_transformer_number in stacked_transformer_numbers: + stacked_transformer_filepath_number = os.path.join(stacked_transformer_filepath_results, f"{stacked_transformer_number}.pkl") + + # get transformer results + with open(stacked_transformer_filepath_number, 'rb') as pickle_file_data: + transformer_dto = pickle.load(pickle_file_data) + + if int(transformer_dto.circuit_trial_number) != int(circuit_number): + raise ValueError(f"{transformer_dto.circuit_trial_number=} != {circuit_number}") + if int(transformer_dto.stacked_transformer_trial_number) != int(stacked_transformer_number): + raise ValueError(f"{transformer_dto.stacked_transformer_trial_number=} != {stacked_transformer_number}") + + transformer_loss_matrix = transformer_dto.p_combined_losses + + total_loss_matrix = (inductor_dto.p_combined_losses + total_transistor_cond_loss_matrix + \ + transformer_dto.p_combined_losses) + + # maximum loss indices + max_loss_all_index = np.unravel_index(total_loss_matrix.argmax(), np.shape(total_loss_matrix)) + # Calculate losses of circuit1 and 2 + max_loss_circuit_1_index = np.unravel_index(b1_transistor_cond_loss_matrix.argmax(), + np.shape(b1_transistor_cond_loss_matrix)) + max_loss_circuit_2_index = np.unravel_index(b2_transistor_cond_loss_matrix.argmax(), + np.shape(b2_transistor_cond_loss_matrix)) + + max_loss_inductor_index = np.unravel_index(inductance_loss_matrix.argmax(), np.shape(inductance_loss_matrix)) + max_loss_transformer_index = np.unravel_index(transformer_loss_matrix.argmax(), np.shape(transformer_loss_matrix)) + + r_th_ind_heat_sink = DctSummmaryProcessing.r_th_ind_heat_sink_A / inductor_dto.area_to_heat_sink + temperature_inductor_heat_sink_max_matrix = 125 - r_th_ind_heat_sink * inductance_loss_matrix + + r_th_xfmr_heat_sink = DctSummmaryProcessing.r_th_xfmr_heat_sink_A / transformer_dto.area_to_heat_sink + temperature_xfmr_heat_sink_max_matrix = 125 - r_th_xfmr_heat_sink * transformer_loss_matrix + + # maximum heat sink temperatures (minimum of all the maximum temperatures of single components) + t_min_matrix = np.minimum(circuit_heat_sink_max_1_matrix, circuit_heat_sink_max_2_matrix) + t_min_matrix = np.minimum(t_min_matrix, temperature_inductor_heat_sink_max_matrix) + t_min_matrix = np.minimum(t_min_matrix, temperature_xfmr_heat_sink_max_matrix) + t_min_matrix = np.minimum(t_min_matrix, DctSummmaryProcessing.heat_sink.t_hs_max) + + # maximum delta temperature over the heat sink + delta_t_max_heat_sink_matrix = t_min_matrix - DctSummmaryProcessing.heat_sink.t_ambient + + r_th_heat_sink_target_matrix = delta_t_max_heat_sink_matrix / total_loss_matrix + + r_th_target = r_th_heat_sink_target_matrix.min() + + data = { + # circuit + "circuit_number": circuit_number, + "circuit_mean_loss": np.mean(total_transistor_cond_loss_matrix), + "circuit_max_all_loss": total_transistor_cond_loss_matrix[max_loss_all_index], + "circuit_max_circuit_ib_loss": total_transistor_cond_loss_matrix[max_loss_circuit_1_index], + "circuit_max_circuit_ob_loss": total_transistor_cond_loss_matrix[max_loss_circuit_2_index], + "circuit_max_inductor_loss": total_transistor_cond_loss_matrix[max_loss_inductor_index], + "circuit_max_transformer_loss": total_transistor_cond_loss_matrix[max_loss_transformer_index], + "circuit_t_j_max_1": circuit_dto.input_config.transistor_dto_1.t_j_max_op, + "circuit_t_j_max_2": circuit_dto.input_config.transistor_dto_2.t_j_max_op, + "circuit_r_th_ib_jhs_1": circuit_r_th_1_jhs, + "circuit_r_th_ib_jhs_2": circuit_r_th_2_jhs, + "circuit_heat_sink_temperature_max_1": circuit_heat_sink_max_1_matrix[max_loss_circuit_1_index], + "circuit_heat_sink_temperature_max_2": circuit_heat_sink_max_2_matrix[max_loss_circuit_2_index], + "circuit_area": 4 * (copper_coin_area_1 + copper_coin_area_2), + # inductor + "inductor_study_name": inductor_study_name, + "inductor_number": inductor_number, + "inductor_volume": inductor_dto.volume, + "inductor_mean_loss": np.mean(inductance_loss_matrix), + "inductor_max_all_loss": inductance_loss_matrix[max_loss_all_index], + "inductor_max_circuit_ib_loss": inductance_loss_matrix[max_loss_circuit_1_index], + "inductor_max_circuit_ob_loss": inductance_loss_matrix[max_loss_circuit_2_index], + "inductor_max_inductor_loss": inductance_loss_matrix[max_loss_inductor_index], + "inductor_max_transformer_loss": inductance_loss_matrix[max_loss_transformer_index], + "inductor_t_max": 0, + "inductor_heat_sink_temperature_max": temperature_inductor_heat_sink_max_matrix[max_loss_inductor_index], + "inductor_area": inductor_dto.area_to_heat_sink, + # transformer + "transformer_study_name": stacked_transformer_study_name, + "transformer_number": stacked_transformer_number, + "transformer_volume": transformer_dto.volume, + "transformer_mean_loss": np.mean(transformer_dto.p_combined_losses), + "transformer_max_all_loss": transformer_loss_matrix[max_loss_all_index], + "transformer_max_circuit_ib_loss": transformer_loss_matrix[max_loss_circuit_1_index], + "transformer_max_circuit_ob_loss": transformer_loss_matrix[max_loss_circuit_2_index], + "transformer_max_inductor_loss": transformer_loss_matrix[max_loss_inductor_index], + "transformer_max_transformer_loss": transformer_loss_matrix[max_loss_transformer_index], + "transformer_t_max": 0, + "transformer_heat_sink_temperature_max": temperature_xfmr_heat_sink_max_matrix[max_loss_transformer_index], + "transformer_area": transformer_dto.area_to_heat_sink, + + # summary + "total_losses": total_loss_matrix[max_loss_all_index], + + # heat sink + "r_th_heat_sink": r_th_target + } + local_df = pd.DataFrame([data]) + + df = pd.concat([df, local_df], axis=0) + + # Calculate the total area as sum of circuit, inductor and transformer area df-comand is like vector sum v1[:]=v2[:]+v3[:]) + df["total_area"] = df["circuit_area"] + df["inductor_area"] + df["transformer_area"] + df["total_mean_loss"] = df["circuit_mean_loss"] + df["inductor_mean_loss"] + df["transformer_mean_loss"] + df["volume_wo_heat_sink"] = df["transformer_volume"] + df["inductor_volume"] + # Save results to file (ASA : later to store only on demand) + df.to_csv(f"{act_ginfo.heatsink_study_path}/result_df.csv") + + # return the data base + return df + + @staticmethod + def select_heatsink_configuration(act_ginfo: dct.GeneralInformation, act_heat_sink_study_name: str, act_df_for_hs: pd.DataFrame): + """Select the heatsink configuration from calculated heatsink pareto front. + + :param act_ginfo : General information about the study + :type act_ginfo : dct.GeneralInformation: + :param act_heat_sink_study_name : Heatsink study name + :type act_heat_sink_study_name : str + :param act_df_for_hs : dataframe with result information of the pareto front for heatsink selection + :type act_df_for_hs : pd.DataFrame + """ + # Variable declaration + + # load heat sink + hs_config_filepath = os.path.join(act_ginfo.heatsink_study_path, f"{act_heat_sink_study_name}.pkl") + hs_config = hct.Optimization.load_config(hs_config_filepath) + # Debug ASA Missing true simulations for remaining function + """ + df_hs = hct.Optimization.study_to_df(hs_config) + + # generate full summary as panda database operation + print(df_hs.loc[df_hs["values_1"] < 1]["values_0"].nsmallest(n=1).values[0]) + act_df_for_hs["heat_sink_volume"] = act_df_for_hs["r_th_heat_sink"].apply( + lambda r_th_max: df_hs.loc[df_hs["values_1"] < r_th_max]["values_0"].nsmallest(n=1).values[0] \ + if np.any(df_hs.loc[df_hs["values_1"] < r_th_max]["values_0"].nsmallest(n=1).values) else None) + + act_df_for_hs["total_volume"] = act_df_for_hs["transformer_volume"] + act_df_for_hs["inductor_volume"] + + act_df_for_hs["heat_sink_volume"] + + # save full summary + df_wo_hs.to_csv(f"{act_ginfo.heatsink_study_path}/df_summary.csv") + """ diff --git a/workspace/DabHeatsinkConf.toml b/workspace/DabHeatsinkConf.toml index a74d6bd..0f6b0ec 100644 --- a/workspace/DabHeatsinkConf.toml +++ b/workspace/DabHeatsinkConf.toml @@ -21,11 +21,10 @@ [ThermalResistanceData] # [tim_thickness, tim_conductivity] - transistor_b1_cooling = [1e-3,12] - transistor_b2_cooling = [1e-3,12] - inductor_cooling = [1e-3,12] - transformer_cooling = [1e-3,12] + transistor_b1_cooling = [1e-3,12.0] + transistor_b2_cooling = [1e-3,12.0] + inductor_cooling = [1e-3,12.0] + transformer_cooling = [1e-3,12.0] # [t_ambient, t_hs_max] in °C - heat_sink = [40, 90] - + heat_sink = [40.0, 90.0] \ No newline at end of file diff --git a/workspace/DabInductorConf.toml b/workspace/DabInductorConf.toml index c78e53b..e20c8ad 100644 --- a/workspace/DabInductorConf.toml +++ b/workspace/DabInductorConf.toml @@ -2,18 +2,18 @@ inductor_config_name = "inductor_01" [Designspace] - core_name_list=["PQ 50/50", "PQ 50/40", "PQ 40/40", "PQ 40/30", "PQ 35/35", "PQ 32/30", "PQ 32/20", "PQ 26/25", "PQ 26/20", "PQ 20/20", "PQ 20/16"] - material_name_list=["3C95"] - litz_wire_list=["1.5x105x0.1", "1.4x200x0.071", "1.1x60x0.1"] + core_name_list = ["PQ 50/50", "PQ 50/40", "PQ 40/40", "PQ 40/30", "PQ 35/35", "PQ 32/30", "PQ 32/20", "PQ 26/25", "PQ 26/20", "PQ 20/20", "PQ 20/16"] + material_name_list = ["3C95"] + litz_wire_list = ["1.5x105x0.1", "1.4x200x0.071", "1.1x60x0.1"] [InsulationData] - primary_to_primary=0.2e-3 - core_bot=1e-3 - core_top=1e-3 - core_right=1e-3 - core_left=1e-3 + primary_to_primary = 0.2e-3 + core_bot = 1e-3 + core_top = 1e-3 + core_right = 1e-3 + core_left = 1e-3 [FilterDistance] Delta = [0.1,0.1] # difference (x ,y) 1 = difference between minimal and maximal result value - Range = [[0.1,0.9],[0.1,0.9]] # Used range of X und Y-axe + Range = [[0.1,0.9],[0.1,0.9]] # Used range of X und Y-axe Deep = [0.01,0.01] # Max difference to real pareto points (X und Y-direction) diff --git a/workspace/progFlow.toml b/workspace/progFlow.toml index 11356ea..c0f25f2 100644 --- a/workspace/progFlow.toml +++ b/workspace/progFlow.toml @@ -10,7 +10,8 @@ Electrical_filtered = "No" # After Electrical filtered result calculation Inductor = "No" # After inductor paretofront calculations of for all correspondent electrical points Transformer = "No" # After transformer paretofront calculations of for all correspondent electrical points - Heatsink = "Pause" # After heatsink paretofront calculation + Heatsink = "No" # After heatsink paretofront calculation + Summary = "Pause" [condbreakpoints] # conditional breakpoints in case of bad definition array (only for experts and currently not implemented) Electrical = 100 # Number of trials with ZVS less than 70% @@ -20,22 +21,22 @@ [electrical] NumberOfTrials = 100 - ReCalculation = "continue" # (new,continue,skip) + ReCalculation = "skip" # (new,continue,skip) Subdirectory = "01_circuit" [inductor] NumberOfTrials = 100 - ReCalculation = "continue" # (new,continue,skip) + ReCalculation = "skip" # (new,continue,skip) Subdirectory = "02_inductor" [transformer] NumberOfTrials = 100 - ReCalculation = "continue" # (new,continue,skip) + ReCalculation = "skip" # (new,continue,skip) Subdirectory = "03_transformer" [heatsink] NumberOfTrials = 100 - ReCalculation = "continue" # (new,continue,skip) + ReCalculation = "skip" # (new,continue,skip) Subdirectory = "04_heat_sink" CircuitStudyNameFlag = "True" # True=Use circuit study name, False (or any word except 'True') = Do not use ciruit study name From 84eae47e5e7526ee097687a00d774d2905cfd683 Mon Sep 17 00:00:00 2001 From: SevenOfNinePE Date: Fri, 14 Mar 2025 23:32:16 +0100 Subject: [PATCH 3/8] Debug workflow after merge from main --- dct/dctmainctl.py | 24 ++++++++++++------------ dct/heat_sink_dtos.py | 2 +- dct/heatsink_sim.py | 2 +- dct/summary_processing.py | 8 +++++--- dct/toml_checker.py | 1 + examples/pareto_summary_wo_hs.py | 2 +- 6 files changed, 21 insertions(+), 18 deletions(-) diff --git a/dct/dctmainctl.py b/dct/dctmainctl.py index 4188ef5..288ba0d 100644 --- a/dct/dctmainctl.py +++ b/dct/dctmainctl.py @@ -49,20 +49,20 @@ def load_flow_control_conf_file(target_file: str) -> tuple[bool, tc.FlowControl] toml_file_exists = False # Separate filename and path - dirname = os.path.dirname(target_file) + dir_name = os.path.dirname(target_file) filename = os.path.basename(target_file) # check path - if os.path.exists(dirname) or dirname == "": + if os.path.exists(dir_name) or dir_name == "": # check filename if os.path.isfile(target_file): with open(target_file, "rb") as f: config = tomllib.load(f) toml_file_exists = True else: - print("File does not exists!") + print(f"File {target_file} does not exists!") else: - print("Path does not exists!") + print(f"Path {dir_name} does not exists!") return toml_file_exists, tc.FlowControl(**config) @@ -83,11 +83,11 @@ def load_conf_file(target_file: str, toml_data: dict) -> bool: toml_file_exists = False # Separate filename and path - dirname = os.path.dirname(target_file) + dir_name = os.path.dirname(target_file) filename = os.path.basename(target_file) # check path - if os.path.exists(dirname) or dirname == "": + if os.path.exists(dir_name) or dir_name == "": # check filename if os.path.isfile(target_file): new_dict_data = toml.load(target_file) @@ -96,9 +96,9 @@ def load_conf_file(target_file: str, toml_data: dict) -> bool: toml_data.update(new_dict_data) toml_file_exists = True else: - print("File does not exists!") + print(f"File does not exists!") else: - print("Path does not exists!") + print(f"Path {dir_name} does not exists!") return {toml_file_exists} @@ -394,12 +394,12 @@ def check_breakpoint(break_point_key: str, info: str): :type info: str """ # Check if breakpoint stops the program - if break_point_key == "Stop": + if break_point_key == "stop": print("Program stops cause by breakpoint at: '"+info+"'!") # stop program sys.exit() - elif break_point_key == "Pause": + elif break_point_key == "pause": # Information print("Active breakpoint at: '"+info+"'!\n") print("'C'=continue, 'S'=stop the program. Please enter your choice") @@ -498,7 +498,7 @@ def executeProgram(workspace_path: str): # Assemble pathname datapath = os.path.join(config_program_flow.general.project_directory, - config_program_flow.general.subdirectory, + config_program_flow.circuit.subdirectory, config_program_flow.general.study_name) # Check, if data are available (skip case) @@ -667,7 +667,7 @@ def executeProgram(workspace_path: str): spro.select_heatsink_configuration(ginfo, config_heat_sink["HeatsinkConfigName"]["heatsink_config_name"], s_df) # Check breakpoint - DctMainCtl.check_breakpoint(config_program_flow["breakpoints"]["Summary"], "Calculation is complete") + DctMainCtl.check_breakpoint(config_program_flow.breakpoints.summary, "Calculation is complete") # Join process if necessary esim.join_process() diff --git a/dct/heat_sink_dtos.py b/dct/heat_sink_dtos.py index 355609e..5768a99 100644 --- a/dct/heat_sink_dtos.py +++ b/dct/heat_sink_dtos.py @@ -5,7 +5,7 @@ @dataclasses.dataclass -class HeatSink: +class HeatSinkTemp: """Fix parameters for the heat sink cooling.""" t_ambient: float diff --git a/dct/heatsink_sim.py b/dct/heatsink_sim.py index 092b565..76034f6 100644 --- a/dct/heatsink_sim.py +++ b/dct/heatsink_sim.py @@ -43,7 +43,7 @@ def init_configuration(act_hct_config_name: str, act_ginfo: dct.GeneralInformati # Check if path exists # check path if not os.path.exists(act_design_space_path): - print("Path does not exists!") + print(f"Path {act_design_space_path} does not exists!") # Return with false return ret_val diff --git a/dct/summary_processing.py b/dct/summary_processing.py index 58cd07f..b88223f 100644 --- a/dct/summary_processing.py +++ b/dct/summary_processing.py @@ -85,11 +85,13 @@ def init_thermal_configuration(act_thermal_configuration_dict: dict) -> bool: successful_init = False # Heat sink parameter: List [t_ambient, t_hs_max] - DctSummmaryProcessing.heat_sink = dct.HeatSink( - t_ambient=act_thermal_configuration_dict["heat_sink"][0], - t_hs_max=act_thermal_configuration_dict["heat_sink"][1] + DctSummmaryProcessing.heat_sink = dct.HeatSinkTemp( + t_ambient=act_thermal_configuration_dict["heat_sink"][0], + t_hs_max=act_thermal_configuration_dict["heat_sink"][1] ) + + return successful_init @staticmethod diff --git a/dct/toml_checker.py b/dct/toml_checker.py index 10719d0..baf9e83 100644 --- a/dct/toml_checker.py +++ b/dct/toml_checker.py @@ -19,6 +19,7 @@ class Breakpoints(BaseModel): inductor: Literal['no', 'pause', 'stop'] transformer: Literal['no', 'pause', 'stop'] heat_sink: Literal['no', 'pause', 'stop'] + summary: Literal['no', 'pause', 'stop'] class CondBreakpoints(BaseModel): """Flow control conditional breakpoints.""" diff --git a/examples/pareto_summary_wo_hs.py b/examples/pareto_summary_wo_hs.py index 70e3eab..345838a 100644 --- a/examples/pareto_summary_wo_hs.py +++ b/examples/pareto_summary_wo_hs.py @@ -50,7 +50,7 @@ ) -heat_sink = dct.HeatSink( +heat_sink = dct.HeatSinkTemp( t_ambient=40, t_hs_max=90, ) From d6a6dac77b2677a7db4dc5a79db3c8db1682881d Mon Sep 17 00:00:00 2001 From: SevenOfNinePE Date: Mon, 7 Apr 2025 08:40:39 +0200 Subject: [PATCH 4/8] Merge with main (Part1) --- dct/dctmainctl.py | 8 +++--- dct/heatsink_sim.py | 2 +- dct/server_ctl.py | 51 ++++++++++++++++++++++++--------------- dct/summary_processing.py | 6 ++--- pyproject.toml | 11 +++------ workspace/progFlow.toml | 17 +++++++------ 6 files changed, 53 insertions(+), 42 deletions(-) diff --git a/dct/dctmainctl.py b/dct/dctmainctl.py index 288ba0d..1c1c931 100644 --- a/dct/dctmainctl.py +++ b/dct/dctmainctl.py @@ -20,8 +20,10 @@ from heatsink_sim import HeatSinkSim as Heatsinksimclass # Import server control class import summary_processing as SumProcessing -# Import server control class +# Import toml-checker import toml_checker as tc +# import server contol class +from server_ctl import Dct_server as srv_ctl # logging.basicConfig(format='%(levelname)s,%(asctime)s:%(message)s', encoding='utf-8') # logging.getLogger('pygeckocircuits2').setLevel(logging.DEBUG) @@ -561,7 +563,7 @@ def executeProgram(workspace_path: str): # -- Start server -------------------------------------------------------------------------------------------- # Debug: Server switched off - # srv_ctl.start_dct_server(histogram_data,False) + srv_ctl.start_dct_server(histogram_data,False) # -- Start simulation ---------------------------------------------------------------------------------------- @@ -673,7 +675,7 @@ def executeProgram(workspace_path: str): esim.join_process() # Shut down server # Debug: Server switched off - # srv_ctl.stop_dct_server() + srv_ctl.stop_dct_server() pass diff --git a/dct/heatsink_sim.py b/dct/heatsink_sim.py index 76034f6..cfc2d10 100644 --- a/dct/heatsink_sim.py +++ b/dct/heatsink_sim.py @@ -104,7 +104,7 @@ def _simulation(act_hct_config: hopt.OptimizationParameters, act_ginfo: dct.Gene # Check number of trials if target_number_trials > 0: - hopt.Optimization.start_proceed_study(config=act_hct_config, number_trials=1000) + hopt.Optimization.start_proceed_study(config=act_hct_config, number_trials=target_number_trials) else: print(f"Target number of trials = {target_number_trials} which are less equal 0!. No simulation is performed") diff --git a/dct/server_ctl.py b/dct/server_ctl.py index 297d6dd..b9b6290 100644 --- a/dct/server_ctl.py +++ b/dct/server_ctl.py @@ -10,17 +10,23 @@ from fastapi.templating import Jinja2Templates from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles +import optuna +from optuna.visualization import plot_pareto_front # own libraries +# Debug server +import logging +logging.basicConfig(level=logging.DEBUG) + class Dct_server: """server class to supervise the simulation.""" # Variable declaration # FastAPI-Server definieren app = FastAPI() - templates = Jinja2Templates(directory="htmltemplates") + templates = Jinja2Templates(directory="/home/andreas/Workspace/Projekt/dab_computational_toolkit/dct/htmltemplates") status_message = "Warte auf Knopfdruck" # Initialer Text # Serverobject srv_obj = None @@ -103,7 +109,7 @@ def _run_server(shared_histogram): # Overtake the shared memory variable Dct_server.shared_histogram = shared_histogram # Start the server (blocking call) - config = uvicorn.Config(Dct_server.app, host="127.0.0.1", port=8004, log_level="info") + config = uvicorn.Config(Dct_server.app, host="127.0.0.1", port=8005, log_level="info") Dct_server.srv_obj = uvicorn.Server(config) # Create thread for the server and start it Dct_server.srv_thread = threading.Thread(target=Dct_server.dct_server_thread) @@ -133,6 +139,20 @@ def dct_server_thread(): # Start the server in a blocking call Dct_server.srv_obj.run() + @staticmethod + def LoadActualParetofront(): + # Verbinde dich mit der bestehenden Optuna-Datenbank + study = optuna.load_study(study_name="circuit_01", storage="sqlite:////home/andreas/Workspace/Projekt/dab_computational_toolkit/workspace/2025-01-31_example/01_circuit/circuit_01/circuit_01.sqlite3") + + # Erzeuge die aktuelle Paretofront + fig = plot_pareto_front(study) + + # Speichere die HTML-Darstellung des Plots in einer Variablen + html_variable = fig.to_html(full_html=False) + + return html_variable + + @app.get("/", response_class=HTMLResponse) async def main_page(request: Request, action: str = None): """Provide the answer on client requests. @@ -153,9 +173,10 @@ async def main_page(request: Request, action: str = None): Dct_server.status_message = "Stoppt den Server und die Simulation (wenn prog_exit_flag==true)" Dct_server.req_stop.value = 1 - return Dct_server.templates.TemplateResponse("html_main.html", {"request": request, "textvariable": Dct_server.status_message}) + return Dct_server.templates.TemplateResponse("html_main.html",{"request": request, "textvariable": Dct_server.status_message}) + - @app.get("/histogram") + @app.get("/histogram", response_class=HTMLResponse) def get_histogram(request: Request): """Provide the answer on client histogram request. @@ -165,22 +186,12 @@ def get_histogram(request: Request): :return: Html- page based on html-template with Histogram information :rtype: _TemplateResponse """ - """ - plt.figure() - plt.bar(range(25), Dct_server.shared_histogram[:25], color='blue', edgecolor='black') - plt.xlabel("Schritte bis zur 5") - plt.ylabel("Häufigkeit") - plt.title("Histogramm der Wartezeiten bis zur 5") - - buf = io.BytesIO() - plt.savefig(buf, format='png') - plt.close() - buf.seek(0) - - img_str = base64.b64encode(buf.read()).decode('utf-8') - """ + + # Return the html-page with updated image data + html_page = Dct_server.LoadActualParetofront() + return HTMLResponse(content=html_page) # Return the html-page with updated image data # return Dct_server.templates.TemplateResponse( "html_histogram.html", {"request": request, "imagedata": img_str}) - imagepath = "StyleSheets/Dummytrafo.png" - return Dct_server.templates.TemplateResponse("html_histogram.html", {"request": request, "image_path": imagepath}) + # imagepath = "StyleSheets/Dummytrafo.png" + # return Dct_server.templates.TemplateResponse("html_histogram.html", {"request": request, "image_path": imagepath}) diff --git a/dct/summary_processing.py b/dct/summary_processing.py index b88223f..65b493b 100644 --- a/dct/summary_processing.py +++ b/dct/summary_processing.py @@ -369,7 +369,8 @@ def select_heatsink_configuration(act_ginfo: dct.GeneralInformation, act_heat_si hs_config_filepath = os.path.join(act_ginfo.heatsink_study_path, f"{act_heat_sink_study_name}.pkl") hs_config = hct.Optimization.load_config(hs_config_filepath) # Debug ASA Missing true simulations for remaining function - """ + + hs_config.heat_sink_optimization_directory="/home/andreas/Workspace/Projekt/dab_computational_toolkit/workspace/2025-01-31_example/04_heat_sink/heatsink_01" df_hs = hct.Optimization.study_to_df(hs_config) # generate full summary as panda database operation @@ -382,5 +383,4 @@ def select_heatsink_configuration(act_ginfo: dct.GeneralInformation, act_heat_si + act_df_for_hs["heat_sink_volume"] # save full summary - df_wo_hs.to_csv(f"{act_ginfo.heatsink_study_path}/df_summary.csv") - """ + # df_wo_hs.to_csv(f"{act_ginfo.heatsink_study_path}/df_summary.csv") diff --git a/pyproject.toml b/pyproject.toml index 0f034d2..19c4ca3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,9 +2,9 @@ name = "dct" version = "0.1.0" authors = [ - { name = "UPB-LEA" }, + { name = "UniSiegen+UPB-LEA" }, ] -description = "Power electroincs DAB converter optimization." +description = "Client for power electroincs DAB converter optimization." readme = "README.rst" requires-python = ">=3.11" classifiers = [ @@ -19,8 +19,6 @@ dependencies = { file = ["requirements.txt"] } optional-dependencies = { dev = { file = ["requirements-dev.txt"] } } [project.urls] -Homepage = "https://github.com/upb-lea/dab_computational_toolkit" -Issues = "https://github.com/upb-lea/dab_computational_toolkit/issues" [build-system] requires = ["hatchling", "hatch-requirements-txt"] @@ -30,12 +28,11 @@ build-backend = "hatchling.build" files = ["requirements.txt"] [tool.hatch.build.targets.wheel] -packages = ["dct/"] +packages = [""] [tool.hatch.build.targets.sdist] include = [ - "dct/*.py", - "tests", + "*.py", "requirements.txt" ] diff --git a/workspace/progFlow.toml b/workspace/progFlow.toml index 35076e6..7f4debb 100644 --- a/workspace/progFlow.toml +++ b/workspace/progFlow.toml @@ -5,12 +5,13 @@ relative_flag = 1 [breakpoints] - # possible values: No/Pause/Stop - circuit_pareto = "no" # After Electrical paretofront calculation + # possible values: no/pause/stop + circuit_pareto = "pause" # After Electrical paretofront calculation circuit_filtered = "no" # After Electrical filtered result calculation inductor = "no" # After inductor paretofront calculations of for all correspondent electrical points transformer = "no" # After transformer paretofront calculations of for all correspondent electrical points - heat_sink = "pause" # After heatsink paretofront calculation + heat_sink = "no" # After heatsink paretofront calculation + summary = "pause" # After heatsink paretofront calculation [conditional_breakpoints] # conditional breakpoints in case of bad definition array (only for experts and currently not implemented) circuit = 100 # Number of trials with ZVS less than 70% @@ -19,23 +20,23 @@ heat_sink = 1000 # Number of trials which exceed a limit value? [circuit] - number_of_trials = 100 + number_of_trials = 500 re_calculation = "continue" # (new,continue,skip) subdirectory = "01_circuit" [inductor] number_of_trials = 100 - re_calculation = "continue" # (new,continue,skip) + re_calculation = "skip" # (new,continue,skip) subdirectory = "02_inductor" [transformer] number_of_trials = 100 - re_calculation = "continue" # (new,continue,skip) + re_calculation = "skip" # (new,continue,skip) subdirectory = "03_transformer" [heat_sink] - number_of_trials = 100 - re_calculation = "continue" # (new,continue,skip) + number_of_trials = 15000 + re_calculation = "new" # (new,continue,skip) subdirectory = "04_heat_sink" circuit_study_name_flag = "True" # True=Use circuit study name, False (or any word except 'True') = Do not use ciruit study name From d0c20f2fd789676c630dddbe4d5f2259f077df58 Mon Sep 17 00:00:00 2001 From: SevenOfNinePE Date: Wed, 9 Apr 2025 10:35:07 +0200 Subject: [PATCH 5/8] Fix bugs in workflow in summary calculation Merged with main branch --- dct/dctmainctl.py | 206 ++++++++++++------------------ dct/heat_sink_optimization.py | 1 + dct/inductor_optimization.py | 16 ++- dct/pareto_dtos.py | 5 +- dct/summary_processing.py | 74 ++++++----- dct/toml_checker.py | 70 +++++++++- dct/transformer_optimization.py | 112 ++++++++-------- workspace/DabHeatSinkConf.toml | 1 - workspace/DabTransformerConf.toml | 26 ++-- workspace/progFlow.toml | 10 +- 10 files changed, 290 insertions(+), 231 deletions(-) diff --git a/dct/dctmainctl.py b/dct/dctmainctl.py index ccb3b62..897c94a 100644 --- a/dct/dctmainctl.py +++ b/dct/dctmainctl.py @@ -4,6 +4,8 @@ import sys import multiprocessing import tomllib +from os.path import abspath + import toml # 3rd party libraries @@ -18,9 +20,12 @@ import transformer_optimization as Transfsimclass # import heatsink_sim import heat_sink_optimization as Heatsinksimclass +# Import server control class +import summary_processing as SumProcessing import toml_checker as tc import pareto_dtos as p_dtos from dct import CircuitOptimization +from server_ctl import Dct_server as srv_ctl # logging.basicConfig(format='%(levelname)s,%(asctime)s:%(message)s', encoding='utf-8') # logging.getLogger('pygeckocircuits2').setLevel(logging.DEBUG) @@ -131,34 +136,40 @@ def user_input_break_point(break_point_key: str, info: str): pass @staticmethod - def init_general_info(act_ginfo: dct.GeneralInformation, act_config_program_flow: tc.FlowControl): + def init_general_info(act_config_program_flow: tc.FlowControl)-> dct.GeneralInformation: """ Init the general information variable. - :param act_ginfo: reference to the general information variable - :type act_ginfo: dct.GeneralInformation :param act_config_program_flow: toml data of the program flow :type act_config_program_flow: dict + :return: general information variable containing general information for the optimization + :rtype: dct.GeneralInformation """ - # Read the current directory name - abs_path = os.getcwd() - # Store project directory and study name - act_ginfo.project_directory = act_config_program_flow.general.project_directory - act_ginfo.circuit_study_name = act_config_program_flow.configuration_data_files.circuit_configuration_file.replace(".toml", "") - # Create path names - act_ginfo.circuit_study_path = os.path.join(abs_path, act_ginfo.project_directory, - DctMainCtl.const_circuit_folder, act_ginfo.circuit_study_name) - act_ginfo.inductor_study_path = os.path.join(abs_path, act_ginfo.project_directory, - DctMainCtl.const_inductor_folder, act_ginfo.circuit_study_name) - act_ginfo.transformer_study_path = os.path.join(abs_path, act_ginfo.project_directory, - DctMainCtl.const_transformer_folder, act_ginfo.circuit_study_name) - # Check, if heat sink study name uses the circuit name - if act_config_program_flow.heat_sink.circuit_study_name_flag == "True": - act_ginfo.heat_sink_study_path = os.path.join(abs_path, act_ginfo.project_directory, - DctMainCtl.const_heat_sink_folder, act_ginfo.circuit_study_name) - else: - act_ginfo.heat_sink_study_path = os.path.join(abs_path, act_ginfo.project_directory, - DctMainCtl.const_heat_sink_folder) + # Set projektdirectory + act_project_directory = act_config_program_flow.general.project_directory + + # Setup variable by set study names + r_ginfo = dct.GeneralInformation( + project_directory=act_project_directory, + circuit_study_name= + act_config_program_flow.configuration_data_files.circuit_configuration_file.replace(".toml", ""), + filtered_list_id=[], + inductor_study_name= + act_config_program_flow.configuration_data_files.inductor_configuration_file.replace(".toml", ""), + transformer_study_name= + act_config_program_flow.configuration_data_files.transformer_configuration_file.replace(".toml", ""), + heat_sink_study_name= + act_config_program_flow.configuration_data_files.heat_sink_configuration_file.replace(".toml", ""), + # Set remaining elements with dummy names + circuit_study_path=os.path.join(act_project_directory, "01_circuit"), + inductor_study_path=os.path.join(act_project_directory, "02_inductor"), + transformer_study_path=os.path.join(act_project_directory, "03_transformer"), + heat_sink_study_path=os.path.join(act_project_directory, "04_heat_sink") + ) + + # Return the result + return r_ginfo + @staticmethod def check_study_data(study_path: str, study_name: str) -> bool: @@ -213,7 +224,8 @@ def load_inductor_config(act_ginfo: dct.GeneralInformation, toml_inductor: dct.T return act_isim.init_configuration(toml_inductor, toml_prog_flow, act_ginfo) @staticmethod - def load_transformer_config(act_ginfo: dct.GeneralInformation, act_config_transformer: dict, act_tsim: Transfsimclass.TransformerOptimization) -> bool: + def load_transformer_config(act_ginfo: dct.GeneralInformation, toml_transformer: dct.TomlInductor, toml_prog_flow: dct.FlowControl, + act_tsim: Transfsimclass.TransformerOptimization) -> bool: """ Load and initialize the transformer optimization configuration. @@ -228,65 +240,9 @@ def load_transformer_config(act_ginfo: dct.GeneralInformation, act_config_transf """ # Variable initialisation - # design space - designspace_dict = {"core_name_list": act_config_transformer["Designspace"]["core_name_list"], - "material_name_list": act_config_transformer["Designspace"]["material_name_list"], - "core_inner_diameter_min_max_list": act_config_transformer["Designspace"]["core_inner_diameter_min_max_list"], - "window_w_min_max_list": act_config_transformer["Designspace"]["window_w_min_max_list"], - "window_h_bot_min_max_list": act_config_transformer["Designspace"]["window_h_bot_min_max_list"], - "primary_litz_wire_list": act_config_transformer["Designspace"]["primary_litz_wire_list"], - "secondary_litz_wire_list": act_config_transformer["Designspace"]["secondary_litz_wire_list"]} - - # Transformer data - transformer_data_dict = {"max_transformer_total_height": act_config_transformer["TransformerData"]["max_transformer_total_height"], - "max_core_volume": act_config_transformer["TransformerData"]["max_core_volume"], - "n_p_top_min_max_list": act_config_transformer["TransformerData"]["n_p_top_min_max_list"], - "n_p_bot_min_max_list": act_config_transformer["TransformerData"]["n_p_bot_min_max_list"], - "iso_window_top_core_top": act_config_transformer["TransformerData"]["iso_window_top_core_top"], - "iso_window_top_core_bot": act_config_transformer["TransformerData"]["iso_window_top_core_bot"], - "iso_window_top_core_left": act_config_transformer["TransformerData"]["iso_window_top_core_left"], - "iso_window_top_core_right": act_config_transformer["TransformerData"]["iso_window_top_core_right"], - "iso_window_bot_core_top": act_config_transformer["TransformerData"]["iso_window_bot_core_top"], - "iso_window_bot_core_bot": act_config_transformer["TransformerData"]["iso_window_bot_core_bot"], - "iso_window_bot_core_left": act_config_transformer["TransformerData"]["iso_window_bot_core_left"], - "iso_window_bot_core_right": act_config_transformer["TransformerData"]["iso_window_bot_core_right"], - "iso_primary_to_primary": act_config_transformer["TransformerData"]["iso_primary_to_primary"], - "iso_secondary_to_secondary": act_config_transformer["TransformerData"]["iso_secondary_to_secondary"], - "iso_primary_to_secondary": act_config_transformer["TransformerData"]["iso_primary_to_secondary"], - "fft_filter_value_factor": act_config_transformer["TransformerData"]["fft_filter_value_factor"], - "mesh_accuracy": act_config_transformer["TransformerData"]["mesh_accuracy"]} - # Initialize inductor optimization and return, if it was successful (true) - return act_tsim.init_configuration(act_config_transformer["TransformerConfigName"]["transformer_config_name"], - act_ginfo, designspace_dict, transformer_data_dict) - - @staticmethod # (ginfo, config_heat_sink, spro) - def init_summary_thermal_data(act_ginfo: dct.GeneralInformation, act_config_heat_sink: dict, act_spro: SumProcessing.DctSummmaryProcessing) -> bool: - """ - Initialize thermal data for summary processing. - - :param act_ginfo : General information about the study - :type act_ginfo : dct.GeneralInformation: - :param act_config_heat_sink: actual heat sink configuration information - :type act_config_heat_sink: dict: heat sink with the necessary configuration parameter - :param act_spro: summary processing object reference - :type act_spro: SumProcessing.DctSummmaryProcessing - :return: True, if the configuration is successful - :rtype: bool - """ - # Variable initialisation + return act_tsim.init_configuration(toml_transformer, toml_prog_flow, act_ginfo) - # Get heat sink dimension data - thermal_configuration_dict = { - "transistor_b1_cooling": act_config_heat_sink["ThermalResistanceData"]["transistor_b1_cooling"], - "transistor_b2_cooling": act_config_heat_sink["ThermalResistanceData"]["transistor_b2_cooling"], - "inductor_cooling": act_config_heat_sink["ThermalResistanceData"]["inductor_cooling"], - "transformer_cooling": act_config_heat_sink["ThermalResistanceData"]["transformer_cooling"], - "heat_sink": act_config_heat_sink["ThermalResistanceData"]["heat_sink"], - } - - # Initialize inductor optimization and return, if it was successful (true) - return act_spro.init_thermal_configuration(thermal_configuration_dict) @staticmethod def check_breakpoint(break_point_key: str, info: str): @@ -415,6 +371,13 @@ def executeProgram(workspace_path: str): if not flow_control_loaded: raise ValueError("Program flow toml file does not exist.") + # Add absolute path to project data path + workspace_path=abspath(workspace_path) + toml_prog_flow.general.project_directory=os.path.join(workspace_path,toml_prog_flow.general.project_directory) + + # Init general information: Project directory, study names and corresponding paths + ginfo = DctMainCtl.init_general_info(toml_prog_flow) + # Init circuit configuration circuit_loaded, dict_circuit = DctMainCtl.load_conf_file(toml_prog_flow.configuration_data_files.circuit_configuration_file) toml_circuit = tc.TomlCircuitParetoDabDesign(**dict_circuit) @@ -426,16 +389,13 @@ def executeProgram(workspace_path: str): circuit_study_name = toml_prog_flow.configuration_data_files.circuit_configuration_file.replace(".toml", "") - # Add project directory and study name - DctMainCtl.init_general_info(ginfo, toml_prog_flow) - # Check, if electrical optimization is to skip if toml_prog_flow.circuit.re_calculation == "skip": # Check, if data are available (skip case) if not DctMainCtl.check_study_data(ginfo.circuit_study_path, ginfo.circuit_study_name): raise ValueError(f"Study {ginfo.circuit_study_name} in path {ginfo.circuit_study_path} does not exist. No sqlite3-database found!") # Check if filtered results folder exists - datapath = os.path.join(ginfo.circuit_study_path, "filtered_results") + datapath = os.path.join(ginfo.circuit_study_path,ginfo.circuit_study_name,"filtered_results") if os.path.exists(datapath): # Set Flag to false filtered_resultFlag = True @@ -444,13 +404,8 @@ def executeProgram(workspace_path: str): if os.path.isfile(os.path.join(datapath, pareto_entry)): ginfo.filtered_list_id.append(os.path.splitext(pareto_entry)[0]) - # Assemble pathname - datapath = os.path.join(toml_prog_flow.general.project_directory, - toml_prog_flow.circuit.subdirectory, - circuit_study_name) - # Check, if data are available (skip case) - if not DctMainCtl.check_study_data(datapath, circuit_study_name): + if not DctMainCtl.check_study_data(ginfo.circuit_study_path, circuit_study_name): raise ValueError( f"Study {toml_prog_flow.general.study_name} in path {datapath} " "does not exist. No sqlite3-database found!" @@ -461,8 +416,6 @@ def executeProgram(workspace_path: str): inductor_loaded, inductor_dict = DctMainCtl.load_conf_file(toml_prog_flow.configuration_data_files.inductor_configuration_file) toml_inductor = dct.TomlInductor(**inductor_dict) - inductor_study_name = toml_prog_flow.configuration_data_files.inductor_configuration_file.replace(".toml", "") - if not inductor_loaded: raise ValueError(f"Inductor configuration file: {target_file} does not exist.") @@ -472,18 +425,20 @@ def executeProgram(workspace_path: str): for id_entry in ginfo.filtered_list_id: # Assemble pathname - datapath = os.path.join(toml_prog_flow.general.project_directory, - toml_prog_flow.inductor.subdirectory, - circuit_study_name, + datapath = os.path.join(ginfo.inductor_study_path, + ginfo.circuit_study_name, id_entry, - ) + ginfo.inductor_study_name) # Check, if data are available (skip case) - if not DctMainCtl.check_study_data(datapath, inductor_study_name): - raise ValueError(f"Study {toml_prog_flow.general.study_name} in path {datapath} does not exist. No sqlite3-database found!") + if not DctMainCtl.check_study_data(datapath, ginfo.inductor_study_name): + raise ValueError(f"Study {ginfo.inductor_study_name} in path {datapath} does not exist. No sqlite3-database found!") # Load the transformer-configuration parameter target_file = toml_prog_flow.configuration_data_files.transformer_configuration_file - if not DctMainCtl.load_conf_file_deprecated(target_file, config_transformer): + transformer_loaded, transformer_dict = DctMainCtl.load_conf_file(toml_prog_flow.configuration_data_files.transformer_configuration_file) + toml_transformer = dct.TomlTransformer(**transformer_dict) + + if not transformer_loaded: raise ValueError(f"Transformer configuration file: {target_file} does not exist.") # Check, if transformer optimization is to skip @@ -491,14 +446,13 @@ def executeProgram(workspace_path: str): # For loop to check, if all filtered values are available for id_entry in ginfo.filtered_list_id: # Assemble pathname - datapath = os.path.join(toml_prog_flow.general.project_directory, - toml_prog_flow.transformer.subdirectory, - circuit_study_name, + datapath = os.path.join(ginfo.transformer_study_path, + ginfo.circuit_study_name, id_entry, - config_transformer["TransformerConfigName"]["transformer_config_name"]) + ginfo.transformer_study_name) # Check, if data are available (skip case) - if not DctMainCtl.check_study_data(datapath, "transformer_01"): - raise ValueError(f"Study {toml_prog_flow.general.study_name} in path {datapath} does not exist. No sqlite3-database found!") + if not DctMainCtl.check_study_data(datapath, ginfo.transformer_study_name): + raise ValueError(f"Study {ginfo.transformer_study_name} in path {datapath} does not exist. No sqlite3-database found!") # Load the heat sink-configuration parameter target_file = toml_prog_flow.configuration_data_files.heat_sink_configuration_file @@ -508,14 +462,16 @@ def executeProgram(workspace_path: str): # Check, if heat sink optimization is to skip if toml_prog_flow.heat_sink.re_calculation == "skip": # Assemble pathname - datapath = os.path.join(ginfo.heat_sink_study_path, config_heat_sink["HeatsinkConfigName"]["heatsink_config_name"]) + datapath = os.path.join(ginfo.heat_sink_study_path, + ginfo.heat_sink_study_name) # Check, if data are available (skip case) - if not DctMainCtl.check_study_data(datapath, "heatsink_01"): - raise ValueError(f"Study {toml_prog_flow.general.study_name} in path {datapath} does not exist. No sqlite3-database found!") + if not DctMainCtl.check_study_data(datapath, ginfo.heat_sink_study_name): + raise ValueError( + f"Study {ginfo.heat_sink_study_name} in path {datapath} does not exist. No sqlite3-database found!") # -- Start server -------------------------------------------------------------------------------------------- # Debug: Server switched off - srv_ctl.start_dct_server(histogram_data,False) + # srv_ctl.start_dct_server(histogram_data,False) # -- Start simulation ---------------------------------------------------------------------------------------- @@ -549,7 +505,7 @@ def executeProgram(workspace_path: str): # Calculate the filtered results CircuitOptimization.filter_study_results(dab_config=config_circuit) # Get filtered result path - datapath = os.path.join(ginfo.circuit_study_path, "filtered_results") + datapath = os.path.join(ginfo.circuit_study_path,ginfo.circuit_study_name,"filtered_results") # Add filtered result list for pareto_entry in os.listdir(datapath): if os.path.isfile(os.path.join(datapath, pareto_entry)): @@ -558,7 +514,7 @@ def executeProgram(workspace_path: str): # Check breakpoint DctMainCtl.check_breakpoint(toml_prog_flow.breakpoints.circuit_filtered, "Filtered value of electric Pareto front calculated") - # Check, if inductor optimization is not to skip + # Check, if inductor optimization is not skipped if not toml_prog_flow.inductor.re_calculation == "skip": # Load initialisation data of inductor simulation and initialize if not DctMainCtl.load_inductor_config(ginfo, toml_inductor, toml_prog_flow, isim): @@ -572,15 +528,14 @@ def executeProgram(workspace_path: str): new_study_flag = False # Start simulation ASA: Filter_factor to correct - isim.simulation_handler(ginfo, toml_prog_flow.inductor.number_of_trials, 1.0, new_study_flag) - + isim.simulation_handler(ginfo, toml_prog_flow.inductor.number_of_trials, 1.0, new_study_flag, True) # Check breakpoint DctMainCtl.check_breakpoint(toml_prog_flow.breakpoints.inductor, "Inductor Pareto front calculated") - # Check, if transformer optimization is not to skip + # Check, if transformer optimization is not skipped if not toml_prog_flow.transformer.re_calculation == "skip": # Load initialisation data of transformer simulation and initialize - if not DctMainCtl.load_transformer_config(ginfo, config_transformer, tsim): + if not DctMainCtl.load_transformer_config(ginfo, toml_transformer, toml_prog_flow, tsim): raise ValueError("Transformer configuration not initialized!") # Check, if old study is to delete, if available if toml_prog_flow.transformer.re_calculation == "new": @@ -591,12 +546,12 @@ def executeProgram(workspace_path: str): new_study_flag = False # Start simulation ASA: Filter_factor to correct - tsim.simulation_handler(ginfo, toml_prog_flow.transformer.number_of_trials, 1.0, new_study_flag) + tsim.simulation_handler(ginfo, toml_prog_flow.transformer.number_of_trials, 1.0, new_study_flag, True) # Check breakpoint DctMainCtl.check_breakpoint(toml_prog_flow.breakpoints.transformer, "Transformer Pareto front calculated") - # Check, if heat sink optimization is to skip + # Check, if heat sink optimization is not skipped if not toml_prog_flow.heat_sink.re_calculation == "skip": heat_sink_loaded, heat_sink_dict = DctMainCtl.load_conf_file(toml_prog_flow.configuration_data_files.heat_sink_configuration_file) @@ -623,24 +578,27 @@ def executeProgram(workspace_path: str): DctMainCtl.check_breakpoint(toml_prog_flow.breakpoints.heat_sink, "Heat sink Pareto front calculated") # Initialisation thermal data - if not DctMainCtl.init_summary_thermal_data(ginfo, config_heat_sink, spro): + heat_sink_loaded, heat_sink_dict = DctMainCtl.load_conf_file(toml_prog_flow.configuration_data_files.heat_sink_configuration_file) + toml_heat_sink = dct.TomlHeatSink(**heat_sink_dict) + + + if not spro.init_thermal_configuration(toml_heat_sink.ThermalResistanceData): raise ValueError("Thermal data configuration not initialized!") # Create list of inductor and transformer study (ASA: Currently not implemented in configuration files) - inductor_study_names = [config_inductor["InductorConfigName"]["inductor_config_name"]] - stacked_transformer_study_names = [config_transformer["TransformerConfigName"]["transformer_config_name"]] + inductor_study_names = [ginfo.inductor_study_name] + stacked_transformer_study_names = [ginfo.transformer_study_name] # Start summary processing by generating the dataframe from calculated simmulation results s_df = spro.generate_result_database(ginfo, inductor_study_names, stacked_transformer_study_names) # Select the needed heatsink configuration - spro.select_heatsink_configuration(ginfo, config_heat_sink["HeatsinkConfigName"]["heatsink_config_name"], s_df) - + spro.select_heatsink_configuration(ginfo, s_df) # Check breakpoint - DctMainCtl.check_breakpoint(config_program_flow.breakpoints.summary, "Calculation is complete") + DctMainCtl.check_breakpoint(toml_prog_flow.breakpoints.summary, "Calculation is complete") # Join process if necessary ASA to check # esim.join_process() # Shut down server # Debug: Server switched off - srv_ctl.stop_dct_server() + # srv_ctl.stop_dct_server() pass diff --git a/dct/heat_sink_optimization.py b/dct/heat_sink_optimization.py index 626b2fa..a822e14 100644 --- a/dct/heat_sink_optimization.py +++ b/dct/heat_sink_optimization.py @@ -134,6 +134,7 @@ def calculate_r_th_tim(copper_coin_bot_area: float, transistor_cooling: Transist # Simulation handler. Later the simulation handler starts a process per list entry. @staticmethod def _simulation(act_hct_config: hct.OptimizationParameters, act_ginfo: dct.GeneralInformation, + target_number_trials: int, re_simulate: bool, debug: bool): """ Perform the simulation. diff --git a/dct/inductor_optimization.py b/dct/inductor_optimization.py index f101575..a782187 100644 --- a/dct/inductor_optimization.py +++ b/dct/inductor_optimization.py @@ -80,7 +80,7 @@ def init_configuration(toml_inductor: dct.TomlInductor, toml_prog_flow: dct.Flow # Create the io_config_list for all trials for circuit_trial_number in act_ginfo.filtered_list_id: - circuit_filepath = os.path.join(act_ginfo.circuit_study_path, "filtered_results", f"{circuit_trial_number}.pkl") + circuit_filepath = os.path.join(act_ginfo.circuit_study_path,act_ginfo.circuit_study_name, "filtered_results", f"{circuit_trial_number}.pkl") # Check filename if os.path.isfile(circuit_filepath): # Read results from circuit optimization @@ -133,7 +133,7 @@ def _simulation(circuit_id: int, act_io_config: fmt.InductorOptimizationDTO, act process_number = 1 # Load configuration - circuit_dto = dct.HandleDabDto.load_from_file(os.path.join(act_ginfo.circuit_study_path, "filtered_results", f"{circuit_id}.pkl")) + circuit_dto = dct.HandleDabDto.load_from_file(os.path.join(act_ginfo.circuit_study_path, act_ginfo.circuit_study_name, "filtered_results", f"{circuit_id}.pkl")) # Check number of trials if target_number_trials > 0: fmt.optimization.InductorOptimization.ReluctanceModel.start_proceed_study(act_io_config, target_number_trials=target_number_trials) @@ -238,7 +238,8 @@ def _simulation(circuit_id: int, act_io_config: fmt.InductorOptimizationDTO, act # Simulation handler. Later the simulation handler starts a process per list entry. @staticmethod def simulation_handler(act_ginfo: dct.GeneralInformation, target_number_trials: int, - filter_factor: float = 1.0, re_simulate: bool = False, debug: bool = False): + filter_factor: float = 1.0, delete_study: bool = False, + re_simulate: bool = False, debug: bool = False): """ Control the multi simulation processes. @@ -253,6 +254,7 @@ def simulation_handler(act_ginfo: dct.GeneralInformation, target_number_trials: :param debug : Debug mode flag :type debug : bool """ + # Later this is to parallelize with multiple processes for act_sim_config in InductorOptimization.sim_config_list: # Debug switch @@ -262,6 +264,14 @@ def simulation_handler(act_ginfo: dct.GeneralInformation, target_number_trials: if target_number_trials > 100: target_number_trials = 100 + # Check the deleteStudyFlag + if delete_study: + # Create path-filename of sqlite database + inductor_study_sqlite_database = os.path.join(act_sim_config[1].inductor_optimization_directory,f"{act_sim_config[1].inductor_study_name}.sqlite3") + # Check if path-filename exists + if os.path.exists(inductor_study_sqlite_database): + os.remove(inductor_study_sqlite_database) + InductorOptimization._simulation(act_sim_config[0], act_sim_config[1], act_ginfo, target_number_trials, filter_factor, re_simulate, debug) if debug: # stop after one circuit run diff --git a/dct/pareto_dtos.py b/dct/pareto_dtos.py index 857793f..e7c1697 100644 --- a/dct/pareto_dtos.py +++ b/dct/pareto_dtos.py @@ -12,8 +12,11 @@ class GeneralInformation: project_directory: str circuit_study_name: str + inductor_study_name: str + transformer_study_name: str + heat_sink_study_name: str # filtered_list_id: List[int] - filtered_list_id = [] + filtered_list_id: list[int] circuit_study_path: str inductor_study_path: str transformer_study_path: str diff --git a/dct/summary_processing.py b/dct/summary_processing.py index 65b493b..235fbf5 100644 --- a/dct/summary_processing.py +++ b/dct/summary_processing.py @@ -9,7 +9,7 @@ # own libraries import dct -from heatsink_sim import ThermalCalcSupport as thr_sup +from heat_sink_optimization import ThermalCalcSupport as thr_sup import hct @@ -32,7 +32,7 @@ class DctSummmaryProcessing: heat_sink = None @staticmethod - def init_thermal_configuration(act_thermal_configuration_dict: dict) -> bool: + def init_thermal_configuration(act_thermal_data: dct.TomlHeatSinkSummaryData) -> bool: """Initialize the thermal parameter of the connection points for the transistors, inductor and transformer. :param act_thermal_configuration_dict : dict with data of the thermal configuration @@ -44,50 +44,57 @@ def init_thermal_configuration(act_thermal_configuration_dict: dict) -> bool: # Variable declaration # Return variable initialized to True successful_init = True - + transistor_b1_cooling: list[float] + transistor_b2_cooling: list[float] + inductor_cooling: list[float] + transformer_cooling: list[float] + # [t_ambient, t_hs_max] in °C + heat_sink: list[float] # Thermal parameter for bridge transistor 1: List [tim_thickness, tim_conductivity] - DctSummmaryProcessing.transistor_b1_cooling = dct.TransistorCooling( - tim_thickness=act_thermal_configuration_dict["transistor_b1_cooling"][0], - tim_conductivity=act_thermal_configuration_dict["transistor_b1_cooling"][1], + DctSummmaryProcessing.transistor_b1_cooling = ( + dct.TransistorCooling( + tim_thickness=act_thermal_data.transistor_b1_cooling[0], + tim_conductivity=act_thermal_data.transistor_b1_cooling[1]) ) - # Thermal parameter for bridge transistor 2: List [tim_thickness, tim_conductivity] - DctSummmaryProcessing.transistor_b2_cooling = dct.TransistorCooling( - tim_thickness=act_thermal_configuration_dict["transistor_b2_cooling"][0], - tim_conductivity=act_thermal_configuration_dict["transistor_b2_cooling"][1], + DctSummmaryProcessing.transistor_b2_cooling = ( + dct.TransistorCooling( + tim_thickness=act_thermal_data.transistor_b2_cooling[0], + tim_conductivity=act_thermal_data.transistor_b2_cooling[1]) ) - # Thermal parameter for inductor: rth per area: List [tim_thickness, tim_conductivity] - inductor_cooling = dct.InductiveElementCooling( - tim_thickness=act_thermal_configuration_dict["inductor_cooling"][0], - tim_conductivity=act_thermal_configuration_dict["inductor_cooling"][1] - ) + tim_thickness = act_thermal_data.inductor_cooling[0] + tim_conductivity = act_thermal_data.inductor_cooling[1] + # Check on zero - if inductor_cooling.tim_conductivity > 0: + if tim_conductivity > 0: # Calculate the thermal resistance area product - DctSummmaryProcessing.r_th_ind_heat_sink_A = inductor_cooling.tim_thickness / inductor_cooling.tim_conductivity + DctSummmaryProcessing.r_th_ind_heat_sink_A = tim_thickness / tim_conductivity else: - print(f"inductor cooling tim conductivity value must be greater zero, but is {inductor_cooling.tim_conductivity}!") + print(f"inductor cooling tim conductivity value must be greater zero, but is {tim_conductivity}!") successful_init = False # Thermal parameter for inductor: rth per area: List [tim_thickness, tim_conductivity] # ASA: Rename database class from InductiveElementCooling to MagneticElementCooling + tim_thickness = act_thermal_data.transformer_cooling[0] + tim_conductivity = act_thermal_data.transformer_cooling[1] + transformer_cooling = dct.InductiveElementCooling( - tim_thickness=act_thermal_configuration_dict["transformer_cooling"][0], - tim_conductivity=act_thermal_configuration_dict["transformer_cooling"][1] + tim_thickness=tim_thickness, + tim_conductivity=tim_conductivity ) # Check on zero ( ASA: Maybe in general all configurtation files are to check for validity in advanced. In this case the check can be removed.) - if inductor_cooling.tim_conductivity > 0: + if tim_conductivity > 0: # Calculate the thermal resistance area product - DctSummmaryProcessing.r_th_xfmr_heat_sink_A = transformer_cooling.tim_thickness / transformer_cooling.tim_conductivity + DctSummmaryProcessing.r_th_xfmr_heat_sink_A = tim_thickness / tim_conductivity else: - print(f"transformer cooling tim conductivity value must be greater zero, but is {transformer_cooling.tim_conductivity}!") + print(f"transformer cooling tim conductivity value must be greater zero, but is {tim_conductivity}!") successful_init = False # Heat sink parameter: List [t_ambient, t_hs_max] DctSummmaryProcessing.heat_sink = dct.HeatSinkTemp( - t_ambient=act_thermal_configuration_dict["heat_sink"][0], - t_hs_max=act_thermal_configuration_dict["heat_sink"][1] + t_ambient=act_thermal_data.heat_sink[0], + t_hs_max=act_thermal_data.heat_sink[1] ) @@ -155,7 +162,7 @@ def generate_result_database(act_ginfo: dct.GeneralInformation, act_inductor_stu # iterate circuit numbers for circuit_number in act_ginfo.filtered_list_id: # Assemble pkl-filename - circuit_filepath_number = os.path.join(act_ginfo.circuit_study_path, "filtered_results", f"{circuit_number}.pkl") + circuit_filepath_number = os.path.join(act_ginfo.circuit_study_path,act_ginfo.circuit_study_name, "filtered_results", f"{circuit_number}.pkl") # Get circuit results circuit_dto = dct.HandleDabDto.load_from_file(circuit_filepath_number) @@ -343,17 +350,18 @@ def generate_result_database(act_ginfo: dct.GeneralInformation, act_inductor_stu df = pd.concat([df, local_df], axis=0) # Calculate the total area as sum of circuit, inductor and transformer area df-comand is like vector sum v1[:]=v2[:]+v3[:]) - df["total_area"] = df["circuit_area"] + df["inductor_area"] + df["transformer_area"] - df["total_mean_loss"] = df["circuit_mean_loss"] + df["inductor_mean_loss"] + df["transformer_mean_loss"] - df["volume_wo_heat_sink"] = df["transformer_volume"] + df["inductor_volume"] + # df["total_area"] = df["circuit_area"] + df["inductor_area"] + df["transformer_area"] + df["total_area"] = df["inductor_area"] + df["transformer_area"] + # df["total_mean_loss"] = df["circuit_mean_loss"] + df["inductor_mean_loss"] + df["transformer_mean_loss"] + # df["volume_wo_heat_sink"] = df["transformer_volume"] + df["inductor_volume"] # Save results to file (ASA : later to store only on demand) - df.to_csv(f"{act_ginfo.heatsink_study_path}/result_df.csv") + df.to_csv(f"{act_ginfo.heat_sink_study_path}/result_df.csv") # return the data base return df @staticmethod - def select_heatsink_configuration(act_ginfo: dct.GeneralInformation, act_heat_sink_study_name: str, act_df_for_hs: pd.DataFrame): + def select_heatsink_configuration(act_ginfo: dct.GeneralInformation, act_df_for_hs: pd.DataFrame): """Select the heatsink configuration from calculated heatsink pareto front. :param act_ginfo : General information about the study @@ -366,11 +374,11 @@ def select_heatsink_configuration(act_ginfo: dct.GeneralInformation, act_heat_si # Variable declaration # load heat sink - hs_config_filepath = os.path.join(act_ginfo.heatsink_study_path, f"{act_heat_sink_study_name}.pkl") + hs_config_filepath = os.path.join(act_ginfo.heat_sink_study_path, act_ginfo.heat_sink_study_name, f"{act_ginfo.heat_sink_study_name}.pkl") hs_config = hct.Optimization.load_config(hs_config_filepath) # Debug ASA Missing true simulations for remaining function - hs_config.heat_sink_optimization_directory="/home/andreas/Workspace/Projekt/dab_computational_toolkit/workspace/2025-01-31_example/04_heat_sink/heatsink_01" + hs_config.heat_sink_optimization_directory=os.path.join(act_ginfo.heat_sink_study_path, act_ginfo.heat_sink_study_name) df_hs = hct.Optimization.study_to_df(hs_config) # generate full summary as panda database operation diff --git a/dct/toml_checker.py b/dct/toml_checker.py index 224bbf7..808efc5 100644 --- a/dct/toml_checker.py +++ b/dct/toml_checker.py @@ -178,10 +178,67 @@ class TomlInductor(BaseModel): # ###################################################### # transformer # ###################################################### +class TomlTransformerDesignSpace(BaseModel): + """Toml checker class for TransformerDesignSpace.""" + material_name_list: list[str] + core_name_list: list[str] + core_inner_diameter_min_max_list: list[float] + window_w_min_max_list: list[float] + window_h_bot_min_max_list: list[float] + primary_litz_wire_list: list[str] + secondary_litz_wire_list: list[str] + n_p_top_min_max_list: list[int] + n_p_bot_min_max_list: list[int] + +class TomlTransformerSettings(BaseModel): + """Toml checker class for TransfomerSettings.""" + + fft_filter_value_factor: float + mesh_accuracy: float + +class TomlTransformerBoundaryConditions(BaseModel): + """Toml checker class for TransformerBondaryConditions.""" + + max_transformer_total_height: float + max_core_volume: float + temperature: float + +class TomlTransformerInsulation(BaseModel): + """Toml checker class for TransformerInsulation.""" + + # insulation for top core window + iso_window_top_core_top: float + iso_window_top_core_bot: float + iso_window_top_core_left: float + iso_window_top_core_right: float + # insulation for bottom core window + iso_window_bot_core_top: float + iso_window_bot_core_bot: float + iso_window_bot_core_left: float + iso_window_bot_core_right: float + # winding-to-winding insulation + iso_primary_to_primary: float + iso_secondary_to_secondary: float + iso_primary_to_secondary: float + +class TomlTransformerFilterDistance(BaseModel): + """Toml checker class for TransformerFilterDistance.""" + + factor_min_dc_losses: float + factor_max_dc_losses: float + +class TomlTransformer(BaseModel): + """Toml checker class for Transformer.""" + + design_space: TomlTransformerDesignSpace + insulation: TomlTransformerInsulation + filter_distance: TomlTransformerFilterDistance + settings: TomlTransformerSettings + boundary_conditions: TomlTransformerBoundaryConditions # ###################################################### -# heat sink +# heat sink inclusive data of summary calculation # ###################################################### @@ -214,6 +271,16 @@ class TomlHeatSinkDesignSpace(BaseModel): number_fins_n_list: list[int] thickness_fin_t_list: list[float] +class TomlHeatSinkSummaryData(BaseModel): + """Toml checker for HeatSinkSummaryData.""" + # [tim_thickness, tim_conductivity] + transistor_b1_cooling: list[float] + transistor_b2_cooling: list[float] + inductor_cooling: list[float] + transformer_cooling: list[float] + # [t_ambient, t_hs_max] in °C + heat_sink: list[float] + class TomlHeatSink(BaseModel): """Toml checker for HeatSink.""" @@ -221,3 +288,4 @@ class TomlHeatSink(BaseModel): design_space: TomlHeatSinkDesignSpace settings: TomlHeatSinkSettings boundary_conditions: TomlHeatSinkBoundaryConditions + ThermalResistanceData: TomlHeatSinkSummaryData diff --git a/dct/transformer_optimization.py b/dct/transformer_optimization.py index 8442c37..a970384 100644 --- a/dct/transformer_optimization.py +++ b/dct/transformer_optimization.py @@ -28,47 +28,44 @@ class TransformerOptimization: sim_config_list = [] @staticmethod - def init_configuration(act_transf_config_name: str, act_ginfo: dct.GeneralInformation, act_design_space_dict: dict, - act_transformer_data_dict: dict) -> bool: + def init_configuration(toml_transformer: dct.TomlTransformer, toml_prog_flow: dct.FlowControl, + act_ginfo: dct.GeneralInformation) -> bool: """ Initialize the configuration. - :param act_transf_config_name : Name of the transformer study - :type act_transf_config_name : str - :param act_ginfo : General information about the study - :type act_ginfo : dct.GeneralInformation: - :param act_design_space_dict : dict with data of the design space - :type act_design_space_dict : dict - :param act_transformer_data_dict : dict with parameter of the transformer - :type act_transformer_data_dict : dict + :param toml_transformer: transformer toml file + :type toml_transformer: dct.TomlTransformer + :param toml_prog_flow: flow control toml file + :type toml_prog_flow: dct.FlowControl + :param act_ginfo: General information + :type act_ginfo: dct.GeneralInformation :return: True, if the configuration was successful initialized :rtype: bool """ # Variable declaration # Return variable initialized to True (ASA: Usage is to add later, currently not used - ret_val = True + transformer_initialization_successful = True - # Insulation parameter act_insulation = fmt.StoInsulation( # insulation for top core window - iso_window_top_core_top=act_transformer_data_dict["iso_window_top_core_top"], - iso_window_top_core_bot=act_transformer_data_dict["iso_window_top_core_bot"], - iso_window_top_core_left=act_transformer_data_dict["iso_window_top_core_left"], - iso_window_top_core_right=act_transformer_data_dict["iso_window_top_core_right"], + iso_window_top_core_top=toml_transformer.insulation.iso_window_top_core_top, + iso_window_top_core_bot=toml_transformer.insulation.iso_window_top_core_bot, + iso_window_top_core_left=toml_transformer.insulation.iso_window_top_core_left, + iso_window_top_core_right=toml_transformer.insulation.iso_window_top_core_right, # insulation for bottom core window - iso_window_bot_core_top=act_transformer_data_dict["iso_window_bot_core_top"], - iso_window_bot_core_bot=act_transformer_data_dict["iso_window_bot_core_bot"], - iso_window_bot_core_left=act_transformer_data_dict["iso_window_bot_core_left"], - iso_window_bot_core_right=act_transformer_data_dict["iso_window_bot_core_right"], + iso_window_bot_core_top=toml_transformer.insulation.iso_window_bot_core_top, + iso_window_bot_core_bot=toml_transformer.insulation.iso_window_bot_core_bot, + iso_window_bot_core_left=toml_transformer.insulation.iso_window_bot_core_left, + iso_window_bot_core_right=toml_transformer.insulation.iso_window_bot_core_right, # winding-to-winding insulation - iso_primary_to_primary=act_transformer_data_dict["iso_primary_to_primary"], - iso_secondary_to_secondary=act_transformer_data_dict["iso_secondary_to_secondary"], - iso_primary_to_secondary=act_transformer_data_dict["iso_primary_to_secondary"] + iso_primary_to_primary=toml_transformer.insulation.iso_primary_to_primary, + iso_secondary_to_secondary=toml_transformer.insulation.iso_secondary_to_secondary, + iso_primary_to_secondary=toml_transformer.insulation.iso_primary_to_secondary ) - # Init the material data source - act_material_data_sources = fmt.StackedTransformerMaterialDataSources( + # Init the material data source + material_data_sources = fmt.StackedTransformerMaterialDataSources( permeability_datasource=fmt.MaterialDataSource.Measurement, permeability_datatype=fmt.MeasurementDataType.ComplexPermeability, permeability_measurement_setup=fmt.MeasurementSetup.MagNet, @@ -78,8 +75,9 @@ def init_configuration(act_transf_config_name: str, act_ginfo: dct.GeneralInform ) # Create fix part of io_config - sto_config_gen = fmt.StoSingleInputConfig( - stacked_transformer_study_name=act_transf_config_name, + sto_config = fmt.StoSingleInputConfig( + stacked_transformer_study_name=toml_prog_flow.configuration_data_files.transformer_configuration_file.replace( + ".toml", ""), # target parameters initialized with default values l_s12_target=0, l_h_target=0, @@ -87,30 +85,30 @@ def init_configuration(act_transf_config_name: str, act_ginfo: dct.GeneralInform # operating point: current waveforms and temperature initialized with default values time_current_1_vec=np.ndarray([]), time_current_2_vec=np.ndarray([]), - temperature=100, # ASA Later it becomes a dynamic value? + temperature=toml_transformer.boundary_conditions.temperature, # ASA Later it becomes a dynamic value? # sweep parameters: geometry and materials - n_p_top_min_max_list=act_transformer_data_dict["n_p_top_min_max_list"], - n_p_bot_min_max_list=act_transformer_data_dict["n_p_bot_min_max_list"], - material_list=act_design_space_dict["material_name_list"], - core_name_list=act_design_space_dict["core_name_list"], - core_inner_diameter_min_max_list=act_design_space_dict["core_inner_diameter_min_max_list"], - window_w_min_max_list=act_design_space_dict["window_w_min_max_list"], - window_h_bot_min_max_list=act_design_space_dict["window_h_bot_min_max_list"], - primary_litz_wire_list=act_design_space_dict["primary_litz_wire_list"], - secondary_litz_wire_list=act_design_space_dict["secondary_litz_wire_list"], + n_p_top_min_max_list=toml_transformer.design_space.n_p_top_min_max_list, + n_p_bot_min_max_list=toml_transformer.design_space.n_p_bot_min_max_list, + material_list=toml_transformer.design_space.material_name_list, + core_name_list=toml_transformer.design_space.core_name_list, + core_inner_diameter_min_max_list=toml_transformer.design_space.core_inner_diameter_min_max_list, + window_w_min_max_list=toml_transformer.design_space.window_w_min_max_list, + window_h_bot_min_max_list=toml_transformer.design_space.window_h_bot_min_max_list, + primary_litz_wire_list=toml_transformer.design_space.primary_litz_wire_list, + secondary_litz_wire_list=toml_transformer.design_space.secondary_litz_wire_list, # maximum limitation for transformer total height and core volume - max_transformer_total_height=act_transformer_data_dict["max_transformer_total_height"], - max_core_volume=act_transformer_data_dict["max_core_volume"], + max_transformer_total_height=toml_transformer.boundary_conditions.max_transformer_total_height, + max_core_volume=toml_transformer.boundary_conditions.max_core_volume, # fix parameters: insulations insulations=act_insulation, # misc stacked_transformer_optimization_directory="", - fft_filter_value_factor=act_transformer_data_dict["fft_filter_value_factor"], - mesh_accuracy=act_transformer_data_dict["mesh_accuracy"], + fft_filter_value_factor=toml_transformer.settings.fft_filter_value_factor, + mesh_accuracy=toml_transformer.settings.mesh_accuracy, # data sources - material_data_sources=act_material_data_sources + material_data_sources=material_data_sources ) # Empty the list @@ -118,7 +116,8 @@ def init_configuration(act_transf_config_name: str, act_ginfo: dct.GeneralInform # Create the sto_config_list for all trials for circuit_trial_number in act_ginfo.filtered_list_id: - circuit_filepath = os.path.join(act_ginfo.circuit_study_path, "filtered_results", f"{circuit_trial_number}.pkl") + circuit_filepath = os.path.join(act_ginfo.circuit_study_path,act_ginfo.circuit_study_name, "filtered_results", + f"{circuit_trial_number}.pkl") # Check filename if os.path.isfile(circuit_filepath): @@ -128,10 +127,11 @@ def init_configuration(act_transf_config_name: str, act_ginfo: dct.GeneralInform sorted_max_angles, i_l_s_max_current_waveform, i_hf_2_max_current_waveform = dct.HandleDabDto.get_max_peak_waveform_transformer( circuit_dto, False) time = sorted_max_angles / 2 / np.pi / circuit_dto.input_config.fs - transformer_target_params = dct.HandleDabDto.export_transformer_target_parameters_dto(dab_dto=circuit_dto) + transformer_target_params = dct.HandleDabDto.export_transformer_target_parameters_dto( + dab_dto=circuit_dto) # Generate new sto_config - next_io_config = copy.deepcopy(sto_config_gen) + next_io_config = copy.deepcopy(sto_config) # Add dynamic values to next_io_config # target parameters next_io_config.l_s12_target = float(transformer_target_params.l_s12_target) @@ -141,13 +141,15 @@ def init_configuration(act_transf_config_name: str, act_ginfo: dct.GeneralInform next_io_config.time_current_1_vec = transformer_target_params.time_current_1_vec next_io_config.time_current_2_vec = transformer_target_params.time_current_2_vec # misc - next_io_config.stacked_transformer_optimization_directory\ - = os.path.join(act_ginfo.transformer_study_path, circuit_trial_number, act_transf_config_name) + next_io_config.stacked_transformer_optimization_directory \ + = os.path.join(act_ginfo.transformer_study_path, circuit_trial_number, + sto_config.stacked_transformer_study_name) TransformerOptimization.sim_config_list.append([circuit_trial_number, next_io_config]) else: print(f"Wrong path or file {circuit_filepath} does not exists!") - return ret_val + return transformer_initialization_successful + @staticmethod def _simulation(circuit_id: int, act_sto_config: fmt.StoSingleInputConfig, act_ginfo: dct.GeneralInformation, @@ -173,7 +175,7 @@ def _simulation(circuit_id: int, act_sto_config: fmt.StoSingleInputConfig, act_g process_number = 1 # Load configuration - circuit_dto = dct.HandleDabDto.load_from_file(os.path.join(act_ginfo.circuit_study_path, "filtered_results", f"{circuit_id}.pkl")) + circuit_dto = dct.HandleDabDto.load_from_file(os.path.join(act_ginfo.circuit_study_path,act_ginfo.circuit_study_name, "filtered_results", f"{circuit_id}.pkl")) # Check number of trials if act_target_number_trials > 0: fmt.optimization.StackedTransformerOptimization.ReluctanceModel.start_proceed_study( @@ -280,7 +282,7 @@ def _simulation(circuit_id: int, act_sto_config: fmt.StoSingleInputConfig, act_g @staticmethod # Simulation handler. Later the simulation handler starts a process per list entry. def simulation_handler(act_ginfo: dct.GeneralInformation, target_number_trials: int, - filter_factor: float = 1.0, re_simulate: bool = False, debug: bool = False): + filter_factor: float = 1.0, delete_study: bool = False, re_simulate: bool = False, debug: bool = False): """ Control the multi simulation processes. @@ -304,6 +306,16 @@ def simulation_handler(act_ginfo: dct.GeneralInformation, target_number_trials: if target_number_trials > 100: target_number_trials = 100 + # Check the deleteStudyFlag + if delete_study: + # Create path-filename of sqlite database + stacked_transformer_study_sqlite_database = ( + os.path.join(act_sim_config[1].stacked_transformer_optimization_directory, + f"{act_sim_config[1].stacked_transformer_study_name}.sqlite3")) + # Check if path-filename exists + if os.path.exists(stacked_transformer_study_sqlite_database): + os.remove(stacked_transformer_study_sqlite_database) + TransformerOptimization._simulation(act_sim_config[0], act_sim_config[1], act_ginfo, target_number_trials, filter_factor, re_simulate, debug) if debug: diff --git a/workspace/DabHeatSinkConf.toml b/workspace/DabHeatSinkConf.toml index b2d2ce3..34bbe0e 100644 --- a/workspace/DabHeatSinkConf.toml +++ b/workspace/DabHeatSinkConf.toml @@ -26,6 +26,5 @@ transistor_b2_cooling = [1e-3,12.0] inductor_cooling = [1e-3,12.0] transformer_cooling = [1e-3,12.0] - # [t_ambient, t_hs_max] in °C heat_sink = [40.0, 90.0] diff --git a/workspace/DabTransformerConf.toml b/workspace/DabTransformerConf.toml index 670cf57..6f4833e 100644 --- a/workspace/DabTransformerConf.toml +++ b/workspace/DabTransformerConf.toml @@ -1,7 +1,4 @@ -[TransformerConfigName] - transformer_config_name = "transformer_01" - -[Designspace] +[design_space] material_name_list=['3C95'] core_name_list=["PQ 40/40", "PQ 40/30", "PQ 35/35", "PQ 32/30", "PQ 32/20", "PQ 26/25", "PQ 26/20", "PQ 20/20", "PQ 20/16"] core_inner_diameter_min_max_list=[15e-3, 30e-3] @@ -9,14 +6,17 @@ window_h_bot_min_max_list=[10e-3, 50e-3] primary_litz_wire_list=['1.1x60x0.1'] secondary_litz_wire_list=['1.35x200x0.071', '1.1x60x0.1'] + # sweep parameters: geometry and materials + n_p_top_min_max_list=[1, 30] + n_p_bot_min_max_list=[10, 80] -[TransformerData] +[boundary_conditions] # maximum limitation for transformer total height and core volume max_transformer_total_height=60e-3 max_core_volume=0.007853982 # 50e-3 ** 2 * 3.141593 - # sweep parameters: geometry and materials - n_p_top_min_max_list=[1, 30] - n_p_bot_min_max_list=[10, 80] + temperature=100 + +[insulation] # insulation for top core window iso_window_top_core_top=1.3e-3 iso_window_top_core_bot=1.3e-3 @@ -31,11 +31,11 @@ iso_primary_to_primary=0.2e-3 iso_secondary_to_secondary=0.2e-3 iso_primary_to_secondary=0.2e-3 - # simulation data parameter + +[settings] fft_filter_value_factor=0.01 mesh_accuracy=0.8 -[FilterDistance] - Delta = [0.1,0.1] # difference (x ,y) 1 = difference between minimal and maximal result value - Range = [[0.1,0.9],[0.1,0.9]] # Used range of X und Y-axe - Deep = [0.01,0.01] # Max difference to real pareto points (X und Y-direction) +[filter_distance] + factor_min_dc_losses=1 + factor_max_dc_losses=100 \ No newline at end of file diff --git a/workspace/progFlow.toml b/workspace/progFlow.toml index 6b37d23..acae1a5 100644 --- a/workspace/progFlow.toml +++ b/workspace/progFlow.toml @@ -5,12 +5,12 @@ [breakpoints] # possible values: no/pause/stop - circuit_pareto = "pause" # After Electrical paretofront calculation + circuit_pareto = "no" # After Electrical paretofront calculation circuit_filtered = "no" # After Electrical filtered result calculation inductor = "no" # After inductor paretofront calculations of for all correspondent electrical points transformer = "no" # After transformer paretofront calculations of for all correspondent electrical points heat_sink = "no" # After heatsink paretofront calculation - summary = "pause" # After heatsink paretofront calculation + summary = "no" # After heatsink paretofront calculation [conditional_breakpoints] # conditional breakpoints in case of bad definition array (only for experts and currently not implemented) circuit = 1000 # Number of trials with ZVS less than 70% @@ -20,7 +20,7 @@ [circuit] number_of_trials = 500 - re_calculation = "continue" # (new,continue,skip) + re_calculation = "skip" # (new,continue,skip) subdirectory = "01_circuit" [inductor] @@ -34,8 +34,8 @@ subdirectory = "03_transformer" [heat_sink] - number_of_trials = 15000 - re_calculation = "new" # (new,continue,skip) + number_of_trials = 100 + re_calculation = "continue" # (new,continue,skip) subdirectory = "04_heat_sink" circuit_study_name_flag = "True" # True=Use circuit study name, False (or any word except 'True') = Do not use ciruit study name From b85cd52ad62f0e8daabc3543395e30968541d689 Mon Sep 17 00:00:00 2001 From: SevenOfNinePE Date: Fri, 11 Apr 2025 07:13:18 +0200 Subject: [PATCH 6/8] Update files according solutions described in conversation of pull request. One update is missing: Last line in summary_processing.py is still comment out. (# df_wo_hs....) --- dct/circuit_optimization.py | 8 +- dct/dctmainctl.py | 30 ++--- dct/heat_sink_dtos.py | 2 +- dct/heat_sink_optimization.py | 16 +-- dct/inductor_optimization.py | 8 +- dct/server_ctl.py | 198 ------------------------------- dct/summary_processing.py | 94 ++++++++------- dct/toml_checker.py | 2 +- dct/transformer_optimization.py | 8 +- examples/pareto_summary_wo_hs.py | 6 +- pyproject.toml | 2 +- workspace/DabHeatSinkConf.toml | 2 +- 12 files changed, 89 insertions(+), 287 deletions(-) delete mode 100644 dct/server_ctl.py diff --git a/dct/circuit_optimization.py b/dct/circuit_optimization.py index f82bf36..5433606 100644 --- a/dct/circuit_optimization.py +++ b/dct/circuit_optimization.py @@ -233,7 +233,7 @@ def run_optimization_mysql(act_storage_url: str, act_study_name: str, act_number def start_proceed_study(dab_config: p_dtos.CircuitParetoDabDesign, number_trials: int, database_type: str = 'sqlite', sampler=optuna.samplers.NSGAIIISampler(), - delete_study: bool = False + enable_delete_study: bool = False ): """Proceed a study which is stored as sqlite database. @@ -245,8 +245,8 @@ def start_proceed_study(dab_config: p_dtos.CircuitParetoDabDesign, number_trials :type database_type: str :param sampler: optuna.samplers.NSGAIISampler() or optuna.samplers.NSGAIIISampler(). Note about the brackets () !! Default: NSGAIII :type sampler: optuna.sampler-object - :param delete_study: Indication, if the old study are to delete (True) or optimization shall be continued. - :type delete_study: bool + :param enable_delete_study: Indication, if the old study are to delete (True) or optimization shall be continued. + :type enable_delete_study: bool """ filepaths = CircuitOptimization.load_filepaths(dab_config.project_directory) @@ -290,7 +290,7 @@ def start_proceed_study(dab_config: p_dtos.CircuitParetoDabDesign, number_trials storage = f"sqlite:///{circuit_study_sqlite_database}" # Check the deleteStudyFlag - if delete_study and os.path.exists(circuit_study_sqlite_database): + if enable_delete_study and os.path.exists(circuit_study_sqlite_database): with os.scandir(circuit_study_working_directory) as entries: for entry in entries: if entry.is_dir() and not entry.is_symlink(): diff --git a/dct/dctmainctl.py b/dct/dctmainctl.py index 25eb7ad..65b74c4 100644 --- a/dct/dctmainctl.py +++ b/dct/dctmainctl.py @@ -3,7 +3,6 @@ import os import sys import tomllib -from os.path import abspath # 3rd party libraries import json @@ -122,8 +121,10 @@ def init_general_info(act_config_program_flow: tc.FlowControl) -> dct.GeneralInf :return: general information variable containing general information for the optimization :rtype: dct.GeneralInformation """ + # Variable declaration + # Set projektdirectory - project_directory = abspath(act_config_program_flow.general.project_directory) + project_directory = os.path.abspath(act_config_program_flow.general.project_directory) # Setup variable by set study names r_ginfo = dct.GeneralInformation( @@ -256,7 +257,7 @@ def circuit_toml_2_dto(toml_circuit: tc.TomlCircuitParetoDabDesign, toml_prog_fl return circuit_dto @staticmethod - def run_optimization_from_toml_configs(workspace_path: str) -> None: + def run_optimization_from_toml_configs(workspace_path: str): """Perform the main program. This function corresponds to 'main', which is called after the instance of the class are created. @@ -274,6 +275,9 @@ def run_optimization_from_toml_configs(workspace_path: str) -> None: hsim = HeatSinkOptimization # Flag for available filtered results filtered_circuit_result_folder_exists = False + # Flag for resimulation (if False the summary will failed) + enable_ind_re_simulation = True + enable_trans_re_simulation = True # Check if workspace path is not provided by argument if workspace_path == "": @@ -299,7 +303,7 @@ def run_optimization_from_toml_configs(workspace_path: str) -> None: raise ValueError("Program flow toml file does not exist.") # Add absolute path to project data path (ASA: Later to remove because do not manipuase) - workspace_path = abspath(workspace_path) + workspace_path = os.path.abspath(workspace_path) toml_prog_flow.general.project_directory = os.path.join(workspace_path, toml_prog_flow.general.project_directory) # Init general information: Project directory, study names and corresponding paths @@ -443,7 +447,7 @@ def run_optimization_from_toml_configs(workspace_path: str) -> None: # Start calculation dct.CircuitOptimization.start_proceed_study(config_circuit, number_trials=toml_prog_flow.circuit.number_of_trials, - delete_study=is_new_circuit_study) + enable_delete_study=is_new_circuit_study) # Check breakpoint DctMainCtl.check_breakpoint(toml_prog_flow.breakpoints.circuit_pareto, "Electric Pareto front calculated") @@ -479,7 +483,7 @@ def run_optimization_from_toml_configs(workspace_path: str) -> None: # Start simulation ASA: Filter_factor to correct isim.init_configuration(toml_inductor, toml_prog_flow, ginfo) isim.simulation_handler(ginfo, toml_prog_flow.inductor.number_of_trials, toml_inductor.filter_distance.factor_min_dc_losses, - toml_inductor.filter_distance.factor_max_dc_losses, is_new_inductor_study, True) + toml_inductor.filter_distance.factor_max_dc_losses, is_new_inductor_study, enable_ind_re_simulation) # Check breakpoint DctMainCtl.check_breakpoint(toml_prog_flow.breakpoints.inductor, "Inductor Pareto front calculated") @@ -502,7 +506,7 @@ def run_optimization_from_toml_configs(workspace_path: str) -> None: # Perform transformer optimization tsim.simulation_handler(ginfo, toml_prog_flow.transformer.number_of_trials, toml_transformer.filter_distance.factor_min_dc_losses, - toml_transformer.filter_distance.factor_max_dc_losses, is_new_transformer_study, True) + toml_transformer.filter_distance.factor_max_dc_losses, is_new_transformer_study, enable_trans_re_simulation) # Check breakpoint DctMainCtl.check_breakpoint(toml_prog_flow.breakpoints.transformer, "Transformer Pareto front calculated") @@ -529,7 +533,7 @@ def run_optimization_from_toml_configs(workspace_path: str) -> None: DctMainCtl.check_breakpoint(toml_prog_flow.breakpoints.heat_sink, "Heat sink Pareto front calculated") # Initialisation thermal data - if not spro.init_thermal_configuration(toml_heat_sink.ThermalResistanceData): + if not spro.init_thermal_configuration(toml_heat_sink.thermal_resistance_data): raise ValueError("Thermal data configuration not initialized!") # Create list of inductor and transformer study (ASA: Currently not implemented in configuration files) inductor_study_names = [ginfo.inductor_study_name] @@ -537,18 +541,10 @@ def run_optimization_from_toml_configs(workspace_path: str) -> None: # Start summary processing by generating the dataframe from calculated simmulation results s_df = spro.generate_result_database(ginfo, inductor_study_names, stacked_transformer_study_names) # Select the needed heatsink configuration - spro.select_heatsink_configuration(ginfo, s_df) + spro.select_heat_sink_configuration(ginfo, s_df) # Check breakpoint DctMainCtl.check_breakpoint(toml_prog_flow.breakpoints.summary, "Calculation is complete") - # Join process if necessary ASA to check - # esim.join_process() - # Shut down server - # Debug: Server switched off - # srv_ctl.stop_dct_server() - - pass - # Program flow control of DAB-optimization if __name__ == "__main__": diff --git a/dct/heat_sink_dtos.py b/dct/heat_sink_dtos.py index 5768a99..a19822f 100644 --- a/dct/heat_sink_dtos.py +++ b/dct/heat_sink_dtos.py @@ -5,7 +5,7 @@ @dataclasses.dataclass -class HeatSinkTemp: +class HeatSinkBoundaryConditions: """Fix parameters for the heat sink cooling.""" t_ambient: float diff --git a/dct/heat_sink_optimization.py b/dct/heat_sink_optimization.py index 07591af..554331b 100644 --- a/dct/heat_sink_optimization.py +++ b/dct/heat_sink_optimization.py @@ -137,19 +137,19 @@ def calculate_r_th_tim(copper_coin_bot_area: float, transistor_cooling: Transist # Simulation handler. Later the simulation handler starts a process per list entry. @staticmethod def _simulation(act_hct_config: hct.OptimizationParameters, - target_number_trials: int, delete_study: bool, debug: bool): + target_number_trials: int, enable_delete_study: bool, debug: bool): """ Perform the simulation. :param target_number_trials: Number of trials for the optimization :type target_number_trials: int - :param delete_study: True to delete the existing study and start a new one - :type delete_study: bool + :param enable_delete_study: True to delete the existing study and start a new one + :type enable_delete_study: bool :param debug: Debug mode flag :type debug: bool """ # delete existing study - if delete_study and os.path.exists(act_hct_config.heat_sink_optimization_directory): + if enable_delete_study and os.path.exists(act_hct_config.heat_sink_optimization_directory): with os.scandir(act_hct_config.heat_sink_optimization_directory) as entries: for entry in entries: if entry.is_dir() and not entry.is_symlink(): @@ -171,7 +171,7 @@ def _simulation(act_hct_config: hct.OptimizationParameters, # Simulation handler. Later the simulation handler starts a process per list entry. @staticmethod def optimization_handler(act_ginfo: type[dct.GeneralInformation], target_number_trials: int, - delete_study: bool = False, debug: bool = False): + enable_delete_study: bool = False, debug: bool = False): """ Control the multi simulation processes. @@ -179,8 +179,8 @@ def optimization_handler(act_ginfo: type[dct.GeneralInformation], target_number_ :type act_ginfo: dct.GeneralInformation: :param target_number_trials: Number of trials for the optimization :type target_number_trials: int - :param delete_study: True to delete the existing study and start a new one - :type delete_study: bool + :param enable_delete_study: True to delete the existing study and start a new one + :type enable_delete_study: bool :param debug: Debug mode flag :type debug: bool """ @@ -194,7 +194,7 @@ def optimization_handler(act_ginfo: type[dct.GeneralInformation], target_number_ if target_number_trials > 100: target_number_trials = 100 - HeatSinkOptimization._simulation(act_sim_config, target_number_trials, delete_study, debug) + HeatSinkOptimization._simulation(act_sim_config, target_number_trials, enable_delete_study, debug) if debug: # stop after one circuit run break diff --git a/dct/inductor_optimization.py b/dct/inductor_optimization.py index 7351e03..a972f30 100644 --- a/dct/inductor_optimization.py +++ b/dct/inductor_optimization.py @@ -240,7 +240,7 @@ def _simulation(circuit_id: int, act_io_config: fmt.InductorOptimizationDTO, act # Simulation handler. Later the simulation handler starts a process per list entry. @staticmethod def simulation_handler(act_ginfo: type[dct.GeneralInformation], target_number_trials: int, - factor_min_dc_losses: float = 1.0, factor_dc_max_losses: float = 100, delete_study: bool = False, + factor_min_dc_losses: float = 1.0, factor_dc_max_losses: float = 100, enable_delete_study: bool = False, re_simulate: bool = False, debug: bool = False): """ Control the multi simulation processes. @@ -253,8 +253,8 @@ def simulation_handler(act_ginfo: type[dct.GeneralInformation], target_number_tr :type factor_min_dc_losses : float :param factor_dc_max_losses: Filter factor for the maximum losses, related to the minimum DC losses :type factor_dc_max_losses: float - :param delete_study: Flag, which indicates to delete the study - :type delete_study: bool + :param enable_delete_study: Flag, which indicates to delete the study + :type enable_delete_study: bool :param re_simulate : Flag to control, if the point are to re-simulate (ASA: Correct the parameter description) :type re_simulate : bool :param debug : Debug mode flag @@ -270,7 +270,7 @@ def simulation_handler(act_ginfo: type[dct.GeneralInformation], target_number_tr target_number_trials = 100 # Check the deleteStudyFlag - if delete_study: + if enable_delete_study: # Create path-filename of sqlite database inductor_study_sqlite_database = os.path.join(act_sim_config[1].inductor_optimization_directory, f"{act_sim_config[1].inductor_study_name}.sqlite3") diff --git a/dct/server_ctl.py b/dct/server_ctl.py deleted file mode 100644 index 60f9476..0000000 --- a/dct/server_ctl.py +++ /dev/null @@ -1,198 +0,0 @@ -"""Server class implementation.""" -# python libraries -import multiprocessing -import threading -import time - -# 3rd party libraries -import uvicorn -from fastapi import FastAPI, Request -from fastapi.templating import Jinja2Templates -from fastapi.responses import HTMLResponse -from fastapi.staticfiles import StaticFiles -import optuna -from optuna.visualization import plot_pareto_front - -# own libraries - - -# Debug server -import logging -logging.basicConfig(level=logging.DEBUG) - -class Dct_server: - """server class to supervise the simulation.""" - - # Variable declaration - # FastAPI-Server definieren - app = FastAPI() - templates = Jinja2Templates(directory="/home/andreas/Workspace/Projekt/dab_computational_toolkit/dct/htmltemplates") - status_message = "Warte auf Knopfdruck" # Initialer Text - # Serverobject - srv_obj = None - # Shared memory variable - req_stop = multiprocessing.Value('i', 0) - stop_flag = multiprocessing.Value('i', 0) - # Server process - _server_process = None - # program exit flag - _prog_exit_flag = False - # Server supervision thread - _srv_supervision_thd = None - - # Mounten des Stylesheetpfades - app.mount("/StyleSheets", StaticFiles(directory="htmltemplates/StyleSheets"), name="Stylesheets") - - @staticmethod - def start_dct_server(shared_histogram, program_exit_flag: bool): - """Start the server to control and supervise simulation. - - :param shared_histogram: Shared memory flag for histogram information - :type shared_histogram: multiprocessing.Value - :param program_exit_flag: Flag, which indicates if the server (False) or the whole simulation (True) is to stop - :type program_exit_flag: boolean - """ - Dct_server._prog_exit_flag = program_exit_flag - - # Start the server process - Dct_server._server_process = multiprocessing.Process(target=Dct_server._run_server, args=(shared_histogram,)) - Dct_server._server_process.start() - # Check if server process supervision is to start due to program exit requested by server - if Dct_server._prog_exit_flag: - # Create thread for the serversupervision and start it - Dct_server._srv_supervision_thd = threading.Thread(target=Dct_server._supervice_server_stop) - Dct_server._srv_supervision_thd.start() - - @staticmethod - def stop_dct_server(): - """Stop the simulation supervision server.""" - # Set program exit flag to false because program will be exit by themself - Dct_server._prog_exit_flag = False - - # Request server to stop - Dct_server.req_stop.value = 1 - # Debug - print("Process shall join") - # Wait for joined server process - Dct_server._server_process.join(5) - # Stop server supervision if started - if Dct_server._srv_supervision_thd is not None: - Dct_server._srv_supervision_thd.join(5) - # Debug - print("Process has joined") - - @staticmethod - def _supervice_server_stop(): - """Stop the FastAPI-Server.""" - # Supervice if the server is stopped by user request - while True: - # Reduce CPU-supervice load by toggle each second - time.sleep(1) - # Check if server is stopped and requested to stop if the program needs to stop too - if Dct_server.stop_flag.value == 1 and Dct_server._prog_exit_flag: - # Check if the program needs to stop too - print("Program stop is requested") - # Soft kill of process does not work - # sys.exit() - break - - @staticmethod - def _run_server(shared_histogram): - """Start FastAPI-server. - - :param request : Request value - :type request : Request - - :return: Html- page based on html-template - :rtype: _TemplateResponse - """ - # Overtake the shared memory variable - Dct_server.shared_histogram = shared_histogram - # Start the server (blocking call) - config = uvicorn.Config(Dct_server.app, host="127.0.0.1", port=8005, log_level="info") - Dct_server.srv_obj = uvicorn.Server(config) - # Create thread for the server and start it - Dct_server.srv_thread = threading.Thread(target=Dct_server.dct_server_thread) - Dct_server.srv_thread.start() - # Supervice if the server is stopped by main - while True: - # Reduce CPU-supervice load by toggle each second - time.sleep(1) - # Check if server is requested to stop - if Dct_server.req_stop.value == 1: - break - - # Stoppt den Server - Dct_server.srv_obj.should_exit = True - # Debug - print("SThread soll joinen") - # Wait for thread stop - Dct_server.srv_thread.join() - # Debug - print("SThread hat gejoint") - # Set server stop flag to 0 - Dct_server.stop_flag.value = 1 - - @staticmethod - def dct_server_thread(): - """Start FastAPI-Server in thread.""" - # Start the server in a blocking call - Dct_server.srv_obj.run() - - @staticmethod - def LoadActualParetofront(): - """Load Pareto front do display.""" - # Connect with optuna-database - study = optuna.load_study(study_name="circuit_01", - storage=("sqlite:////home/andreas/Workspace/Projekt" - "/dab_computational_toolkit/workspace/2025-01-31_example/" - "01_circuit/circuit_01/circuit_01.sqlite3")) - - # Erzeuge die aktuelle Paretofront - fig = plot_pareto_front(study) - - # Speichere die HTML-Darstellung des Plots in einer Variablen - html_variable = fig.to_html(full_html=False) - - return html_variable - - @app.get("/", response_class=HTMLResponse) - async def main_page(request: Request, action: str = None): - """Provide the answer on client requests. - - :param request : Request value - :type request : Request - :param action : Requested action - :type action : Requested action - - :return: Html- page based on html-template - :rtype: _TemplateResponse - """ - if action == "continue": - Dct_server.status_message = "Weiter ist aktiv" - elif action == "pause": - Dct_server.status_message = "Pause ist aktiv" - elif action == "stop": - Dct_server.status_message = "Stoppt den Server und die Simulation (wenn prog_exit_flag==true)" - Dct_server.req_stop.value = 1 - - return Dct_server.templates.TemplateResponse("html_main.html", {"request": request, "textvariable": Dct_server.status_message}) - - @app.get("/histogram", response_class=HTMLResponse) - def get_histogram(request: Request): - """Provide the answer on client histogram request. - - :param request : Request value - :type request : Request - - :return: Html- page based on html-template with Histogram information - :rtype: _TemplateResponse - """ - # Return the html-page with updated image data - html_page = Dct_server.LoadActualParetofront() - return HTMLResponse(content=html_page) - - # Return the html-page with updated image data - # return Dct_server.templates.TemplateResponse( "html_histogram.html", {"request": request, "imagedata": img_str}) - # imagepath = "StyleSheets/Dummytrafo.png" - # return Dct_server.templates.TemplateResponse("html_histogram.html", {"request": request, "image_path": imagepath}) diff --git a/dct/summary_processing.py b/dct/summary_processing.py index 0dc0e00..d09e679 100644 --- a/dct/summary_processing.py +++ b/dct/summary_processing.py @@ -19,14 +19,14 @@ class DctSummmaryProcessing: # Variable declaration # Areas and transistor cooling parameter - copper_coin_area_1 = None - transistor_b1_cooling = None - copper_coin_area_2 = None - transistor_b2_cooling = None + copper_coin_area_1: float + transistor_b1_cooling: float + copper_coin_area_2: float + transistor_b2_cooling: float - # Thermal resistance * area - r_th_ind_heat_sink_A = None - r_th_xfmr_heat_sink_A = None + # Thermal resistance + r_th_ind_heat_sink_a: float + r_th_xfmr_heat_sink_a: float # Heat sink parameter heat_sink = None @@ -56,32 +56,32 @@ def init_thermal_configuration(act_thermal_data: dct.TomlHeatSinkSummaryData) -> tim_conductivity=act_thermal_data.transistor_b2_cooling[1]) # Thermal parameter for inductor: rth per area: List [tim_thickness, tim_conductivity] - tim_thickness = act_thermal_data.inductor_cooling[0] - tim_conductivity = act_thermal_data.inductor_cooling[1] + inductor_tim_thickness = act_thermal_data.inductor_cooling[0] + inductor_tim_conductivity = act_thermal_data.inductor_cooling[1] # Check on zero - if tim_conductivity > 0: + if inductor_tim_conductivity > 0: # Calculate the thermal resistance area product - DctSummmaryProcessing.r_th_ind_heat_sink_A = tim_thickness / tim_conductivity + DctSummmaryProcessing.r_th_ind_heat_sink_a = inductor_tim_thickness / inductor_tim_conductivity else: - print(f"inductor cooling tim conductivity value must be greater zero, but is {tim_conductivity}!") + print(f"inductor cooling tim conductivity value must be greater zero, but is {inductor_tim_conductivity}!") successful_init = False # Thermal parameter for inductor: rth per area: List [tim_thickness, tim_conductivity] # ASA: Rename database class from InductiveElementCooling to MagneticElementCooling - tim_thickness = act_thermal_data.transformer_cooling[0] - tim_conductivity = act_thermal_data.transformer_cooling[1] + transformer_tim_thickness = act_thermal_data.transformer_cooling[0] + transformer_tim_conductivity = act_thermal_data.transformer_cooling[1] transformer_cooling = dct.InductiveElementCooling( - tim_thickness=tim_thickness, - tim_conductivity=tim_conductivity + tim_thickness=transformer_tim_thickness, + tim_conductivity=transformer_tim_conductivity ) # Check on zero ( ASA: Maybe in general all configurtation files are to check for validity in advanced. In this case the check can be removed.) - if tim_conductivity > 0: + if transformer_tim_conductivity > 0: # Calculate the thermal resistance area product - DctSummmaryProcessing.r_th_xfmr_heat_sink_A = tim_thickness / tim_conductivity + DctSummmaryProcessing.r_th_xfmr_heat_sink_a = transformer_tim_thickness / transformer_tim_conductivity else: - print(f"transformer cooling tim conductivity value must be greater zero, but is {tim_conductivity}!") + print(f"transformer cooling tim conductivity value must be greater zero, but is {transformer_tim_conductivity}!") successful_init = False # Heat sink parameter: List [t_ambient, t_hs_max] @@ -91,17 +91,19 @@ def init_thermal_configuration(act_thermal_data: dct.TomlHeatSinkSummaryData) -> return successful_init @staticmethod - def _generate_number_list(act_dir_name: str, act_device_numbers: list[str]) -> bool: + def _generate_magnetic_number_list(act_dir_name: str) -> tuple[bool, list[str]]: """Generate a list of the numbers from filenames. :param act_dir_name : Name of the directory containing the files :type act_dir_name : str - :param act_device_numbers : Reference to the device number list object - :type act_device_numbers : str - :return: True, if the directory exists and contains minimum one file - :rtype: bool + :return: tuple of bool and result list. True, if the directory exists and contains minimum one file + :rtype: tuple """ + # Variable deklaration + magnetic_result_numbers: list[str] = [] + is_magnetic_list_generated = False + # Check if target folder 09_circuit_dtos_incl_inductor_losses is created if os.path.exists(act_dir_name): # Create list of filespath @@ -116,18 +118,18 @@ def _generate_number_list(act_dir_name: str, act_device_numbers: list[str]) -> b # Check file type extension = os.path.splitext(os.path.basename(file_name))[1] if extension == '.pkl': - act_device_numbers.append(device_number) + magnetic_result_numbers.append(device_number) else: print(f"File {device_number}{extension} has no extension '.pkl'!") else: print(f"File'{file_path}' does not exists!") else: - print("Path 'act_dir_name' does not exists!") + print(f"Path {act_dir_name} does not exists!") - if len(act_device_numbers) > 0: - return True - else: - return False + if not magnetic_result_numbers: + is_magnetic_list_generated = True + + return is_magnetic_list_generated, magnetic_result_numbers @staticmethod def generate_result_database(act_ginfo: dct.GeneralInformation, act_inductor_study_names: list[str], @@ -135,7 +137,7 @@ def generate_result_database(act_ginfo: dct.GeneralInformation, act_inductor_stu """Generate a database df by summaries the calculation results. :param act_ginfo : General information about the study - :type act_ginfo : dct.GeneralInformation: + :type act_ginfo : dct.GeneralInformation :param act_inductor_study_names : List of names with inductor studies which are to process :type act_inductor_study_names : list[str] :param act_stacked_transformer_study_names : List of names with transformer studies which are to process @@ -200,14 +202,16 @@ def generate_result_database(act_ginfo: dct.GeneralInformation, act_inductor_stu inductor_study_name, "09_circuit_dtos_incl_inductor_losses") - # Check, if inductor number list cannot be generated - if not DctSummmaryProcessing._generate_number_list(inductor_filepath_results, inductor_numbers): + # Generate magnetic list + is_inductor_list_generated, inductor_full_operating_range_list = ( + DctSummmaryProcessing._generate_number_list(inductor_filepath_results)) + if not is_inductor_list_generated: print(f"Path {inductor_filepath_results} does not exists or does not contains any pkl-files!") # Next circuit continue # iterate inductor numbers - for inductor_number in inductor_numbers: + for inductor_number in inductor_full_operating_range_list: inductor_filepath_number = os.path.join(inductor_filepath_results, f"{inductor_number}.pkl") # Get inductor results @@ -231,14 +235,15 @@ def generate_result_database(act_ginfo: dct.GeneralInformation, act_inductor_stu "09_circuit_dtos_incl_transformer_losses") # Check, if stacked transformer number list cannot be generated - if not DctSummmaryProcessing._generate_number_list(stacked_transformer_filepath_results, - stacked_transformer_numbers): + is_transformer_list_generated, stacked_transformer_full_operating_range_list = ( + DctSummmaryProcessing._generate_number_list(stacked_transformer_filepath_results)) + if not is_transformer_list_generated: print(f"Path {stacked_transformer_filepath_results} does not exists or does not contains any pkl-files!") # Next circuit continue # iterate transformer numbers - for stacked_transformer_number in stacked_transformer_numbers: + for stacked_transformer_number in stacked_transformer_full_operating_range_list: stacked_transformer_filepath_number = os.path.join(stacked_transformer_filepath_results, f"{stacked_transformer_number}.pkl") # get transformer results @@ -266,10 +271,10 @@ def generate_result_database(act_ginfo: dct.GeneralInformation, act_inductor_stu max_loss_inductor_index = np.unravel_index(inductance_loss_matrix.argmax(), np.shape(inductance_loss_matrix)) max_loss_transformer_index = np.unravel_index(transformer_loss_matrix.argmax(), np.shape(transformer_loss_matrix)) - r_th_ind_heat_sink = DctSummmaryProcessing.r_th_ind_heat_sink_A / inductor_dto.area_to_heat_sink + r_th_ind_heat_sink = DctSummmaryProcessing.r_th_ind_heat_sink_a / inductor_dto.area_to_heat_sink temperature_inductor_heat_sink_max_matrix = 125 - r_th_ind_heat_sink * inductance_loss_matrix - r_th_xfmr_heat_sink = DctSummmaryProcessing.r_th_xfmr_heat_sink_A / transformer_dto.area_to_heat_sink + r_th_xfmr_heat_sink = DctSummmaryProcessing.r_th_xfmr_heat_sink_a / transformer_dto.area_to_heat_sink temperature_xfmr_heat_sink_max_matrix = 125 - r_th_xfmr_heat_sink * transformer_loss_matrix # maximum heat sink temperatures (minimum of all the maximum temperatures of single components) @@ -339,10 +344,9 @@ def generate_result_database(act_ginfo: dct.GeneralInformation, act_inductor_stu df = pd.concat([df, local_df], axis=0) # Calculate the total area as sum of circuit, inductor and transformer area df-comand is like vector sum v1[:]=v2[:]+v3[:]) - # df["total_area"] = df["circuit_area"] + df["inductor_area"] + df["transformer_area"] - df["total_area"] = df["inductor_area"] + df["transformer_area"] - # df["total_mean_loss"] = df["circuit_mean_loss"] + df["inductor_mean_loss"] + df["transformer_mean_loss"] - # df["volume_wo_heat_sink"] = df["transformer_volume"] + df["inductor_volume"] + df["total_area"] = df["circuit_area"] + df["inductor_area"] + df["transformer_area"] + df["total_mean_loss"] = df["circuit_mean_loss"] + df["inductor_mean_loss"] + df["transformer_mean_loss"] + df["volume_wo_heat_sink"] = df["transformer_volume"] + df["inductor_volume"] # Save results to file (ASA : later to store only on demand) df.to_csv(f"{act_ginfo.heat_sink_study_path}/result_df.csv") @@ -350,7 +354,7 @@ def generate_result_database(act_ginfo: dct.GeneralInformation, act_inductor_stu return df @staticmethod - def select_heatsink_configuration(act_ginfo: dct.GeneralInformation, act_df_for_hs: pd.DataFrame): + def select_heat_sink_configuration(act_ginfo: dct.GeneralInformation, act_df_for_hs: pd.DataFrame): """Select the heatsink configuration from calculated heatsink pareto front. :param act_ginfo : General information about the study name and study path @@ -378,4 +382,4 @@ def select_heatsink_configuration(act_ginfo: dct.GeneralInformation, act_df_for_ + act_df_for_hs["heat_sink_volume"] # save full summary - # df_wo_hs.to_csv(f"{act_ginfo.heatsink_study_path}/df_summary.csv") + # df_wo_hs.to_csv(f"{act_ginfo.heat_sink_study_path}/df_summary.csv") diff --git a/dct/toml_checker.py b/dct/toml_checker.py index 4f4ab1a..ed80505 100644 --- a/dct/toml_checker.py +++ b/dct/toml_checker.py @@ -278,4 +278,4 @@ class TomlHeatSink(BaseModel): design_space: TomlHeatSinkDesignSpace settings: TomlHeatSinkSettings boundary_conditions: TomlHeatSinkBoundaryConditions - ThermalResistanceData: TomlHeatSinkSummaryData + thermal_resistance_data: TomlHeatSinkSummaryData diff --git a/dct/transformer_optimization.py b/dct/transformer_optimization.py index 90cb3b1..3682264 100644 --- a/dct/transformer_optimization.py +++ b/dct/transformer_optimization.py @@ -282,7 +282,7 @@ def _simulation(circuit_id: int, act_sto_config: fmt.StoSingleInputConfig, act_g @staticmethod # Simulation handler. Later the simulation handler starts a process per list entry. def simulation_handler(act_ginfo: type[dct.GeneralInformation], target_number_trials: int, - factor_dc_min_losses: float = 1.0, factor_dc_max_losses: float = 100, delete_study: bool = False, + factor_dc_min_losses: float = 1.0, factor_dc_max_losses: float = 100, enable_delete_study: bool = False, re_simulate: bool = False, debug: bool = False): """ Control the multi simulation processes. @@ -295,8 +295,8 @@ def simulation_handler(act_ginfo: type[dct.GeneralInformation], target_number_tr :type factor_dc_min_losses : float :param factor_dc_max_losses: Filter factor for the maximum losses, related to the minimum DC losses :type factor_dc_max_losses: float - :param delete_study: Flag, which indicates to delete the study - :type delete_study: bool + :param enable_delete_study: Flag, which indicates to delete the study + :type enable_delete_study: bool :param re_simulate : Flag to control, if the point are to re-simulate (ASA: Correct the parameter description) :type re_simulate : bool :param debug : Debug mode flag @@ -312,7 +312,7 @@ def simulation_handler(act_ginfo: type[dct.GeneralInformation], target_number_tr target_number_trials = 100 # Check the deleteStudyFlag - if delete_study: + if enable_delete_study: # Create path-filename of sqlite database stacked_transformer_study_sqlite_database = ( os.path.join(act_sim_config[1].stacked_transformer_optimization_directory, diff --git a/examples/pareto_summary_wo_hs.py b/examples/pareto_summary_wo_hs.py index dadaa02..a39be98 100644 --- a/examples/pareto_summary_wo_hs.py +++ b/examples/pareto_summary_wo_hs.py @@ -50,7 +50,7 @@ ) -heat_sink = dct.HeatSinkTemp( +heat_sink_boundary_conditions = dct.HeatSinkBoundaryConditions( t_ambient=40, t_hs_max=90, ) @@ -209,10 +209,10 @@ def hover(event): t_min_matrix = np.minimum(circuit_heat_sink_max_1_matrix, circuit_heat_sink_max_2_matrix) t_min_matrix = np.minimum(t_min_matrix, temperature_inductor_heat_sink_max_matrix) t_min_matrix = np.minimum(t_min_matrix, temperature_xfmr_heat_sink_max_matrix) - t_min_matrix = np.minimum(t_min_matrix, heat_sink.t_hs_max) + t_min_matrix = np.minimum(t_min_matrix, heat_sink_boundary_conditions.t_hs_max) # maximum delta temperature over the heat sink - delta_t_max_heat_sink_matrix = t_min_matrix - heat_sink.t_ambient + delta_t_max_heat_sink_matrix = t_min_matrix - heat_sink_boundary_conditions.t_ambient r_th_heat_sink_target_matrix = delta_t_max_heat_sink_matrix / total_loss_matrix diff --git a/pyproject.toml b/pyproject.toml index 4c16bbb..0f034d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = [ ] description = "Power electroincs DAB converter optimization." readme = "README.rst" -requires-python = "~=3.10" +requires-python = ">=3.11" classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", diff --git a/workspace/DabHeatSinkConf.toml b/workspace/DabHeatSinkConf.toml index c91b203..65da153 100644 --- a/workspace/DabHeatSinkConf.toml +++ b/workspace/DabHeatSinkConf.toml @@ -17,7 +17,7 @@ # W/(m*K) thermal_conductivity_copper = 136 -[ThermalResistanceData] +[thermal_resistance_data] # [tim_thickness, tim_conductivity] transistor_b1_cooling = [1e-3,12.0] transistor_b2_cooling = [1e-3,12.0] From e6c927ce4942a2669e0a33d1607c1057eb264979 Mon Sep 17 00:00:00 2001 From: SevenOfNinePE Date: Fri, 11 Apr 2025 20:37:16 +0200 Subject: [PATCH 7/8] Solve the df-issue (conversation about save full summary) Solve the special case bug by replacing the enable_delete_study out of the optimizer function to the flow control. This was necessary because different dependencies of the studys. (e.g. if circuit study is deleted also inductor and transformer studyn needs to be deleted too.) --- dct/circuit_optimization.py | 15 +---- dct/dctmainctl.py | 98 +++++++++++++++++++-------------- dct/heat_sink_optimization.py | 22 +------- dct/inductor_optimization.py | 13 +---- dct/summary_processing.py | 12 ++-- dct/transformer_optimization.py | 14 +---- 6 files changed, 70 insertions(+), 104 deletions(-) diff --git a/dct/circuit_optimization.py b/dct/circuit_optimization.py index 5433606..583eb1e 100644 --- a/dct/circuit_optimization.py +++ b/dct/circuit_optimization.py @@ -5,7 +5,6 @@ import logging import json import pickle -import shutil # 3rd party libraries import optuna @@ -232,8 +231,7 @@ def run_optimization_mysql(act_storage_url: str, act_study_name: str, act_number @staticmethod def start_proceed_study(dab_config: p_dtos.CircuitParetoDabDesign, number_trials: int, database_type: str = 'sqlite', - sampler=optuna.samplers.NSGAIIISampler(), - enable_delete_study: bool = False + sampler=optuna.samplers.NSGAIIISampler() ): """Proceed a study which is stored as sqlite database. @@ -245,8 +243,6 @@ def start_proceed_study(dab_config: p_dtos.CircuitParetoDabDesign, number_trials :type database_type: str :param sampler: optuna.samplers.NSGAIISampler() or optuna.samplers.NSGAIIISampler(). Note about the brackets () !! Default: NSGAIII :type sampler: optuna.sampler-object - :param enable_delete_study: Indication, if the old study are to delete (True) or optimization shall be continued. - :type enable_delete_study: bool """ filepaths = CircuitOptimization.load_filepaths(dab_config.project_directory) @@ -289,15 +285,6 @@ def start_proceed_study(dab_config: p_dtos.CircuitParetoDabDesign, number_trials # Means, in total there are four slashes including the path itself '////home/.../database.sqlite3' storage = f"sqlite:///{circuit_study_sqlite_database}" - # Check the deleteStudyFlag - if enable_delete_study and os.path.exists(circuit_study_sqlite_database): - with os.scandir(circuit_study_working_directory) as entries: - for entry in entries: - if entry.is_dir() and not entry.is_symlink(): - shutil.rmtree(entry.path) - else: - os.remove(entry.path) - # Create study object in drive study_in_storage = optuna.create_study(study_name=dab_config.circuit_study_name, storage=storage, diff --git a/dct/dctmainctl.py b/dct/dctmainctl.py index 65b74c4..3df4985 100644 --- a/dct/dctmainctl.py +++ b/dct/dctmainctl.py @@ -1,6 +1,7 @@ """Main control program to optimise the DAB converter.""" # python libraries import os +import shutil import sys import tomllib @@ -91,13 +92,40 @@ def generate_conf_file(path: str) -> bool: DabElectricConf.toml, DabInductorConf.toml, DabTransformerConf.toml and DabHeatSinkConf.toml, :param path : Location of the configuration - :type path : str: + :type path : str :return: true, if the files are stored successfully :rtype: bool """ return False + @staticmethod + def delete_study_content(folder_name: str, study_file_name: str = ""): + """ + Delete the study files and the femmt folders. + + If a new study is to generate the old obsolete files and folders needs to be deleted. + + :param folder_name : Location of the study files + :type folder_name : str + :param study_file_name : Name of the study files (without extension) + :type study_file_name : str + """ + # Check if folder exists + if os.path.exists(folder_name): + # Delete all content of the folder + for item in os.listdir(folder_name): + # Create the full pathname + full_path = os.path.join(folder_name, item) + # Check if it is a folder + if os.path.isdir(full_path): + # Delete the folder + shutil.rmtree(full_path) + # Check if it is the target file name + elif os.path.isfile(full_path) and os.path.splitext(item)[0] == study_file_name: + # Delete this file + os.remove(full_path) + @staticmethod def user_input_break_point(break_point_key: str, info: str): """ @@ -311,12 +339,6 @@ def run_optimization_from_toml_configs(workspace_path: str): DctMainCtl.set_up_folder_structure(toml_prog_flow) - # read study names from toml file names - circuit_study_name = toml_prog_flow.configuration_data_files.circuit_configuration_file.replace(".toml", "") - inductor_study_name = toml_prog_flow.configuration_data_files.inductor_configuration_file.replace(".toml", "") - transformer_study_name = toml_prog_flow.configuration_data_files.transformer_configuration_file.replace(".toml", "") - heat_sink_study_name = toml_prog_flow.configuration_data_files.heat_sink_configuration_file.replace(".toml", "") - # -------------------------- # Circuit flow control # -------------------------- @@ -372,7 +394,7 @@ def run_optimization_from_toml_configs(workspace_path: str): # Check, if data are available (skip case) if not DctMainCtl.check_study_data(inductor_results_datapath, ginfo.inductor_study_name): raise ValueError( - f"Study {inductor_study_name} in path {inductor_results_datapath} does not exist. No sqlite3-database found!") + f"Study {ginfo.inductor_study_name} in path {inductor_results_datapath} does not exist. No sqlite3-database found!") # -------------------------- # Transformer flow control @@ -414,7 +436,7 @@ def run_optimization_from_toml_configs(workspace_path: str): # Check, if heat sink optimization is to skip if toml_prog_flow.heat_sink.calculation_mode == "skip": # Assemble pathname - heat_sink_results_datapath = os.path.join(ginfo.heat_sink_study_path, heat_sink_study_name) + heat_sink_results_datapath = os.path.join(ginfo.heat_sink_study_path, ginfo.heat_sink_study_name) # Check, if data are available (skip case) if not DctMainCtl.check_study_data(heat_sink_results_datapath, ginfo.heat_sink_study_name): raise ValueError( @@ -439,21 +461,25 @@ def run_optimization_from_toml_configs(workspace_path: str): raise ValueError("Electrical configuration not initialized!") # Check, if old study is to delete, if available if toml_prog_flow.circuit.calculation_mode == "new": - # delete old study - is_new_circuit_study = True - else: - # overtake the trails of the old study - is_new_circuit_study = False + # delete old circuit study data + DctMainCtl.delete_study_content(os.path.join(ginfo.circuit_study_path, ginfo.circuit_study_name), ginfo.circuit_study_name) + filtered_circuit_results_datapath = os.path.join(ginfo.circuit_study_path, ginfo.circuit_study_name, + "filtered_results") + # Create the filtered result folder + os.makedirs(filtered_circuit_results_datapath, exist_ok=True) + filtered_circuit_result_folder_exists = True + # Delete obsolete folders of inductor and transformer + DctMainCtl.delete_study_content(ginfo.inductor_study_path) + DctMainCtl.delete_study_content(ginfo.transformer_study_path) # Start calculation - dct.CircuitOptimization.start_proceed_study(config_circuit, number_trials=toml_prog_flow.circuit.number_of_trials, - enable_delete_study=is_new_circuit_study) + dct.CircuitOptimization.start_proceed_study(config_circuit, number_trials=toml_prog_flow.circuit.number_of_trials) # Check breakpoint DctMainCtl.check_breakpoint(toml_prog_flow.breakpoints.circuit_pareto, "Electric Pareto front calculated") - # Check if filter results are not available - if not filtered_circuit_result_folder_exists: + # Check, if electrical optimization is not to skip + if not toml_prog_flow.circuit.calculation_mode == "skip": # Calculate the filtered results CircuitOptimization.filter_study_results(dab_config=config_circuit) # Get filtered result path @@ -470,20 +496,17 @@ def run_optimization_from_toml_configs(workspace_path: str): # Inductor optimization # -------------------------- - # Check, if inductor optimization is not to skip - if not toml_prog_flow.inductor.calculation_mode == "skip": + # Check, if inductor optimization is not to skip (cannot be skipped if circuit calculation mode is new) + if not toml_prog_flow.inductor.calculation_mode == "skip" or toml_prog_flow.circuit.calculation_mode == "new": # Check, if old study is to delete, if available if toml_prog_flow.inductor.calculation_mode == "new": - # delete old study - is_new_inductor_study = True - else: - # overtake the trails of the old study - is_new_inductor_study = False + # Delete old inductor study + DctMainCtl.delete_study_content(ginfo.inductor_study_path) # Start simulation ASA: Filter_factor to correct isim.init_configuration(toml_inductor, toml_prog_flow, ginfo) isim.simulation_handler(ginfo, toml_prog_flow.inductor.number_of_trials, toml_inductor.filter_distance.factor_min_dc_losses, - toml_inductor.filter_distance.factor_max_dc_losses, is_new_inductor_study, enable_ind_re_simulation) + toml_inductor.filter_distance.factor_max_dc_losses, enable_ind_re_simulation) # Check breakpoint DctMainCtl.check_breakpoint(toml_prog_flow.breakpoints.inductor, "Inductor Pareto front calculated") @@ -492,21 +515,19 @@ def run_optimization_from_toml_configs(workspace_path: str): # Transformer optimization # -------------------------- - # Check, if transformer optimization is not to skip - if not toml_prog_flow.transformer.calculation_mode == "skip": + # Check, if transformer optimization is not to skip (cannot be skipped if circuit calculation mode is new) + if not toml_prog_flow.transformer.calculation_mode == "skip" or toml_prog_flow.circuit.calculation_mode == "new": # Check, if old study is to delete, if available if toml_prog_flow.transformer.calculation_mode == "new": - # delete old study - is_new_transformer_study = True - else: - # overtake the trails of the old study - is_new_transformer_study = False + # Delete old transformer study + DctMainCtl.delete_study_content(ginfo.transformer_study_path) + # Initialize transformer configuration tsim.init_configuration(toml_transformer, toml_prog_flow, ginfo) # Perform transformer optimization tsim.simulation_handler(ginfo, toml_prog_flow.transformer.number_of_trials, toml_transformer.filter_distance.factor_min_dc_losses, - toml_transformer.filter_distance.factor_max_dc_losses, is_new_transformer_study, enable_trans_re_simulation) + toml_transformer.filter_distance.factor_max_dc_losses, enable_trans_re_simulation) # Check breakpoint DctMainCtl.check_breakpoint(toml_prog_flow.breakpoints.transformer, "Transformer Pareto front calculated") @@ -519,15 +540,12 @@ def run_optimization_from_toml_configs(workspace_path: str): if not toml_prog_flow.heat_sink.calculation_mode == "skip": # Check, if old study is to delete, if available if toml_prog_flow.heat_sink.calculation_mode == "new": - # delete old study - is_new_heat_sink_study = True - else: - # overtake the trails of the old study - is_new_heat_sink_study = False + # Delete old heatsink study + DctMainCtl.delete_study_content(os.path.join(ginfo.heat_sink_study_path, ginfo.heat_sink_study_name), ginfo.heat_sink_study_name) hsim.init_configuration(toml_heat_sink, toml_prog_flow) # Perform heat sink optimization - hsim.optimization_handler(ginfo, toml_prog_flow.heat_sink.number_of_trials, is_new_heat_sink_study) + hsim.optimization_handler(ginfo, toml_prog_flow.heat_sink.number_of_trials) # Check breakpoint DctMainCtl.check_breakpoint(toml_prog_flow.breakpoints.heat_sink, "Heat sink Pareto front calculated") diff --git a/dct/heat_sink_optimization.py b/dct/heat_sink_optimization.py index 554331b..9a1b07c 100644 --- a/dct/heat_sink_optimization.py +++ b/dct/heat_sink_optimization.py @@ -1,7 +1,6 @@ """Inductor optimization class.""" # python libraries import os -import shutil # 3rd party libraries @@ -136,27 +135,15 @@ def calculate_r_th_tim(copper_coin_bot_area: float, transistor_cooling: Transist # Simulation handler. Later the simulation handler starts a process per list entry. @staticmethod - def _simulation(act_hct_config: hct.OptimizationParameters, - target_number_trials: int, enable_delete_study: bool, debug: bool): + def _simulation(act_hct_config: hct.OptimizationParameters, target_number_trials: int, debug: bool): """ Perform the simulation. :param target_number_trials: Number of trials for the optimization :type target_number_trials: int - :param enable_delete_study: True to delete the existing study and start a new one - :type enable_delete_study: bool :param debug: Debug mode flag :type debug: bool """ - # delete existing study - if enable_delete_study and os.path.exists(act_hct_config.heat_sink_optimization_directory): - with os.scandir(act_hct_config.heat_sink_optimization_directory) as entries: - for entry in entries: - if entry.is_dir() and not entry.is_symlink(): - shutil.rmtree(entry.path) - else: - os.remove(entry.path) - # Check number of trials if target_number_trials > 0: print(f"{HeatSinkOptimization.optimization_config_list=}") @@ -170,8 +157,7 @@ def _simulation(act_hct_config: hct.OptimizationParameters, # Simulation handler. Later the simulation handler starts a process per list entry. @staticmethod - def optimization_handler(act_ginfo: type[dct.GeneralInformation], target_number_trials: int, - enable_delete_study: bool = False, debug: bool = False): + def optimization_handler(act_ginfo: type[dct.GeneralInformation], target_number_trials: int, debug: bool = False): """ Control the multi simulation processes. @@ -179,8 +165,6 @@ def optimization_handler(act_ginfo: type[dct.GeneralInformation], target_number_ :type act_ginfo: dct.GeneralInformation: :param target_number_trials: Number of trials for the optimization :type target_number_trials: int - :param enable_delete_study: True to delete the existing study and start a new one - :type enable_delete_study: bool :param debug: Debug mode flag :type debug: bool """ @@ -194,7 +178,7 @@ def optimization_handler(act_ginfo: type[dct.GeneralInformation], target_number_ if target_number_trials > 100: target_number_trials = 100 - HeatSinkOptimization._simulation(act_sim_config, target_number_trials, enable_delete_study, debug) + HeatSinkOptimization._simulation(act_sim_config, target_number_trials, debug) if debug: # stop after one circuit run break diff --git a/dct/inductor_optimization.py b/dct/inductor_optimization.py index a972f30..f30250c 100644 --- a/dct/inductor_optimization.py +++ b/dct/inductor_optimization.py @@ -240,7 +240,7 @@ def _simulation(circuit_id: int, act_io_config: fmt.InductorOptimizationDTO, act # Simulation handler. Later the simulation handler starts a process per list entry. @staticmethod def simulation_handler(act_ginfo: type[dct.GeneralInformation], target_number_trials: int, - factor_min_dc_losses: float = 1.0, factor_dc_max_losses: float = 100, enable_delete_study: bool = False, + factor_min_dc_losses: float = 1.0, factor_dc_max_losses: float = 100, re_simulate: bool = False, debug: bool = False): """ Control the multi simulation processes. @@ -253,8 +253,6 @@ def simulation_handler(act_ginfo: type[dct.GeneralInformation], target_number_tr :type factor_min_dc_losses : float :param factor_dc_max_losses: Filter factor for the maximum losses, related to the minimum DC losses :type factor_dc_max_losses: float - :param enable_delete_study: Flag, which indicates to delete the study - :type enable_delete_study: bool :param re_simulate : Flag to control, if the point are to re-simulate (ASA: Correct the parameter description) :type re_simulate : bool :param debug : Debug mode flag @@ -269,15 +267,6 @@ def simulation_handler(act_ginfo: type[dct.GeneralInformation], target_number_tr if target_number_trials > 100: target_number_trials = 100 - # Check the deleteStudyFlag - if enable_delete_study: - # Create path-filename of sqlite database - inductor_study_sqlite_database = os.path.join(act_sim_config[1].inductor_optimization_directory, - f"{act_sim_config[1].inductor_study_name}.sqlite3") - # Check if path-filename exists - if os.path.exists(inductor_study_sqlite_database): - os.remove(inductor_study_sqlite_database) - InductorOptimization._simulation(act_sim_config[0], act_sim_config[1], act_ginfo, target_number_trials, factor_min_dc_losses, factor_dc_max_losses, re_simulate, debug) diff --git a/dct/summary_processing.py b/dct/summary_processing.py index d09e679..428a401 100644 --- a/dct/summary_processing.py +++ b/dct/summary_processing.py @@ -85,8 +85,8 @@ def init_thermal_configuration(act_thermal_data: dct.TomlHeatSinkSummaryData) -> successful_init = False # Heat sink parameter: List [t_ambient, t_hs_max] - DctSummmaryProcessing.heat_sink = dct.HeatSinkTemp(t_ambient=act_thermal_data.heat_sink[0], - t_hs_max=act_thermal_data.heat_sink[1]) + DctSummmaryProcessing.heat_sink = dct.HeatSinkBoundaryConditions(t_ambient=act_thermal_data.heat_sink[0], + t_hs_max=act_thermal_data.heat_sink[1]) # Return if initialisation was successful performed (True) return successful_init @@ -126,7 +126,7 @@ def _generate_magnetic_number_list(act_dir_name: str) -> tuple[bool, list[str]]: else: print(f"Path {act_dir_name} does not exists!") - if not magnetic_result_numbers: + if magnetic_result_numbers: is_magnetic_list_generated = True return is_magnetic_list_generated, magnetic_result_numbers @@ -204,7 +204,7 @@ def generate_result_database(act_ginfo: dct.GeneralInformation, act_inductor_stu # Generate magnetic list is_inductor_list_generated, inductor_full_operating_range_list = ( - DctSummmaryProcessing._generate_number_list(inductor_filepath_results)) + DctSummmaryProcessing._generate_magnetic_number_list(inductor_filepath_results)) if not is_inductor_list_generated: print(f"Path {inductor_filepath_results} does not exists or does not contains any pkl-files!") # Next circuit @@ -236,7 +236,7 @@ def generate_result_database(act_ginfo: dct.GeneralInformation, act_inductor_stu # Check, if stacked transformer number list cannot be generated is_transformer_list_generated, stacked_transformer_full_operating_range_list = ( - DctSummmaryProcessing._generate_number_list(stacked_transformer_filepath_results)) + DctSummmaryProcessing._generate_magnetic_number_list(stacked_transformer_filepath_results)) if not is_transformer_list_generated: print(f"Path {stacked_transformer_filepath_results} does not exists or does not contains any pkl-files!") # Next circuit @@ -382,4 +382,4 @@ def select_heat_sink_configuration(act_ginfo: dct.GeneralInformation, act_df_for + act_df_for_hs["heat_sink_volume"] # save full summary - # df_wo_hs.to_csv(f"{act_ginfo.heat_sink_study_path}/df_summary.csv") + act_df_for_hs.to_csv(f"{act_ginfo.heat_sink_study_path}/df_summary.csv") diff --git a/dct/transformer_optimization.py b/dct/transformer_optimization.py index 3682264..0827c44 100644 --- a/dct/transformer_optimization.py +++ b/dct/transformer_optimization.py @@ -282,7 +282,7 @@ def _simulation(circuit_id: int, act_sto_config: fmt.StoSingleInputConfig, act_g @staticmethod # Simulation handler. Later the simulation handler starts a process per list entry. def simulation_handler(act_ginfo: type[dct.GeneralInformation], target_number_trials: int, - factor_dc_min_losses: float = 1.0, factor_dc_max_losses: float = 100, enable_delete_study: bool = False, + factor_dc_min_losses: float = 1.0, factor_dc_max_losses: float = 100, re_simulate: bool = False, debug: bool = False): """ Control the multi simulation processes. @@ -295,8 +295,6 @@ def simulation_handler(act_ginfo: type[dct.GeneralInformation], target_number_tr :type factor_dc_min_losses : float :param factor_dc_max_losses: Filter factor for the maximum losses, related to the minimum DC losses :type factor_dc_max_losses: float - :param enable_delete_study: Flag, which indicates to delete the study - :type enable_delete_study: bool :param re_simulate : Flag to control, if the point are to re-simulate (ASA: Correct the parameter description) :type re_simulate : bool :param debug : Debug mode flag @@ -311,16 +309,6 @@ def simulation_handler(act_ginfo: type[dct.GeneralInformation], target_number_tr if target_number_trials > 100: target_number_trials = 100 - # Check the deleteStudyFlag - if enable_delete_study: - # Create path-filename of sqlite database - stacked_transformer_study_sqlite_database = ( - os.path.join(act_sim_config[1].stacked_transformer_optimization_directory, - f"{act_sim_config[1].stacked_transformer_study_name}.sqlite3")) - # Check if path-filename exists - if os.path.exists(stacked_transformer_study_sqlite_database): - os.remove(stacked_transformer_study_sqlite_database) - TransformerOptimization._simulation(act_sim_config[0], act_sim_config[1], act_ginfo, target_number_trials, factor_dc_min_losses, factor_dc_max_losses, re_simulate, debug) From 9cf036c823c4f6ecc1a6d5bfc0d23aa6f3b1d5af Mon Sep 17 00:00:00 2001 From: SevenOfNinePE Date: Mon, 14 Apr 2025 11:56:24 +0200 Subject: [PATCH 8/8] Update remaining conversations after clarification: Replace hardcoded folder names by subdirectory Update variable name for thermal resistance per unit area and comment it is suitable way Fix the heat_sink variable. --- dct/dctmainctl.py | 16 ++++++++-------- dct/summary_processing.py | 35 +++++++++++++++++++---------------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/dct/dctmainctl.py b/dct/dctmainctl.py index 3df4985..319c32b 100644 --- a/dct/dctmainctl.py +++ b/dct/dctmainctl.py @@ -35,10 +35,10 @@ def set_up_folder_structure(toml_prog_flow: tc.FlowControl) -> None: """ # ASA: Merge ginfo and set_up_folder_structure Fix file structure on top layer project_directory = os.path.abspath(toml_prog_flow.general.project_directory) - circuit_path = os.path.join(project_directory, "01_circuit") - inductor_path = os.path.join(project_directory, "02_inductor") - transformer_path = os.path.join(project_directory, "03_transformer") - heat_sink_path = os.path.join(project_directory, "04_heat_sink") + circuit_path = os.path.join(project_directory, toml_prog_flow.circuit.subdirectory) + inductor_path = os.path.join(project_directory, toml_prog_flow.inductor.subdirectory) + transformer_path = os.path.join(project_directory, toml_prog_flow.transformer.subdirectory) + heat_sink_path = os.path.join(project_directory, toml_prog_flow.heat_sink.subdirectory) path_dict = {'circuit': circuit_path, 'inductor': inductor_path, @@ -167,10 +167,10 @@ def init_general_info(act_config_program_flow: tc.FlowControl) -> dct.GeneralInf heat_sink_study_name=act_config_program_flow .configuration_data_files.heat_sink_configuration_file.replace(".toml", ""), # Set remaining elements with dummy names - circuit_study_path=os.path.join(project_directory, "01_circuit"), - inductor_study_path=os.path.join(project_directory, "02_inductor"), - transformer_study_path=os.path.join(project_directory, "03_transformer"), - heat_sink_study_path=os.path.join(project_directory, "04_heat_sink")) + circuit_study_path=os.path.join(project_directory, act_config_program_flow.circuit.subdirectory), + inductor_study_path=os.path.join(project_directory, act_config_program_flow.inductor.subdirectory), + transformer_study_path=os.path.join(project_directory, act_config_program_flow.transformer.subdirectory), + heat_sink_study_path=os.path.join(project_directory, act_config_program_flow.heat_sink.subdirectory)) # Return the result return r_ginfo diff --git a/dct/summary_processing.py b/dct/summary_processing.py index 428a401..52fef8f 100644 --- a/dct/summary_processing.py +++ b/dct/summary_processing.py @@ -25,11 +25,11 @@ class DctSummmaryProcessing: transistor_b2_cooling: float # Thermal resistance - r_th_ind_heat_sink_a: float - r_th_xfmr_heat_sink_a: float + r_th_per_unit_area_ind_heat_sink: float + r_th_per_unit_area_xfmr_heat_sink: float - # Heat sink parameter - heat_sink = None + # Heat sink boudary condition parameter + heat_sink_boundary_conditions: dct.HeatSinkBoundaryConditions @staticmethod def init_thermal_configuration(act_thermal_data: dct.TomlHeatSinkSummaryData) -> bool: @@ -61,8 +61,9 @@ def init_thermal_configuration(act_thermal_data: dct.TomlHeatSinkSummaryData) -> # Check on zero if inductor_tim_conductivity > 0: - # Calculate the thermal resistance area product - DctSummmaryProcessing.r_th_ind_heat_sink_a = inductor_tim_thickness / inductor_tim_conductivity + # Calculate the thermal resistance per unit area as term from the formula r_th = 1/lambda * l / A + # r_th_per_unit_area_ind_heat_sink = 1/lambda * l. Later r_th = r_th_per_unit_area_ind_heat_sink / A + DctSummmaryProcessing.r_th_per_unit_area_ind_heat_sink = inductor_tim_thickness / inductor_tim_conductivity else: print(f"inductor cooling tim conductivity value must be greater zero, but is {inductor_tim_conductivity}!") successful_init = False @@ -78,15 +79,16 @@ def init_thermal_configuration(act_thermal_data: dct.TomlHeatSinkSummaryData) -> ) # Check on zero ( ASA: Maybe in general all configurtation files are to check for validity in advanced. In this case the check can be removed.) if transformer_tim_conductivity > 0: - # Calculate the thermal resistance area product - DctSummmaryProcessing.r_th_xfmr_heat_sink_a = transformer_tim_thickness / transformer_tim_conductivity + # Calculate the thermal resistance per unit area as term from the formula r_th = 1/lambda * l / A + # r_th_per_unit_area_xfmr_heat_sink = 1/lambda * l. Later r_th = r_th_per_unit_area_xfmr_heat_sink / A + DctSummmaryProcessing.r_th_per_unit_area_xfmr_heat_sink = transformer_tim_thickness / transformer_tim_conductivity else: print(f"transformer cooling tim conductivity value must be greater zero, but is {transformer_tim_conductivity}!") successful_init = False # Heat sink parameter: List [t_ambient, t_hs_max] - DctSummmaryProcessing.heat_sink = dct.HeatSinkBoundaryConditions(t_ambient=act_thermal_data.heat_sink[0], - t_hs_max=act_thermal_data.heat_sink[1]) + DctSummmaryProcessing.heat_sink_boundary_conditions = dct.HeatSinkBoundaryConditions(t_ambient=act_thermal_data.heat_sink[0], + t_hs_max=act_thermal_data.heat_sink[1]) # Return if initialisation was successful performed (True) return successful_init @@ -270,21 +272,22 @@ def generate_result_database(act_ginfo: dct.GeneralInformation, act_inductor_stu max_loss_inductor_index = np.unravel_index(inductance_loss_matrix.argmax(), np.shape(inductance_loss_matrix)) max_loss_transformer_index = np.unravel_index(transformer_loss_matrix.argmax(), np.shape(transformer_loss_matrix)) - - r_th_ind_heat_sink = DctSummmaryProcessing.r_th_ind_heat_sink_a / inductor_dto.area_to_heat_sink + # Calculate the thermal resistance according r_th = 1/lambda * l / A + # For inductor: r_th_per_unit_area_ind_heat_sink = 1/lambda * l + r_th_ind_heat_sink = DctSummmaryProcessing.r_th_per_unit_area_ind_heat_sink / inductor_dto.area_to_heat_sink temperature_inductor_heat_sink_max_matrix = 125 - r_th_ind_heat_sink * inductance_loss_matrix - - r_th_xfmr_heat_sink = DctSummmaryProcessing.r_th_xfmr_heat_sink_a / transformer_dto.area_to_heat_sink + # For transformer: r_th_per_unit_area_xfmr_heat_sink = 1/lambda * l. + r_th_xfmr_heat_sink = DctSummmaryProcessing.r_th_per_unit_area_xfmr_heat_sink / transformer_dto.area_to_heat_sink temperature_xfmr_heat_sink_max_matrix = 125 - r_th_xfmr_heat_sink * transformer_loss_matrix # maximum heat sink temperatures (minimum of all the maximum temperatures of single components) t_min_matrix = np.minimum(circuit_heat_sink_max_1_matrix, circuit_heat_sink_max_2_matrix) t_min_matrix = np.minimum(t_min_matrix, temperature_inductor_heat_sink_max_matrix) t_min_matrix = np.minimum(t_min_matrix, temperature_xfmr_heat_sink_max_matrix) - t_min_matrix = np.minimum(t_min_matrix, DctSummmaryProcessing.heat_sink.t_hs_max) + t_min_matrix = np.minimum(t_min_matrix, DctSummmaryProcessing.heat_sink_boundary_conditions.t_hs_max) # maximum delta temperature over the heat sink - delta_t_max_heat_sink_matrix = t_min_matrix - DctSummmaryProcessing.heat_sink.t_ambient + delta_t_max_heat_sink_matrix = t_min_matrix - DctSummmaryProcessing.heat_sink_boundary_conditions.t_ambient r_th_heat_sink_target_matrix = delta_t_max_heat_sink_matrix / total_loss_matrix