Task 231: .FMU File Format

Task 231: .FMU File Format

1. List of Properties Intrinsic to the .FMU File Format

The .FMU (Functional Mock-up Unit) file format is a ZIP archive container defined by the Functional Mock-up Interface (FMI) standard. Its intrinsic properties refer to the structured components, metadata, and elements that define its internal file system and behavior, independent of specific model content. These are derived from the FMI specification (version 3.0.1 as the latest stable reference) and include the archive structure, required/optional directories/files, and the schema of the core modelDescription.xml file. Below is a comprehensive list:

  • Container Format: ZIP archive with .fmu extension, ensuring portability and compression of all internal files.
  • Required Files:
  • modelDescription.xml: UTF-8 encoded XML file describing the model, adhering to fmi3ModelDescription.xsd schema.
  • Shared library (for binary FMUs): Platform-specific binary (e.g., .dll on Windows, .so on Linux) in binaries/ directory, named after the modelIdentifier attribute.
  • Optional Files/Directories:
  • binaries/ (e.g., binaries/win64, binaries/linux64): Contains platform-specific executables for FMI functions.
  • sources/: Contains C/C++ source code files for source-code FMUs, along with buildDescription.xml (adhering to fmi3BuildDescription.xsd) for compilation instructions.
  • resources/: Additional data files (e.g., tables, images) referenced by the FMU.
  • documentation/: HTML or other files for FMU usage documentation.
  • licenses/: Additional license text if needed beyond the XML attribute.
  • extra/: Vendor-specific extensions or annotations.
  • terminalsAndIcons/terminalsAndIcons.xml: XML for graphical terminals and icons, adhering to fmi3TerminalsAndIcons.xsd.
  • modelDescription.xml Root Attributes (under ):
  • fmiVersion: The FMI standard version (e.g., "3.0").
  • modelName: Unique name of the model.
  • instantiationToken: Unique identifier for instantiation.
  • description: Textual description of the model.
  • author: Creator of the FMU.
  • version: Model version.
  • copyright: Copyright notice.
  • license: License information.
  • generationTool: Tool used to generate the FMU.
  • generationDateAndTime: ISO 8601 timestamp of generation.
  • variableNamingConvention: "flat" or "structured" for variable names.
  • modelDescription.xml Child Elements:
  • : List of units with SI base units (kg, m, s, A, K, mol, cd, rad), including factor and offset for conversions.
  • : Definitions of variable types (e.g., fmi3Float64, fmi3Int32, fmi3Binary).
  • : Categories for debug logging.
  • : Simulation defaults like startTime, stopTime, tolerance, stepSize.
  • : List of variables, each with:
  • name: Unique identifier.
  • valueReference: Integer handle for C-API access.
  • causality: input, output, parameter, calculatedParameter, local, independent.
  • variability: constant, fixed, tunable, discrete, continuous.
  • initial: exact, approx, calculated.
  • type-specific attributes (e.g., start value, min/max, unit, quantity).
  • clocks: Association with clocked partitions.
  • dimension: For arrays (row-major serialization).
  • : Dependencies and orders, including:
  • : List of output variables.
  • : Derivatives of continuous states.
  • : Clock-associated states.
  • : Variables with unknown initial values.
  • : For event detection.
  • Dependencies: Variable influence graphs.
  • Interface Types (at least one required): , , or , each with:
  • modelIdentifier: Base name for binaries/sources.
  • Capability flags (booleans): e.g., needsExecutionTool, canBeInstantiatedOnlyOncePerProcess, canHandleVariableCommunicationStepSize, canGetAndSetFMUState, canSerializeFMUState, providesDirectionalDerivatives.
  • : Namespace-specific extensions (e.g., org.modelica).
  • Other Intrinsic Properties:
  • Platform Independence: Supports multiple OS/architectures via separate binaries.
  • Semantic Versioning: Major.minor.patch for compatibility.
  • Clock Support: For scheduled execution, with attributes like intervalVariability (constant, fixed, tunable, triggered) and supportsFraction.
  • Variable Types: Mapped to C types (e.g., fmi3Float32, fmi3UInt64, fmi3String, fmi3Binary).
  • Array Handling: Multi-dimensional variables serialized in row-major order.
  • Uniqueness Constraints: All names and valueReferences must be unique.

