Commit e8ddc283 authored by Greenwood, Scott's avatar Greenwood, Scott
Browse files

added examples to genericcsm for running autocsm and the fmu

parent 7464a25d
Loading
Loading
Loading
Loading
+187 −75
Original line number Diff line number Diff line
# -*- coding: utf-8 -*-
"""
Created on Wed Jun  5 13:10:34 2024
Created on Fri Sep 20 11:54:58 2024

@author: fig
"""

import pathlib
from languages.modelica.modelica_methods import ModelicaMethods
from languages.julia.julia_methods import JuliaMethods

import helper_functions
import pathlib
from helper_functions import Loader

class AutoCSM:
    def __init__(self, language='modelica', architecture='nested', terminal_animation=True):
        self.language = language
        self.architecture = architecture
        self.language_methods = {
            'modelica': ModelicaMethods(self),
            'julia': JuliaMethods(self)
        }
        self.terminal_animation = terminal_animation

    def set_language(self, language):
        if language in self.language_methods:
            self.language = language
        else:
            raise ValueError(f"Unsupported language: {language}")
    """
    AutoCSM class to automate the creation of simulation architectures and models.

    Attributes:
        language (str): Programming language for model generation ('modelica', 'julia').
        architecture (str): Architecture type for the system ('nested').
        terminal_animation (bool): Whether terminal animations are shown during processing.
        input_specification (str): Path to the input specification file.
        output_path (str): Path to store output files.
        project_path (str): Path to the project directory.
        model_path (str): Path to the output directory of model created during create_model().
    """

    def set_architecture(self, architecture):
        self.architecture = architecture
    # Class-level constant for language methods
    LANGUAGE_METHODS = {
        'modelica': ModelicaMethods,
        'julia': JuliaMethods
        }
 
    def set_input_specification(self, input_specification):
        self.input_specification = input_specification
    # ARCHITECTURES = ['nested'] # at level of the language
    def __init__(self, language: str = 'modelica', architecture: str = 'nested', terminal_animation: bool = True):
        self._language = None
        self._architecture = None
        self._terminal_animation = None
        self._input_specification = None
        self._output_path = None
        self._project_path = None
        
    def set_output_path(self, output_path):
        self.output_path = output_path
        # Internal variables
        self._model_path = None
        
    def set_model_path(self, model_path):
        self.model_path = model_path
        # Setting via the property to validate.
        self.language = language
        self.architecture = architecture
        self.terminal_animation = terminal_animation
        
    def set_project_path(self, project_path):
        # Remove package.mo if contained in project_path (i.e., a common issue)
        temp = pathlib.Path(project_path)
    @property
    def terminal_animation(self) -> str:
        """Returns the terminal_animation setting."""
        return self._terminal_animation

    @terminal_animation.setter
    def terminal_animation(self, value: bool) -> None:
        """Sets the terminal_animation."""
        self._terminal_animation = value
        
    @property
    def language(self) -> str:
        """Returns the current language setting."""
        return self._language

    @language.setter
    def language(self, value: str) -> None:
        """Sets the language for model generation, with validation."""
        if value not in self.LANGUAGE_METHODS:
            raise ValueError(f"Unsupported language: {value}")
        self._language = value
        self._language_method = self.LANGUAGE_METHODS[value](self)

    @property
    def architecture(self) -> str:
        """Returns the current architecture setting."""
        return self._architecture

    @architecture.setter
    def architecture(self, value: str) -> None:
        """Sets the architecture type."""
        self._architecture = value

    @property
    def input_specification(self) -> str:
        """Returns the input specification path."""
        return self._input_specification

    @input_specification.setter
    def input_specification(self, path: str) -> None:
        """Sets and validates the input specification file path."""
        if not pathlib.Path(path).is_file():
            raise FileNotFoundError(f"Input specification file not found: {path}")
        self._input_specification = path
        
    @property
    def output_path(self) -> str:
        """Returns the output directory path."""
        return self._output_path

    @output_path.setter
    def output_path(self, path: str) -> None:
        """Sets and validates the output directory path."""
        output_dir = pathlib.Path(path)
        if not output_dir.exists():
            output_dir.mkdir(parents=True, exist_ok=True)  # Create directories if they don't exist
        self._output_path = output_dir.as_posix()
        
    @property
    def model_path(self) -> str:
        """Returns the output directory path."""
        return self._model_path
    
    @model_path.setter
    def model_path(self, path: str) -> None:
        """Sets the path to the model file."""
        self._model_path = path

    @property
    def project_path(self) -> str:
        """Returns the project path."""
        return self._project_path

    @project_path.setter
    def project_path(self, path: str) -> None:
        """Sets the project path, ensuring it's not 'package.mo'."""
        temp = pathlib.Path(path)
        if temp.name == "package.mo":
            temp = temp.parent
        self.project_path = str(temp)
        self._project_path = temp.as_posix()
                                 
    ## Sub-class methods
    def create_architecture(self, *args, **kwargs):
        if self.terminal_animation:
            with helper_functions.Loader("Creating the architecture..."):
                return self.language_methods[self.language].create_architecture(*args, **kwargs)
        else:
            print("Creating the architecture...")
            return self.language_methods[self.language].create_architecture(*args, **kwargs)
    def _execute_with_animation(self, message: str, method, *args, **kwargs):
        """
        Executes a method with an optional terminal animation.
        
    def create_model(self, *args, **kwargs):
        Args:
            message (str): The message to display during animation.
            method (callable): The method to be executed.
        """
        if self.terminal_animation:
            with helper_functions.Loader("Creating the model..."):
                return self.language_methods[self.language].create_model(*args, **kwargs)
            with Loader(message):
                return method(*args, **kwargs)
        else:
            print("Creating the model...")
            return self.language_methods[self.language].create_model(*args, **kwargs)
            print(message)
            return method(*args, **kwargs)

    def create_fmu(self, *args, **kwargs):
        if self.terminal_animation:
            with helper_functions.Loader("Creating the FMU..."):
                return self.language_methods[self.language].create_fmu(*args, **kwargs)
        else:
            print("Creating the FMU...")
            return self.language_methods[self.language].create_fmu(*args, **kwargs)
    def create_architecture(self, *args: tuple, **kwargs: dict) -> None:
        """
        Creates the architecture based on the selected language and architecture.

if __name__ == "__main__":
        Args:
            *args: Additional positional arguments passed to the architecture creation method.
            **kwargs: Additional keyword arguments passed to the architecture creation method.
        """
        try:
            return self._execute_with_animation("Creating the architecture...",
                                                self._language_method.create_architecture, *args, **kwargs)
        except Exception as e:
            print(f"Failed to create the architecture: {str(e)}")
            raise

    def create_model(self, model_name='Simulator.mo',*args: tuple, **kwargs: dict) -> None:
        """
        Creates the model for simulation.

        Args:
            *args: Additional positional arguments passed to the model creation method.
            **kwargs: Additional keyword arguments passed to the model creation method.
        """
        self.model_path = pathlib.Path.as_posix(pathlib.Path(self.output_path) / model_name)
        
    # Input specification JSON file path
    json_file_path = '../examples/data/input_specification_v0.json'
        try:
            return self._execute_with_animation("Creating the model...",
                                                self._language_method.create_model, *args, **kwargs)
        except Exception as e:
            print(f"Failed to create the model: {str(e)}")
            raise

    # Output location of generated files
    output_path = '../temp'
    def create_fmu(self, *args: tuple, **kwargs: dict) -> None:
        """
        Creates the Functional Mock-up Unit (FMU).

    # Path to the project which uses an AutoCSM language method 
    project_path = '../examples/modelica/GenericCSM'
        Args:
            *args: Additional positional arguments passed to the FMU creation method.
            **kwargs: Additional keyword arguments passed to the FMU creation method.
        """
        try:
            return self._execute_with_animation("Creating the FMU...",
                                                self._language_method.create_fmu, *args, **kwargs)
        except Exception as e:
            print(f"Failed to create the FMU: {str(e)}")
            raise

    # Model dependecies: List of libraries needed for FMU generation
    dependencies = [r'../../../Modelica/modelica-buildings/Buildings/package.mo',
          		    r'../../../Modelica/TRANSFORM-Library/TRANSFORM/package.mo']
