Summary
MLFMU generates FMUs with a C++ path parsing bug that causes FMU instantiation to fail on Unix systems when using absolute paths. The bug is in the formatOnnxPath function which incorrectly strips URI prefixes.
Root Cause
In mlfmu/fmu_build/templates/onnx_fmu/onnxFmu.cpp, the formatOnnxPath function has incorrect substring logic around lines 44-46:
// BUGGY CODE
std::wstring startPath = path.substr(0, 8); // "file:///"
std::wstring endPath = path.substr(8); // strips 8 chars - TOO MANY!
if (startPath == L"file:///") {
path = endPath; // Results in "tmp/path" instead of "/tmp/path"
}
Problem Description
When fmpy (and other FMU libraries) pass absolute paths to the FMU, they convert them to file:// URIs:
- Input:
file:///tmp/extracted_fmu/resources/model.onnx
- Expected output:
/tmp/extracted_fmu/resources/model.onnx
- Actual output:
tmp/extracted_fmu/resources/model.onnx ❌ (missing leading /)
This causes ONNX model loading to fail with:
[ERROR] Load model from tmp/extracted_dir/resources/model.onnx failed: File doesn't exist
Reproduction Case
#!/usr/bin/env python3
"""
This script demonstrates the C++ path parsing bug in MLFMU-generated FMUs
where 'file:///' URI prefixes are incorrectly stripped, causing missing
leading slashes in absolute paths on Unix systems.
"""
import tempfile
from pathlib import Path
import json
import onnx
import onnx.helper as helper
from mlfmu.api import MlFmuBuilder
def create_simple_onnx_model(onnx_path: Path) -> None:
"""Create a minimal valid ONNX model for testing."""
# Create simple model: 2 inputs -> 1 output (just sum the inputs)
input_tensor = helper.make_tensor_value_info('input', onnx.TensorProto.FLOAT, [1, 2])
output_tensor = helper.make_tensor_value_info('output', onnx.TensorProto.FLOAT, [1, 1])
# Sum operation: output = input[0] + input[1]
node = helper.make_node(
'ReduceSum',
inputs=['input'],
outputs=['output'],
axes=[1] # Sum along the feature dimension
)
# Create graph and model with opset 11 for compatibility
graph = helper.make_graph([node], "simple_graph", [input_tensor], [output_tensor])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 11)])
# Save ONNX model
onnx.save_model(model, str(onnx_path))
def create_simple_mlfmu_interface(interface_path: Path) -> None:
"""Create a minimal MLFMU interface configuration."""
interface = {
"name": "SimpleBugDemo",
"inputs": [
{"name": "A", "agentInputIndexes": ["0"]},
{"name": "B", "agentInputIndexes": ["1"]}
],
"outputs": [
{"name": "C", "agentOutputIndexes": ["0"]}
]
}
with open(interface_path, 'w') as f:
json.dump(interface, f, indent=2)
def test_fmu_instantiation(fmu_path: Path) -> None:
"""Test FMU instantiation using fmpy (this triggers the path bug)."""
import fmpy
from fmpy import read_model_description
from fmpy.fmi2 import FMU2Slave
print("🔍 Testing FMU instantiation (where path bug occurs)...")
# Load model description
model_description = read_model_description(str(fmu_path))
print(f"✅ Model description loaded: {model_description.modelName}")
# Extract FMU to temporary directory
with tempfile.TemporaryDirectory() as temp_extract_dir:
extracted_dir = fmpy.extract(str(fmu_path), temp_extract_dir)
print(f"✅ FMU extracted to: {extracted_dir}")
print("🚨 Creating FMU2Slave with absolute path (triggers C++ path bug)...")
# Initialize FMU slave - this will work
fmu_slave = FMU2Slave(
guid=model_description.guid,
unzipDirectory=extracted_dir, # This is an absolute path like /tmp/xxx
modelIdentifier=model_description.coSimulation.modelIdentifier,
instanceName="test_bug_demo",
)
print("✅ FMU2Slave created")
print("🎯 Calling fmu_slave.instantiate() - this will trigger the C++ path bug...")
try:
# This is where the C++ path parsing bug occurs
# The FMU's C++ code will try to load the ONNX model using a path like:
# "file:///tmp/extracted_dir/resources/simple_model.onnx"
# But the buggy formatOnnxPath() strips 8 characters, leaving:
# "tmp/extracted_dir/resources/simple_model.onnx" (missing leading slash)
fmu_slave.instantiate()
print("🎉 FMU instantiation succeeded! (Bug may be fixed)")
# Clean up if successful
try:
fmu_slave.terminate()
fmu_slave.freeInstance()
except:
pass
except Exception as instantiation_error:
print(f"❌ FMU instantiation failed: {instantiation_error}")
# Check if this is the expected path-related error
error_str = str(instantiation_error).lower()
if "file doesn't exist" in error_str or "load model" in error_str:
print("✅ This is the expected MLFMU C++ path parsing bug!")
print(" The ONNX model path is missing its leading slash due to")
print(" incorrect URI parsing in mlfmu/fmu_build/templates/onnx_fmu/onnxFmu.cpp")
else:
print("Different error occurred:", instantiation_error)
def create_minimal_reproduction():
"""Main function to reproduce the MLFMU path bug."""
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
print(f"Working directory: {temp_path}")
print(f"Absolute path: {temp_path.resolve()}")
print()
# Step 1: Create ONNX model
print("📝 Creating ONNX model...")
onnx_path = temp_path / "simple_model.onnx"
create_simple_onnx_model(onnx_path)
print(f"✅ ONNX model created: {onnx_path}")
print()
# Step 2: Create MLFMU interface
print("📋 Creating MLFMU interface...")
interface_path = temp_path / "interface.json"
create_simple_mlfmu_interface(interface_path)
print(f"✅ Interface created: {interface_path}")
print()
# Step 3: Build FMU with MLFMU
print("🔨 Building FMU with MLFMU...")
builder = MlFmuBuilder(
onnx_path=str(onnx_path),
interface_path=str(interface_path),
fmu_output_folder=str(temp_path.resolve()), # Use absolute paths
root_directory=str(temp_path.resolve())
)
try:
builder.generate()
print("✅ FMU generation completed")
builder.compile()
print("✅ FMU compilation completed")
builder.build()
print("✅ FMU build completed")
except Exception as e:
print(f"❌ FMU build failed: {e}")
return
# Step 4: Find the generated FMU
# MLFMU may create the FMU in different locations depending on the configuration
fmu_files = []
# Check temp directory first
fmu_files = list(temp_path.rglob("*.fmu"))
# If not found, check current working directory (common for MLFMU)
if not fmu_files:
fmu_files = list(Path.cwd().glob("*.fmu"))
if fmu_files:
print(f"📂 Found FMU in current directory: {[f.name for f in fmu_files]}")
if not fmu_files:
print("❌ No FMU file found after build")
return
fmu_file = fmu_files[0]
print(f"✅ FMU created: {fmu_file}")
print()
# Step 5: Test FMU instantiation (this will trigger the path bug)
test_fmu_instantiation(fmu_file)
if __name__ == "__main__":
print("🧪 MLFMU Path Bug Reproduction")
print("=" * 50)
create_minimal_reproduction()
Expected vs Actual Behavior
| Input URI |
Expected Path |
Actual Path |
Status |
file:///tmp/path/model.onnx |
/tmp/path/model.onnx |
tmp/path/model.onnx |
❌ Missing leading / |
file:///home/user/model.onnx |
/home/user/model.onnx |
home/user/model.onnx |
❌ Missing leading / |
Impact
- Severity: High - FMUs cannot be instantiated on Unix systems with absolute paths
- Scope: Affects all FMUs generated by MLFMU when used with standard FMU libraries (fmpy, PyFMI, etc.)
- Workaround: None available - the bug is in compiled C++ code within the FMU binary
Proposed Fix
Option 1: Simple Fix
// In mlfmu/fmu_build/templates/onnx_fmu/onnxFmu.cpp
// Replace lines ~44-46:
// CURRENT (BUGGY)
std::wstring startPath = path.substr(0, 8); // "file:///"
std::wstring endPath = path.substr(8); // strips 8 chars
if (startPath == L"file:///") {
path = endPath;
}
// FIXED
std::wstring startPath = path.substr(0, 7); // "file://"
std::wstring endPath = path.substr(7); // strips 7 chars
if (startPath == L"file://") {
path = endPath;
}
Option 2: Robust Fix
// Handle both file:// and file:/// prefixes correctly
if (path.length() >= 7 && path.substr(0, 7) == L"file://") {
// Always strip exactly 7 characters for "file://"
path = path.substr(7);
}
Environment
- OS: Linux (Ubuntu 22.04, similar Unix systems)
- MLFMU version: 1.0.3
- Python: 3.10+
- fmpy: 0.3.22
- ONNX: 1.17.0
Additional Notes
This bug specifically affects Unix systems where absolute paths start with /. Windows systems using drive letters (e.g., C:\path) may not be affected due to different URI formatting.
The bug occurs because:
- fmpy converts absolute paths to
file:// URIs as per standard
- MLFMU's C++ code assumes all
file:// URIs have 3 slashes (file:///)
- The substring logic strips 8 characters instead of 7
- Result: missing leading slash in absolute paths
Summary
MLFMU generates FMUs with a C++ path parsing bug that causes FMU instantiation to fail on Unix systems when using absolute paths. The bug is in the
formatOnnxPathfunction which incorrectly strips URI prefixes.Root Cause
In
mlfmu/fmu_build/templates/onnx_fmu/onnxFmu.cpp, theformatOnnxPathfunction has incorrect substring logic around lines 44-46:Problem Description
When
fmpy(and other FMU libraries) pass absolute paths to the FMU, they convert them tofile://URIs:file:///tmp/extracted_fmu/resources/model.onnx/tmp/extracted_fmu/resources/model.onnxtmp/extracted_fmu/resources/model.onnx❌ (missing leading/)This causes ONNX model loading to fail with:
Reproduction Case
Expected vs Actual Behavior
file:///tmp/path/model.onnx/tmp/path/model.onnxtmp/path/model.onnx/file:///home/user/model.onnx/home/user/model.onnxhome/user/model.onnx/Impact
Proposed Fix
Option 1: Simple Fix
Option 2: Robust Fix
Environment
Additional Notes
This bug specifically affects Unix systems where absolute paths start with
/. Windows systems using drive letters (e.g.,C:\path) may not be affected due to different URI formatting.The bug occurs because:
file://URIs as per standardfile://URIs have 3 slashes (file:///)