These properties ensure the FMU is tool-agnostic, simulator-independent, and executable across environments.

3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .FMU Property Dump

Below is a self-contained HTML snippet with embedded JavaScript that can be embedded in a Ghost blog post (or any HTML-enabled blog). It allows users to drag and drop a .FMU file, unzips it in-browser using JSZip (included via CDN for simplicity), parses modelDescription.xml using DOMParser, extracts and dumps all properties from the list in #1 to the screen in a readable format. No external dependencies beyond the CDN.

Drag and drop a .FMU file here

4. Python Class for .FMU Handling

import zipfile
import xml.etree.ElementTree as ET
import os
import shutil

class FMUHandler:
    def __init__(self, fmu_path):
        self.fmu_path = fmu_path
        self.temp_dir = 'fmu_temp'
        self.xml_path = os.path.join(self.temp_dir, 'modelDescription.xml')
        self.root = None
        self._decode()

    def _decode(self):
        """Unzip and parse XML."""
        if os.path.exists(self.temp_dir):
            shutil.rmtree(self.temp_dir)
        with zipfile.ZipFile(self.fmu_path, 'r') as zip_ref:
            zip_ref.extractall(self.temp_dir)
        if not os.path.exists(self.xml_path):
            raise ValueError('modelDescription.xml not found')
        tree = ET.parse(self.xml_path)
        self.root = tree.getroot()

    def read_properties(self):
        """Read and return properties as dict."""
        props = {
            'root_attributes': {attr: self.root.attrib.get(attr, 'N/A') for attr in [
                'fmiVersion', 'modelName', 'instantiationToken', 'description', 'author',
                'version', 'copyright', 'license', 'generationTool', 'generationDateAndTime',
                'variableNamingConvention'
            ]},
            'unit_definitions_count': len(self.root.find('UnitDefinitions') or []),
            'type_definitions_count': len(self.root.find('TypeDefinitions') or []),
            'log_categories_count': len(self.root.find('LogCategories') or []),
            'default_experiment': {
                attr: (self.root.find('DefaultExperiment') or {}).attrib.get(attr, 'N/A')
                for attr in ['startTime', 'stopTime', 'tolerance', 'stepSize']
            },
            'model_variables': [
                {attr: var.attrib.get(attr, 'N/A') for attr in [
                    'name', 'valueReference', 'causality', 'variability', 'initial'
                ]} for var in (self.root.find('ModelVariables') or [])
            ],
            'model_structure': {
                'outputs_count': len(self.root.find('ModelStructure/Outputs') or []),
                'continuous_state_derivatives_count': len(self.root.find('ModelStructure/ContinuousStateDerivatives') or []),
                'initial_unknowns_count': len(self.root.find('ModelStructure/InitialUnknowns') or []),
            },
            'interface_types': {}
        }
        for itype in ['ModelExchange', 'CoSimulation', 'ScheduledExecution']:
            elem = self.root.find(itype)
            if elem is not None:
                props['interface_types'][itype] = {
                    attr: elem.attrib.get(attr, 'false') for attr in [
                        'modelIdentifier', 'needsExecutionTool', 'canHandleVariableCommunicationStepSize',
                        'canGetAndSetFMUState', 'providesDirectionalDerivatives'
                    ]
                }
        # Other files
        props['other_files'] = [f for f in os.listdir(self.temp_dir) if f != 'modelDescription.xml']
        return props

    def print_properties(self):
        """Print properties to console."""
        props = self.read_properties()
        print('FMU Properties:')
        for key, value in props.items():
            print(f'{key}: {value}')

    def write_properties(self, new_props):
        """Modify properties and re-zip FMU."""
        # Update root attributes example
        for attr, val in new_props.get('root_attributes', {}).items():
            self.root.set(attr, val)
        # More updates can be added for other elements
        ET.ElementTree(self.root).write(self.xml_path, encoding='utf-8', xml_declaration=True)
        new_fmu_path = self.fmu_path.replace('.fmu', '_modified.fmu')
        with zipfile.ZipFile(new_fmu_path, 'w') as zip_ref:
            for folder, _, files in os.walk(self.temp_dir):
                for file in files:
                    zip_ref.write(os.path.join(folder, file), os.path.relpath(os.path.join(folder, file), self.temp_dir))
        print(f'Modified FMU saved to {new_fmu_path}')

    def __del__(self):
        """Cleanup temp dir."""
        if os.path.exists(self.temp_dir):
            shutil.rmtree(self.temp_dir)