if __name__ == "__main__":

    # Instantiate AutoCSM
    csm = AutoCSM(language='modelica', architecture='nested')

    # Load teh specification
    csm.set_input_specification(json_file_path)
    # Load the JSON specification
    csm.input_specification = '../examples/data/input_specification_v0.json'

    # Set the path to output created files
    csm.set_output_path(output_path)
    csm.output_path = '../temp'

    # This line creates an project with a file structure for the specified language and architecture
    csm.create_architecture() # more notes
    # This line creates a project with a file structure for the specified language and architecture
    csm.create_architecture() # Useful if a project is being created from scratch
    
    # Set the path to the project of interest
    csm.set_project_path(project_path)
    # Set the path to the project which uses an AutoCSM language method 
    csm.project_path = '../examples/modelica/GenericCSM'

    # Create the model for export to FMU (i.e., Simulator.mo)
    csm.create_model(uniform=[False, False])
    # Note this does not specify a solver in the model file. That is added in create_FMU.

    # Model dependencies: List of libraries needed for FMU generation
    dependencies = [
        r'../../../Modelica/TRANSFORM-Library/TRANSFORM/package.mo'
        ]
    
    # Generate the FMU (i.e., Simulator.fmu)
    csm.create_fmu(dependencies)
    csm.create_fmu(dependencies, experimentSettings={'solver':'Sdirk34hw','tolerance':1e-5})
 No newline at end of file
