"""Utilities for generating valid Functional Mockup Units."""
from datetime import datetime
from pathlib import Path
from typing import Iterable, Union
from uuid import uuid4
from zipfile import ZipFile
import pandas
from jinja2 import Environment, FileSystemLoader
from lxml import etree
from autofmu import __version__
from autofmu.strategies import (
LinearRegressionResult,
LogisticRegressionResult,
linear_regression,
logistic_regression,
)
from autofmu.utils import compile_fmu, slugify
[docs]def generate_model_description(
model_name: str,
model_identifier: str,
guid: str,
inputs: Iterable[str],
outputs: Iterable[str],
) -> etree.ElementTree:
"""Generate a valid FMI 2.0 model description XML document.
Arguments:
model_name: name of the model as used in the modeling environment
model_identifier: short class name according to C syntax, for example, "A_B_C"
guid: globaly unique identifier that identifies this model
inputs: variable input names
outputs: variable output names
Returns:
Valid FMI 2.0 model description XML document
"""
root = etree.Element(
"fmiModelDescription",
attrib={
"fmiVersion": "2.0",
"modelName": model_name,
"guid": guid,
"generationTool": f"autofmu {__version__}",
"generationDateAndTime": datetime.utcnow().isoformat(),
},
)
# Model exchange
model_exchange = etree.SubElement(
root,
"ModelExchange",
{"modelIdentifier": model_identifier},
)
sourcefiles = etree.SubElement(model_exchange, "SourceFiles")
etree.SubElement(sourcefiles, "File", {"name": f"{model_identifier}.c"})
# Co simulation
co_simulation = etree.SubElement(
root,
"CoSimulation",
{"modelIdentifier": model_identifier},
)
sourcefiles = etree.SubElement(co_simulation, "SourceFiles")
etree.SubElement(sourcefiles, "File", {"name": f"{model_identifier}.c"})
# Log categories
log_categories = etree.SubElement(root, "LogCategories")
etree.SubElement(log_categories, "Category", {"name": "logAll"})
etree.SubElement(log_categories, "Category", {"name": "logError"})
etree.SubElement(log_categories, "Category", {"name": "logFmiCall"})
etree.SubElement(log_categories, "Category", {"name": "logEvent"})
# Model variables and model structure
model_variables = etree.SubElement(root, "ModelVariables")
model_structure = etree.SubElement(root, "ModelStructure")
model_structure_outputs = etree.SubElement(model_structure, "Outputs")
model_structure_initial_unknowns = etree.SubElement(
model_structure,
"InitialUnknowns",
)
for index, variable in enumerate(inputs, 1):
scalar_variable = etree.SubElement(
model_variables,
"ScalarVariable",
{"name": variable, "valueReference": str(index), "causality": "input"},
)
etree.SubElement(scalar_variable, "Real", {"start": "0.0"})
for index, variable in enumerate(outputs, len(list(inputs)) + 1):
scalar_variable = etree.SubElement(
model_variables,
"ScalarVariable",
{"name": variable, "valueReference": str(index), "causality": "output"},
)
etree.SubElement(scalar_variable, "Real")
etree.SubElement(
model_structure_outputs,
"Unknown",
{"index": str(index), "dependencies": ""},
)
etree.SubElement(
model_structure_initial_unknowns,
"Unknown",
{"index": str(index)},
)
return etree.ElementTree(root)
[docs]def generate_model_source(
guid: str,
inputs: Iterable[str],
outputs: Iterable[str],
strategy: str,
result: Union[LinearRegressionResult, LogisticRegressionResult],
) -> str:
"""Generate a valid FMI 2.0 C source code implementation.
Arguments:
guid: globaly unique identifier that identifies this model
inputs: variable input names
outputs: variable output names
result: a result from an approximation calculation
Returns:
Valid C source code that implements the FMI
"""
env = Environment(
block_start_string="/*%",
block_end_string="%*/",
variable_start_string="/**",
variable_end_string="**/",
loader=FileSystemLoader(Path(__file__).parent / "sources"),
autoescape=True,
)
template = env.get_template("fmi2Functions.c")
return template.render(
{
"guid": guid,
"inputs": inputs,
"outputs": outputs,
"strategy": strategy,
"result": result,
}
)
[docs]def generate_fmu(
dataframe: pandas.DataFrame,
model_name: str,
inputs: Iterable[str],
outputs: Iterable[str],
outfile: Path,
strategy: str,
) -> None:
"""Generate a valid FMU model.
Arguments:
dataframe: dataframe that contains the data used for the approximation
model_name: name of the model as used in the modeling environment
inputs: variable input names
outputs: variable output names
outfile: path to the file to write the FMU
strategy: strategy to use to find the approximation (e.g, "linear")
"""
model_identifier = slugify(model_name)
guid = str(uuid4())
with ZipFile(outfile, "w") as fmu:
# Write model description to the FMU zip file
model_description = generate_model_description(
model_name, model_identifier, guid, inputs, outputs
)
fmu.writestr(
"modelDescription.xml",
etree.tostring(model_description, pretty_print=True),
)
# Write header files to the FMU zip file
headers = (Path(__file__).parent / "sources" / "headers").glob("**/*.h")
for header in headers:
fmu.write(str(header), f"sources/headers/{header.name}")
# Write source files to the FMU zip file
if strategy == "linear":
result = linear_regression(dataframe, inputs, outputs) # type: ignore
elif strategy == "logistic":
result = logistic_regression(dataframe, inputs, outputs) # type: ignore
model_source = generate_model_source(
guid=guid,
inputs=inputs,
outputs=outputs,
strategy=strategy,
result=result,
)
fmu.writestr("sources/fmi2Functions.c", model_source)
# Compile the generated source files
compile_fmu(model_identifier, outfile)