Python API Usage Guide¶
The Python API of FMU Manipulation Toolbox allows you to fully automate your FMU manipulation tasks and integrate them into your workflows.
Installation and Import¶
# Installation
# pip install fmu-manipulation-toolbox
# Import
from fmu_manipulation_toolbox.operations import (
FMU,
FMUError,
OperationSummary,
OperationStripTopLevel,
OperationMergeTopLevel,
OperationTrimUntil,
OperationRenameFromCSV,
OperationSaveNamesToCSV,
OperationRemoveRegexp,
OperationKeepOnlyRegexp,
OperationRemoveSources,
)
FMU Class: Fundamentals¶
The FMU class is a wrapper around an FMU archive (.fmu zip file). It extracts the archive into a temporary directory and allows you to apply operations to its modelDescription.xml descriptor.
Architecture
The FMU class does not expose variables or metadata directly as properties. All inspection and modification is done through operations — objects that implement the visitor pattern and are applied via fmu.apply_operation(operation).
Load an FMU¶
from fmu_manipulation_toolbox.operations import FMU, FMUError
try:
fmu = FMU("path/to/module.fmu")
except FMUError as e:
print(f"Cannot load FMU: {e}")
Apply an Operation¶
from fmu_manipulation_toolbox.operations import FMU, OperationSummary
fmu = FMU("module.fmu")
# Create an operation instance
operation = OperationSummary()
# Apply it — this parses modelDescription.xml and invokes the operation's callbacks
fmu.apply_operation(operation)
Save a Modified FMU¶
⚠️ Important: The original FMU is never modified. repack() creates a new archive.
Extract the Model Descriptor¶
# Save a copy of the (possibly modified) modelDescription.xml
fmu.save_descriptor("path/to/modelDescription.xml")
Filter Operations by Causality¶
You can restrict an operation to specific port types using the apply_on parameter:
from fmu_manipulation_toolbox.operations import FMU, OperationRemoveRegexp
fmu = FMU("module.fmu")
# Remove matching ports, but only among inputs
operation = OperationRemoveRegexp(r"^debug_.*")
fmu.apply_operation(operation, apply_on=["input"])
fmu.repack("module-clean.fmu")
Valid values for apply_on: "parameter", "input", "output", "local".
Available Operations¶
Display FMU Summary¶
from fmu_manipulation_toolbox.operations import FMU, OperationSummary
fmu = FMU("module.fmu")
operation = OperationSummary()
fmu.apply_operation(operation)
# After apply, you can access the port counts:
print(operation.nb_port_per_causality)
# e.g. {'input': 10, 'output': 15, 'parameter': 5, 'local': 12}
The summary is logged via the fmu_manipulation_toolbox logger.
Export Names to CSV¶
from fmu_manipulation_toolbox.operations import FMU, OperationSaveNamesToCSV
fmu = FMU("module.fmu")
# The filename is passed at construction time
operation = OperationSaveNamesToCSV("ports_list.csv")
fmu.apply_operation(operation)
# The CSV file is written during apply_operation and closed automatically.
The generated CSV has columns: name;newName;valueReference;causality;variability;scalarType;startValue.
Rename from CSV¶
from fmu_manipulation_toolbox.operations import FMU, OperationRenameFromCSV
fmu = FMU("module.fmu")
# Load the CSV mapping and apply renamings
operation = OperationRenameFromCSV("renaming.csv")
fmu.apply_operation(operation)
fmu.repack("module-renamed.fmu")
The CSV must be semicolon-delimited with at least two columns: original name and new name. If the new name is empty, the port is removed.
Remove Top-Level Hierarchy¶
from fmu_manipulation_toolbox.operations import FMU, OperationStripTopLevel
fmu = FMU("module.fmu")
operation = OperationStripTopLevel()
fmu.apply_operation(operation)
fmu.repack("module-flat.fmu")
Effect:
- Before:
System.Motor.Speed - After:
Motor.Speed
Merge Top-Level Hierarchy¶
from fmu_manipulation_toolbox.operations import FMU, OperationMergeTopLevel
fmu = FMU("module.fmu")
operation = OperationMergeTopLevel()
fmu.apply_operation(operation)
fmu.repack("module-merged.fmu")
Effect:
- Before:
System.Motor.Speed - After:
System_Motor.Speed
Trim Until Separator¶
from fmu_manipulation_toolbox.operations import FMU, OperationTrimUntil
fmu = FMU("module.fmu")
operation = OperationTrimUntil("_")
fmu.apply_operation(operation)
fmu.repack("module-trimmed.fmu")
Effect:
- Before:
_internal_Motor_Speed - After:
internal_Motor_Speed
Filter by Regular Expression¶
Keep only matching ports:
from fmu_manipulation_toolbox.operations import FMU, OperationKeepOnlyRegexp
fmu = FMU("module.fmu")
operation = OperationKeepOnlyRegexp(r"^Motor\..*")
fmu.apply_operation(operation)
fmu.repack("module-motor-only.fmu")
Remove matching ports:
from fmu_manipulation_toolbox.operations import FMU, OperationRemoveRegexp
fmu = FMU("module.fmu")
operation = OperationRemoveRegexp(r"^Internal\..*")
fmu.apply_operation(operation)
fmu.repack("module-clean.fmu")
Remove Sources¶
from fmu_manipulation_toolbox.operations import FMU, OperationRemoveSources
fmu = FMU("module.fmu")
operation = OperationRemoveSources()
fmu.apply_operation(operation)
fmu.repack("module-no-src.fmu")
Remoting Operations¶
from fmu_manipulation_toolbox.operations import FMU
from fmu_manipulation_toolbox.remoting import (
OperationAddRemotingWin32,
OperationAddRemotingWin64,
OperationAddFrontendWin32,
OperationAddFrontendWin64,
)
fmu = FMU("module32.fmu")
# Add 64-bit remoting interface to a 32-bit FMU
operation = OperationAddRemotingWin64()
fmu.apply_operation(operation)
fmu.repack("module-dual.fmu")
Note
Remoting operations are only supported for FMI 2.0 FMUs on Windows.
Checker Operations¶
from fmu_manipulation_toolbox.operations import FMU
from fmu_manipulation_toolbox.checker import get_checkers
fmu = FMU("module.fmu")
# Run all registered checkers (XSD validation, etc.)
for checker_class in get_checkers():
checker = checker_class()
fmu.apply_operation(checker)
Chain Multiple Operations¶
from fmu_manipulation_toolbox.operations import (
FMU,
OperationRemoveRegexp,
OperationStripTopLevel,
OperationRenameFromCSV,
)
fmu = FMU("module.fmu")
# Operation 1: Remove internal variables
op1 = OperationRemoveRegexp(r"^_internal.*")
fmu.apply_operation(op1)
# Operation 2: Simplify hierarchy
op2 = OperationStripTopLevel()
fmu.apply_operation(op2)
# Operation 3: Rename from CSV
op3 = OperationRenameFromCSV("new_names.csv")
fmu.apply_operation(op3)
# Save final result
fmu.repack("module-transformed.fmu")
Writing Custom Operations¶
You can create your own operation by subclassing OperationAbstract:
from fmu_manipulation_toolbox.operations import FMU, OperationAbstract, FMUPort
class OperationCountPorts(OperationAbstract):
"""Count ports by type."""
def __init__(self):
self.counts = {}
def port_attrs(self, fmu_port: FMUPort) -> int:
fmi_type = fmu_port.fmi_type
self.counts[fmi_type] = self.counts.get(fmi_type, 0) + 1
return 0 # 0 = keep port, non-zero = remove port
def closure(self):
"""Called after all ports have been processed."""
print(f"Port type counts: {self.counts}")
Available Callbacks¶
| Method | Called When |
|---|---|
fmi_attrs(attrs) | <fmiModelDescription> element is parsed |
cosimulation_attrs(attrs) | <CoSimulation> element is parsed |
experiment_attrs(attrs) | <DefaultExperiment> element is parsed |
port_attrs(fmu_port) -> int | Each port/variable. Return 0 to keep, non-zero to remove |
closure() | After the full descriptor has been parsed |
The fmu_port argument is an FMUPort object that supports dict-like access to attributes:
def port_attrs(self, fmu_port: FMUPort) -> int:
name = fmu_port["name"]
causality = fmu_port.get("causality", "local")
value_ref = fmu_port["valueReference"]
fmi_type = fmu_port.fmi_type # e.g. "Real", "Float64", "Integer"
# Modify in place
fmu_port["name"] = "new_" + name
return 0
Automation and Scripts¶
Batch Processing Script¶
from pathlib import Path
from fmu_manipulation_toolbox.operations import (
FMU,
FMUError,
OperationStripTopLevel,
OperationRemoveRegexp,
)
def process_fmu(input_path, output_path):
"""Process a single FMU."""
try:
fmu = FMU(str(input_path))
op1 = OperationRemoveRegexp(r"^_internal.*")
fmu.apply_operation(op1)
op2 = OperationStripTopLevel()
fmu.apply_operation(op2)
fmu.repack(str(output_path))
print(f"✓ Processed: {input_path}")
return True
except FMUError as e:
print(f"✗ Error with {input_path}: {e}")
return False
def process_directory(input_dir, output_dir):
"""Process all FMUs in a directory."""
input_path = Path(input_dir)
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
success_count = 0
fail_count = 0
for fmu_file in input_path.glob("*.fmu"):
output_file = output_path / f"processed_{fmu_file.name}"
if process_fmu(fmu_file, output_file):
success_count += 1
else:
fail_count += 1
print(f"\n=== Summary ===")
print(f"Successful: {success_count}")
print(f"Failed: {fail_count}")
if __name__ == "__main__":
process_directory("./input_fmus", "./output_fmus")
Parallel Processing¶
from multiprocessing import Pool
from pathlib import Path
from fmu_manipulation_toolbox.operations import FMU, FMUError, OperationStripTopLevel
def process_single_fmu(args):
"""Function to process one FMU (for multiprocessing)."""
input_path, output_path = args
try:
fmu = FMU(input_path)
operation = OperationStripTopLevel()
fmu.apply_operation(operation)
fmu.repack(output_path)
return (input_path, True, None)
except FMUError as e:
return (input_path, False, str(e))
def parallel_processing(input_dir, output_dir, num_workers=4):
"""Process FMUs in parallel."""
input_path = Path(input_dir)
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
tasks = []
for fmu_file in input_path.glob("*.fmu"):
output_file = output_path / f"processed_{fmu_file.name}"
tasks.append((str(fmu_file), str(output_file)))
with Pool(num_workers) as pool:
results = pool.map(process_single_fmu, tasks)
for fmu_path, success, error in results:
if success:
print(f"✓ {fmu_path}")
else:
print(f"✗ {fmu_path}: {error}")
# Usage
parallel_processing("./input_fmus", "./output_fmus", num_workers=4)
Best Practices¶
✅ Do¶
# Always handle loading errors
from fmu_manipulation_toolbox.operations import FMU, FMUError
try:
fmu = FMU("module.fmu")
except FMUError as e:
print(f"Loading error: {e}")
sys.exit(1)
# Use absolute paths to avoid issues
from pathlib import Path
fmu_path = Path("module.fmu").resolve()
fmu = FMU(str(fmu_path))
# Chain operations in the correct order
# (remove first, then rename the remaining ports)
❌ Avoid¶
# ❌ Forgetting to apply the operation
operation = OperationStripTopLevel()
# fmu.apply_operation(operation) # FORGOTTEN!
fmu.repack("output.fmu") # No changes applied!
# ❌ Overwriting the original FMU
fmu = FMU("original.fmu")
fmu.apply_operation(operation)
fmu.repack("original.fmu") # DANGER! Never overwrite the source file
# ✅ Always create a new file
fmu.repack("original_modified.fmu")