+151 −30
Original line number Diff line number Diff line
@@ -13,41 +13,162 @@ from . import modelica_create_fmu_dymola
from . import modelica_create_architecture

class ModelicaMethods:
    """
    This class provides methods for Modelica-specific tasks such as creating architecture,
    generating models, and building FMUs.
    
    Attributes:
        parent (AutoCSM): The parent AutoCSM object that stores configuration details.
        base_path (str): The base path where Modelica templates are located.
    """
    BASE_PATH = pathlib.Path(__file__).parent
    TEMPLATE_FOLDER = (pathlib.Path(BASE_PATH) / '../../../methods/modelica/ExaDigiT_AutoCSM/Templates/TemplateSystem').resolve()
    STRUCTURE_PATH = (pathlib.Path(BASE_PATH) / '../../../methods/modelica/ExaDigiT_AutoCSM/Templates/Structure.mo').resolve()
    EXADIGIT_PACKAGE_PATH = (pathlib.Path(BASE_PATH) / '../../../methods/modelica/ExaDigiT_AutoCSM/package.mo').resolve()

    SUPPORTED_ARCHITECTURES = ['nested']
    SUPPORTED_FMU_COMPILERS = ['dymola']
    
    def __init__(self, parent):
        self.parent = parent
        self.base_path = os.path.dirname(__file__)
        
    def create_architecture(self, template_folder=None):
        if template_folder is None:
            template_folder = os.path.abspath(os.path.join(self.base_path, '../../../methods/modelica/ExaDigiT_AutoCSM/Templates/TemplateSystem'))
    def validate_architecture(self):
        """
        Validates if the current architecture is supported.
    
        Raises:
            ValueError: If the architecture is not supported.
        """
        if self.parent.architecture not in self.SUPPORTED_ARCHITECTURES:
            raise ValueError(f"Unsupported architecture: {self.parent.architecture}. "
                             f"Supported architectures are: {self.SUPPORTED_ARCHITECTURES}")
    
    def validate_fmu_compiler(self, fmu_compiler):
        """
        Validates if the current fmu compiler is supported.
    
        Raises:
            ValueError: If the fmu compiler is not supported.
        """
        if fmu_compiler not in self.SUPPORTED_FMU_COMPILERS:
            raise ValueError(f"Unsupported fmu compiler: {fmu_compiler}. "
                             f"Supported fmu compilers are: {self.SUPPORTED_FMU_COMPILERS}")
       
    def validate_and_convert_paths(self, paths: list) -> list:
        """
        Validates a list of file paths, converts them to absolute paths, and checks if they exist.
        
        Args:
            paths (list): List of file paths (relative or absolute).
        
        Returns:
            list: List of dependencies as absolute paths.
        
        Raises:
            FileNotFoundError: If any of the file paths do not exist.
        """
        absolute_paths = []
        
        for path in paths:
            abs_path = pathlib.Path(path).resolve()  # Convert to absolute path
            if not abs_path.exists():  # Check if the file exists
                raise FileNotFoundError(f"The file '{abs_path}' does not exist.")
            absolute_paths.append(abs_path.as_posix())
        
        return absolute_paths
            
    def create_architecture(self, template_folder: str = None):
        """
        Creates the Modelica architecture by copying the template system structure.
        
        Args:
            template_folder (str, optional): Path to the template folder. If not provided, uses the default.
        """
        # Validate the architecture
        self.validate_architecture()
       
        # Update input parameters
        template_folder = template_folder or self.TEMPLATE_FOLDER.as_posix()
        
        try:
            modelica_create_architecture.main(self.parent.input_specification,
                                              self.parent.output_path,
                                              template_folder,
                                              self.parent.architecture)
        except Exception as e:
            raise RuntimeError(f"Failed to create architecture: {str(e)}")
        
    def create_model(self, structure_path: str = None, parent_class: str = 'ExaDigiT_AutoCSM.BaseClasses.Tests.PartialTest', **kwargs):
        """
        Creates the Modelica model for the given architecture.
        
    def create_model(self, structure_path=None, parent_class='ExaDigiT_AutoCSM.BaseClasses.Tests.PartialTest', uniform=[True,True]):
        if structure_path is None:
            structure_path = os.path.abspath(os.path.join(self.base_path, '../../../methods/modelica/ExaDigiT_AutoCSM/Templates/Structure.mo'))
        Args:
            structure_path (str, optional): Path to the model structure file. If not provided, uses default.
            parent_class (str): The base class used in the Modelica model.
            uniform (list, optional): A list indicating uniform settings. Defaults to [True, True].
        """
        # Validate the architecture
        self.validate_architecture()
        
        self.parent.set_model_path(os.path.abspath(os.path.join(self.parent.output_path,'Simulator.mo')))
        # Update input parameters
        structure_path = structure_path or self.STRUCTURE_PATH.as_posix()
        
        try:
            if self.parent.architecture == 'nested':
                # Extract parameters from kwargs
                uniform = kwargs.pop('uniform', None)
                uniform = [True, True] if uniform is None or not uniform else uniform
                
                modelica_create_model_nested.main(self.parent.input_specification,
                                                  self.parent.project_path,
                                                  pathlib.Path(self.parent.project_path).name,
                                                  parent_class,
                                                  self.parent.model_path,
                                                  structure_path,
                                      uniform=uniform)
                                                  uniform)
            #TODO: Add new architecture methods here
        except Exception as e:
            raise RuntimeError(f"Failed to create model: {str(e)}")
            
    def create_fmu(self,  dependencies=[],  fmu_compiler='dymola', experimentSettings={}, **kwargs):
    def create_fmu(self,  dependencies: list = None,  fmu_compiler: str = 'dymola', experimentSettings: dict = None, **kwargs):
        """
        Generates an FMU (Functional Mock-up Unit) using the specified compiler.
        
        Args:
            dependencies (list, optional): List of dependencies required for FMU generation. If not provided, uses defaults.
            fmu_compiler (str): Compiler to use for FMU generation. Defaults to 'dymola'.
            experimentSettings (dict, optional): FMU experiment settings. Defaults to an empty dict.
            **kwargs: Additional keyword arguments passed to the FMU compiler method.
        """
        # Validate the fmu compiler
        self.validate_fmu_compiler(fmu_compiler)
            
        if self.parent.project_path not in dependencies:
            dependencies.append(os.path.abspath(os.path.join(self.parent.project_path, 'package.mo')))
        # Verify dependencies are valid
        dependencies = self.validate_and_convert_paths(dependencies)
        
        if 'ExaDigiT_AutoCSM/package.mo' not in dependencies:
            dependencies.append(os.path.abspath(os.path.join(self.base_path, '../../../methods/modelica/ExaDigiT_AutoCSM/package.mo')))
        # Update input parameters
        dependencies = dependencies or []
        experimentSettings = experimentSettings or {}
    
        try:
            # Adds package if not already in dependency list
            project_package = (pathlib.Path(self.parent.project_path) / 'package.mo').resolve()
            if project_package.as_posix() not in dependencies:
                dependencies.append(project_package.as_posix())
            
            # This avoids redefining the ExaDigiT method template if it already was included in the dependency in a different location (e.g., a customized version)
            if '/'.join(list(self.EXADIGIT_PACKAGE_PATH.parts[-2])) not in dependencies:
                dependencies.append(self.EXADIGIT_PACKAGE_PATH.as_posix())
    
            if fmu_compiler == 'dymola':
            modelica_create_fmu_dymola.main(dependencies, self.parent.model_path, pathlib.Path(self.parent.model_path).parent, experimentSettings, showwindow=False)
 No newline at end of file
                # Extract parameters from kwargs
                showwindow = kwargs.pop('showwindow', False)
                
                modelica_create_fmu_dymola.main(dependencies, 
                                                self.parent.model_path, 
                                                pathlib.Path(self.parent.output_path).resolve(),
                                                experimentSettings, 
                                                showwindow)
            #TODO: Add new fmu compiler methods here
        except Exception as e:
            raise RuntimeError(f"Failed to create FMU: {str(e)}")
 No newline at end of file