# Usage example:
# handler = FMUHandler('example.fmu')
# handler.print_properties()
# handler.write_properties({'root_attributes': {'description': 'Updated description'}})

5. Java Class for .FMU Handling

import java.io.*;
import java.util.zip.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.*;
import java.nio.file.*;
import java.util.*;

public class FMUHandler {
    private String fmuPath;
    private String tempDir = "fmu_temp";
    private String xmlPath = tempDir + "/modelDescription.xml";
    private Document doc;

    public FMUHandler(String fmuPath) throws Exception {
        this.fmuPath = fmuPath;
        decode();
    }

    private void decode() throws Exception {
        Files.createDirectories(Paths.get(tempDir));
        try (ZipFile zip = new ZipFile(fmuPath)) {
            Enumeration<? extends ZipEntry> entries = zip.entries();
            while (entries.hasMoreElements()) {
                ZipEntry entry = entries.nextElement();
                File file = new File(tempDir + "/" + entry.getName());
                if (entry.isDirectory()) {
                    file.mkdirs();
                } else {
                    try (InputStream is = zip.getInputStream(entry)) {
                        Files.copy(is, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
                    }
                }
            }
        }
        if (!new File(xmlPath).exists()) {
            throw new FileNotFoundException("modelDescription.xml not found");
        }
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        doc = builder.parse(xmlPath);
    }

    public Map<String, Object> readProperties() {
        Map<String, Object> props = new HashMap<>();
        Element root = doc.getDocumentElement();

        Map<String, String> rootAttrs = new HashMap<>();
        String[] attrKeys = {"fmiVersion", "modelName", "instantiationToken", "description", "author",
                              "version", "copyright", "license", "generationTool", "generationDateAndTime",
                              "variableNamingConvention"};
        for (String attr : attrKeys) {
            rootAttrs.put(attr, root.getAttribute(attr).isEmpty() ? "N/A" : root.getAttribute(attr));
        }
        props.put("root_attributes", rootAttrs);

        props.put("unit_definitions_count", getChildCount(root, "UnitDefinitions"));
        props.put("type_definitions_count", getChildCount(root, "TypeDefinitions"));
        props.put("log_categories_count", getChildCount(root, "LogCategories"));

        Element defaultExp = (Element) root.getElementsByTagName("DefaultExperiment").item(0);
        Map<String, String> defExpMap = new HashMap<>();
        String[] defAttrs = {"startTime", "stopTime", "tolerance", "stepSize"};
        for (String attr : defAttrs) {
            defExpMap.put(attr, defaultExp != null && !defaultExp.getAttribute(attr).isEmpty() ? defaultExp.getAttribute(attr) : "N/A");
        }
        props.put("default_experiment", defExpMap);

        List<Map<String, String>> variables = new ArrayList<>();
        NodeList varList = root.getElementsByTagName("ModelVariables").item(0).getChildNodes();
        for (int i = 0; i < varList.getLength(); i++) {
            if (varList.item(i) instanceof Element) {
                Element var = (Element) varList.item(i);
                Map<String, String> varMap = new HashMap<>();
                String[] varAttrs = {"name", "valueReference", "causality", "variability", "initial"};
                for (String attr : varAttrs) {
                    varMap.put(attr, var.getAttribute(attr).isEmpty() ? "N/A" : var.getAttribute(attr));
                }
                variables.add(varMap);
            }
        }
        props.put("model_variables", variables);

        Map<String, Integer> structure = new HashMap<>();
        structure.put("outputs_count", getChildCount(root.getElementsByTagName("ModelStructure").item(0), "Outputs"));
        structure.put("continuous_state_derivatives_count", getChildCount(root.getElementsByTagName("ModelStructure").item(0), "ContinuousStateDerivatives"));
        structure.put("initial_unknowns_count", getChildCount(root.getElementsByTagName("ModelStructure").item(0), "InitialUnknowns"));
        props.put("model_structure", structure);

        Map<String, Map<String, String>> interfaces = new HashMap<>();
        String[] types = {"ModelExchange", "CoSimulation", "ScheduledExecution"};
        for (String type : types) {
            Element el = (Element) root.getElementsByTagName(type).item(0);
            if (el != null) {
                Map<String, String> ifMap = new HashMap<>();
                String[] ifAttrs = {"modelIdentifier", "needsExecutionTool", "canHandleVariableCommunicationStepSize",
                                    "canGetAndSetFMUState", "providesDirectionalDerivatives"};
                for (String attr : ifAttrs) {
                    ifMap.put(attr, el.getAttribute(attr).isEmpty() ? "false" : el.getAttribute(attr));
                }
                interfaces.put(type, ifMap);
            }
        }
        props.put("interface_types", interfaces);

        List<String> otherFiles = new ArrayList<>();
        Files.walk(Paths.get(tempDir)).filter(Files::isRegularFile)
             .map(Path::toString).filter(p -> !p.endsWith("modelDescription.xml"))
             .forEach(otherFiles::add);
        props.put("other_files", otherFiles);

        return props;
    }

    private int getChildCount(Node parent, String tag) {
        if (parent == null) return 0;
        return parent.getElementsByTagName(tag).item(0).getChildNodes().getLength();
    }

    public void printProperties() {
        Map<String, Object> props = readProperties();
        System.out.println("FMU Properties:");
        props.forEach((key, value) -> System.out.println(key + ": " + value));
    }

    public void writeProperties(Map<String, Object> newProps) throws Exception {
        // Example: Update root attributes
        @SuppressWarnings("unchecked")
        Map<String, String> newRootAttrs = (Map<String, String>) newProps.get("root_attributes");
        if (newRootAttrs != null) {
            newRootAttrs.forEach((attr, val) -> doc.getDocumentElement().setAttribute(attr, val));
        }
        // More updates can be implemented

        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        transformer.transform(new DOMSource(doc), new StreamResult(new File(xmlPath)));

        String newFmuPath = fmuPath.replace(".fmu", "_modified.fmu");
        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(newFmuPath))) {
            Files.walk(Paths.get(tempDir)).filter(Files::isRegularFile).forEach(path -> {
                try {
                    ZipEntry ze = new ZipEntry(path.toString().substring(tempDir.length() + 1));
                    zos.putNextEntry(ze);
                    Files.copy(path, zos);
                    zos.closeEntry();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        }
        System.out.println("Modified FMU saved to " + newFmuPath);
    }

    public void cleanup() throws IOException {
        Files.walk(Paths.get(tempDir)).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
    }

    // Usage example:
    // public static void main(String[] args) throws Exception {
    //     FMUHandler handler = new FMUHandler("example.fmu");
    //     handler.printProperties();
    //     Map<String, Object> updates = new HashMap<>();
    //     Map<String, String> rootUpdates = new HashMap<>();
    //     rootUpdates.put("description", "Updated description");
    //     updates.put("root_attributes", rootUpdates);
    //     handler.writeProperties(updates);
    //     handler.cleanup();
    // }
}

6. JavaScript Class for .FMU Handling

const fs = require('fs'); // For Node.js environment
const JSZip = require('jszip'); // Requires 'jszip' npm package
const { DOMParser, XMLSerializer } = require('xmldom'); // Requires 'xmldom' npm package

class FMUHandler {
  constructor(fmuPath) {
    this.fmuPath = fmuPath;
    this.tempDir = 'fmu_temp'; // Note: Node.js file system ops; not for browser
    this.xmlPath = `${this.tempDir}/modelDescription.xml`;
    this.doc = null;
    this.decode();
  }

  decode() {
    // Unzip (sync for simplicity; use async in production)
    const data = fs.readFileSync(this.fmuPath);
    JSZip.loadAsync(data).then(zip => {
      zip.forEach((relPath, file) => {
        if (!file.dir) {
          const content = file.async('nodebuffer');
          const dest = `${this.tempDir}/${relPath}`;
          fs.mkdirSync(path.dirname(dest), { recursive: true });
          fs.writeFileSync(dest, content);
        }
      });
      const xmlData = fs.readFileSync(this.xmlPath, 'utf8');
      const parser = new DOMParser();
      this.doc = parser.parseFromString(xmlData, 'application/xml');
    }).catch(err => console.error('Decode error:', err));
  }

  readProperties() {
    const props = {};
    const root = this.doc.getElementsByTagName('fmiModelDescription')[0];

    const rootAttrs = {};
    ['fmiVersion', 'modelName', 'instantiationToken', 'description', 'author',
     'version', 'copyright', 'license', 'generationTool', 'generationDateAndTime',
     'variableNamingConvention'].forEach(attr => {
      rootAttrs[attr] = root.getAttribute(attr) || 'N/A';
    });
    props.root_attributes = rootAttrs;

    props.unit_definitions_count = (this.doc.getElementsByTagName('UnitDefinitions')[0] || {}).childNodes.length || 0;
    props.type_definitions_count = (this.doc.getElementsByTagName('TypeDefinitions')[0] || {}).childNodes.length || 0;
    props.log_categories_count = (this.doc.getElementsByTagName('LogCategories')[0] || {}).childNodes.length || 0;

    const defaultExp = this.doc.getElementsByTagName('DefaultExperiment')[0];
    const defExpMap = {};
    ['startTime', 'stopTime', 'tolerance', 'stepSize'].forEach(attr => {
      defExpMap[attr] = defaultExp ? (defaultExp.getAttribute(attr) || 'N/A') : 'N/A';
    });
    props.default_experiment = defExpMap;

    const variables = [];
    const varNodes = this.doc.getElementsByTagName('ModelVariables')[0].childNodes;
    for (let i = 0; i < varNodes.length; i++) {
      if (varNodes[i].nodeType === 1) {
        const varMap = {};
        ['name', 'valueReference', 'causality', 'variability', 'initial'].forEach(attr => {
          varMap[attr] = varNodes[i].getAttribute(attr) || 'N/A';
        });
        variables.push(varMap);
      }
    }
    props.model_variables = variables;

    const structure = {};
    const modelStruct = this.doc.getElementsByTagName('ModelStructure')[0];
    structure.outputs_count = (modelStruct.getElementsByTagName('Outputs')[0] || {}).childNodes.length || 0;
    structure.continuous_state_derivatives_count = (modelStruct.getElementsByTagName('ContinuousStateDerivatives')[0] || {}).childNodes.length || 0;
    structure.initial_unknowns_count = (modelStruct.getElementsByTagName('InitialUnknowns')[0] || {}).childNodes.length || 0;
    props.model_structure = structure;

    const interfaces = {};
    ['ModelExchange', 'CoSimulation', 'ScheduledExecution'].forEach(type => {
      const el = this.doc.getElementsByTagName(type)[0];
      if (el) {
        const ifMap = {};
        ['modelIdentifier', 'needsExecutionTool', 'canHandleVariableCommunicationStepSize',
         'canGetAndSetFMUState', 'providesDirectionalDerivatives'].forEach(attr => {
          ifMap[attr] = el.getAttribute(attr) || 'false';
        });
        interfaces[type] = ifMap;
      }
    });
    props.interface_types = interfaces;

    props.other_files = fs.readdirSync(this.tempDir).filter(f => f !== 'modelDescription.xml');
    return props;
  }

  printProperties() {
    const props = this.readProperties();
    console.log('FMU Properties:');
    console.dir(props, { depth: null });
  }

  writeProperties(newProps) {
    // Example: Update root attributes
    const root = this.doc.getElementsByTagName('fmiModelDescription')[0];
    Object.entries(newProps.root_attributes || {}).forEach(([attr, val]) => {
      root.setAttribute(attr, val);
    });
    // More updates can be added

    const serializer = new XMLSerializer();
    fs.writeFileSync(this.xmlPath, serializer.serializeToString(this.doc));

    const newFmuPath = this.fmuPath.replace('.fmu', '_modified.fmu');
    const zip = new JSZip();
    fs.readdirSync(this.tempDir, { recursive: true }).forEach(file => {
      if (!fs.statSync(file).isDirectory()) {
        zip.file(file.replace(this.tempDir + '/', ''), fs.readFileSync(file));
      }
    });
    zip.generateNodeStream({ type: 'nodebuffer', streamFiles: true })
      .pipe(fs.createWriteStream(newFmuPath))
      .on('finish', () => console.log(`Modified FMU saved to ${newFmuPath}`));
  }

  cleanup() {
    fs.rmSync(this.tempDir, { recursive: true, force: true });
  }
}

// Usage example:
// const handler = new FMUHandler('example.fmu');
// handler.printProperties();
// handler.writeProperties({ root_attributes: { description: 'Updated description' } });
// handler.cleanup();

7. C Class for .FMU Handling

(Note: Since C does not have native classes, this is implemented as a C++ class for object-oriented features. It uses minizip for ZIP handling and tinyxml2 for XML parsing; assume these libraries are linked. Include headers: <minizip/unzip.h>, <minizip/zip.h>, <tinyxml2.h>.)

#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <filesystem> // C++17
#include <minizip/unzip.h>
#include <minizip/zip.h>
#include <tinyxml2.h>

namespace fs = std::filesystem;

class FMUHandler {
private:
    std::string fmuPath;
    std::string tempDir = "fmu_temp";
    std::string xmlPath = tempDir + "/modelDescription.xml";
    tinyxml2::XMLDocument doc;

    void decode() {
        fs::create_directory(tempDir);
        unzFile uf = unzOpen(fmuPath.c_str());
        if (!uf) throw std::runtime_error("Cannot open FMU");
        do {
            char filename[256];
            if (unzGetCurrentFileInfo(uf, nullptr, filename, sizeof(filename), nullptr, 0, nullptr, 0) != UNZ_OK) break;
            std::string dest = tempDir + "/" + filename;
            if (filename[strlen(filename)-1] == '/') {
                fs::create_directories(dest);
                continue;
            }
            unzOpenCurrentFile(uf);
            std::ofstream out(dest, std::ios::binary);
            char buf[4096];
            int read;
            while ((read = unzReadCurrentFile(uf, buf, sizeof(buf))) > 0) out.write(buf, read);
            out.close();
            unzCloseCurrentFile(uf);
        } while (unzGoToNextFile(uf) == UNZ_OK);
        unzClose(uf);

        if (doc.LoadFile(xmlPath.c_str()) != tinyxml2::XML_SUCCESS) {
            throw std::runtime_error("Cannot parse modelDescription.xml");
        }
    }

public:
    FMUHandler(const std::string& fmuPath) : fmuPath(fmuPath) {
        decode();
    }

    std::map<std::string, std::any> readProperties() {
        std::map<std::string, std::any> props;
        auto root = doc.FirstChildElement("fmiModelDescription");

        std::map<std::string, std::string> rootAttrs;
        const char* attrs[] = {"fmiVersion", "modelName", "instantiationToken", "description", "author",
                               "version", "copyright", "license", "generationTool", "generationDateAndTime",
                               "variableNamingConvention"};
        for (auto attr : attrs) {
            rootAttrs[attr] = root->Attribute(attr) ? root->Attribute(attr) : "N/A";
        }
        props["root_attributes"] = rootAttrs;

        props["unit_definitions_count"] = countChildren(root->FirstChildElement("UnitDefinitions"));
        props["type_definitions_count"] = countChildren(root->FirstChildElement("TypeDefinitions"));
        props["log_categories_count"] = countChildren(root->FirstChildElement("LogCategories"));

        auto defaultExp = root->FirstChildElement("DefaultExperiment");
        std::map<std::string, std::string> defExpMap;
        const char* defAttrs[] = {"startTime", "stopTime", "tolerance", "stepSize"};
        for (auto attr : defAttrs) {
            defExpMap[attr] = defaultExp && defaultExp->Attribute(attr) ? defaultExp->Attribute(attr) : "N/A";
        }
        props["default_experiment"] = defExpMap;

        std::vector<std::map<std::string, std::string>> variables;
        auto varElem = root->FirstChildElement("ModelVariables")->FirstChildElement();
        while (varElem) {
            std::map<std::string, std::string> varMap;
            const char* varAttrs[] = {"name", "valueReference", "causality", "variability", "initial"};
            for (auto attr : varAttrs) {
                varMap[attr] = varElem->Attribute(attr) ? varElem->Attribute(attr) : "N/A";
            }
            variables.push_back(varMap);
            varElem = varElem->NextSiblingElement();
        }
        props["model_variables"] = variables;

        std::map<std::string, int> structure;
        auto modelStruct = root->FirstChildElement("ModelStructure");
        structure["outputs_count"] = countChildren(modelStruct->FirstChildElement("Outputs"));
        structure["continuous_state_derivatives_count"] = countChildren(modelStruct->FirstChildElement("ContinuousStateDerivatives"));
        structure["initial_unknowns_count"] = countChildren(modelStruct->FirstChildElement("InitialUnknowns"));
        props["model_structure"] = structure;

        std::map<std::string, std::map<std::string, std::string>> interfaces;
        const char* types[] = {"ModelExchange", "CoSimulation", "ScheduledExecution"};
        for (auto type : types) {
            auto el = root->FirstChildElement(type);
            if (el) {
                std::map<std::string, std::string> ifMap;
                const char* ifAttrs[] = {"modelIdentifier", "needsExecutionTool", "canHandleVariableCommunicationStepSize",
                                         "canGetAndSetFMUState", "providesDirectionalDerivatives"};
                for (auto attr : ifAttrs) {
                    ifMap[attr] = el->Attribute(attr) ? el->Attribute(attr) : "false";
                }
                interfaces[type] = ifMap;
            }
        }
        props["interface_types"] = interfaces;

        std::vector<std::string> otherFiles;
        for (const auto& entry : fs::recursive_directory_iterator(tempDir)) {
            if (entry.is_regular_file() && entry.path().filename() != "modelDescription.xml") {
                otherFiles.push_back(entry.path().string());
            }
        }
        props["other_files"] = otherFiles;

        return props;
    }

    int countChildren(tinyxml2::XMLElement* elem) {
        if (!elem) return 0;
        int count = 0;
        for (auto child = elem->FirstChildElement(); child; child = child->NextSiblingElement()) ++count;
        return count;
    }

    void printProperties() {
        auto props = readProperties();
        std::cout << "FMU Properties:" << std::endl;
        // Printing logic (simplified; use recursion for maps/vectors)
        for (const auto& [key, value] : props) {
            std::cout << key << ": "; // Extend with type checks for any
            // Example for string map: if auto m = std::any_cast<std::map<std::string, std::string>>(value);
            std::cout << std::endl;
        }
    }

    void writeProperties(const std::map<std::string, std::any>& newProps) {
        auto root = doc.FirstChildElement("fmiModelDescription");
        auto newRootAttrs = std::any_cast<std::map<std::string, std::string>>(newProps.at("root_attributes"));
        for (const auto& [attr, val] : newRootAttrs) {
            root->SetAttribute(attr.c_str(), val.c_str());
        }
        // More updates can be added

        doc.SaveFile(xmlPath.c_str());

        std::string newFmuPath = fmuPath.substr(0, fmuPath.find_last_of('.')) + "_modified.fmu";
        zipFile zf = zipOpen(newFmuPath.c_str(), APPEND_STATUS_CREATE);
        for (const auto& entry : fs::recursive_directory_iterator(tempDir)) {
            if (entry.is_regular_file()) {
                std::string relPath = entry.path().string().substr(tempDir.length() + 1);
                zip_fileinfo zi{};
                zipOpenNewFileInZip(zf, relPath.c_str(), &zi, nullptr, 0, nullptr, 0, nullptr, Z_DEFLATED, Z_DEFAULT_COMPRESSION);
                std::ifstream in(entry.path().string(), std::ios::binary);
                char buf[4096];
                while (in.read(buf, sizeof(buf))) zipWriteInFileInZip(zf, buf, in.gcount());
                zipCloseFileInZip(zf);
            }
        }
        zipClose(zf, nullptr);
        std::cout << "Modified FMU saved to " << newFmuPath << std::endl;
    }

    ~FMUHandler() {
        fs::remove_all(tempDir);
    }
};

// Usage example:
// int main() {
//     try {
//         FMUHandler handler("example.fmu");
//         handler.printProperties();
//         std::map<std::string, std::any> updates;
//         std::map<std::string, std::string> rootUpdates = {{"description", "Updated description"}};
//         updates["root_attributes"] = rootUpdates;
//         handler.writeProperties(updates);
//     } catch (const std::exception& e) {
//         std::cerr << e.what() << std::endl;
//     }
//     return 0;
// }