+64 −0
Original line number Diff line number Diff line
{
    "Name": "GenericCSM",
	"InstanceName":"simulator",
	"Structure":{"n":1},
	"ClassName":"v0",
	"SourceName":"NULL",
    "Systems": [
        {
            "Name": "Datacenter",
			"InstanceName":"datacenter",
			"Structure":{"n":1},
			"ClassName":"v0",
			"SourceName":"NULL",
            "Systems":[
                {
                    "Name": "CoolingBlock",
					"InstanceName":"computeBlock",
					"Structure":{"n":10},
					"ClassName":"v0",
					"SourceName":"NULL",
                    "Systems":[
                        {
                            "Name": "CDU",
							"InstanceName":"cdu",
							"ClassName":"v0",
							"Structure":{"n":1},
							"SourceName":"NULL",
                            "Systems":[{}]
                        },
                        {
                            "Name": "Cabinet",
							"Structure":{"n":3},
							"InstanceName":"cabinet",
							"ClassName":"v0",
							"SourceName":"v0",
                            "Systems":[{}]
                        }]
                }]
        },
        {
            "Name": "CentralEnergyPlant",
			"InstanceName":"centralEnergyPlant",
			"Structure":{"n":1},
			"ClassName":"v0",
			"SourceName":"NULL",
            "Systems":[
                {
                    "Name": "IntermediateLoop",
					"InstanceName":"intermediateLoop",
					"Structure":{"n":1},
					"ClassName":"v0",
					"SourceName":"NULL",
                    "Systems":[{}]
                },
                {
                    "Name": "CoolingTowerLoop",
					"InstanceName":"coolingTowerLoop",
					"Structure":{"n":1},
					"ClassName":"v0",
					"SourceName":"v0",
                    "Systems":[{}]
                }]
        }]
}
 No newline at end of file
+4 −2
Original line number Diff line number Diff line
@@ -2,7 +2,9 @@ within GenericCSM.Systems.Datacenter.BaseClasses;
model Summary
  extends ExaDigiT_AutoCSM.BaseClasses.Systems.PartialSummary;

  input SI.MassFlowRate m_flow_bypass = 0.0 "Bypass mass flow rate" annotation(Dialog(group="Inputs"));
  output Real V_flow_prim_GPM = m_flow_bypass/0.063 "Bypass volume flow rate in GPM"  annotation(Dialog(group="Outputs", enable=false));
  input SI.MassFlowRate m_flow_prim=0.0 "Bypass mass flow rate"     annotation(Dialog(group="Inputs"));
  output Real V_flow_prim_GPM=m_flow_prim/0.063
    "Bypass volume flow rate in GPM"
    annotation (Dialog(group="Outputs", enable=false));

end Summary;
+2 −1
Original line number Diff line number Diff line
@@ -16,7 +16,8 @@ model v0
      m_flow=from_gpm(1900.0)),
    redeclare replaceable Controls.CS_Constant controls,
    redeclare replaceable Data.NULL data,
    redeclare replaceable Sources.NULL sources);
    redeclare replaceable Sources.NULL sources,
    summary(m_flow_prim=sensor_m_flow.m_flow));

  TRANSFORM.Fluid.Volumes.MixingVolume plenum_inlet(
    redeclare package Medium = Medium,
Loading