Skip to content

Commit fdb488a

Browse files
committed
feat: add Dymola compilation helper functions to handle license checks and refactor tests
1 parent 7392640 commit fdb488a

3 files changed

Lines changed: 131 additions & 94 deletions

File tree

src/python/feelpp/mo2fmu/compilers/openmodelica.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
from __future__ import annotations
1515

1616
import contextlib
17+
import importlib
1718
import os
1819
import shutil
1920
import subprocess
2021
import tempfile
2122
import uuid
23+
import warnings
2224
from dataclasses import dataclass, field
2325
from pathlib import Path
2426
from typing import Any, Optional
@@ -208,6 +210,41 @@ def _build_fmu_flags(self, config: CompilationConfig) -> str:
208210

209211
return f'version="{fmi_version}", fmuType="{fmi_type}", platforms={platforms}'
210212

213+
def _getPyparsingDeprecationWarning(self) -> type[Warning] | None:
214+
"""Return pyparsing's deprecation warning category when available."""
215+
try:
216+
pyparsingWarnings = importlib.import_module("pyparsing.warnings")
217+
except ImportError:
218+
return None
219+
220+
warningCategory = getattr(pyparsingWarnings, "PyparsingDeprecationWarning", None)
221+
if isinstance(warningCategory, type) and issubclass(warningCategory, Warning):
222+
return warningCategory
223+
return None
224+
225+
def _getOmcSessionClass(self) -> Any:
226+
"""Import OMPython while suppressing its deprecated pyparsing usage warnings."""
227+
warningCategory = self._getPyparsingDeprecationWarning()
228+
229+
with warnings.catch_warnings():
230+
if warningCategory is not None:
231+
warnings.filterwarnings(
232+
"ignore",
233+
message=".*deprecated.*",
234+
category=warningCategory,
235+
)
236+
else:
237+
warnings.filterwarnings(
238+
"ignore",
239+
message=".*deprecated.*",
240+
category=DeprecationWarning,
241+
module="pyparsing.*",
242+
)
243+
244+
from OMPython import OMCSessionZMQ
245+
246+
return OMCSessionZMQ
247+
211248
def _compile_with_ompython(
212249
self,
213250
model: ModelicaModel,
@@ -216,7 +253,7 @@ def _compile_with_ompython(
216253
logger: Any,
217254
) -> CompilationResult:
218255
"""Compile using OMPython session."""
219-
from OMPython import OMCSessionZMQ
256+
OMCSessionZMQ = self._getOmcSessionClass()
220257

221258
fmu_name = config.output_name or model.model_name
222259
target_fmu = output_dir / f"{fmu_name}.fmu"
@@ -541,7 +578,7 @@ def _check_model_ompython(
541578
self, model: ModelicaModel, packages: Optional[list[str]] = None
542579
) -> bool:
543580
"""Check model using OMPython."""
544-
from OMPython import OMCSessionZMQ
581+
OMCSessionZMQ = self._getOmcSessionClass()
545582

546583
omc = None
547584
try:

tests/test_fmu_simulation.py

Lines changed: 52 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import pytest
1414

1515
from feelpp.mo2fmu import compileFmu
16+
from feelpp.mo2fmu.compilers.base import CompilationResult
1617
from feelpp.mo2fmu.compilers.dymola import DymolaConfig
1718

1819

@@ -52,6 +53,41 @@ def _get_dymola_config() -> DymolaConfig:
5253
)
5354

5455

56+
def _hasNoShareableLicense(result: CompilationResult) -> bool:
57+
"""Return True when Dymola compilation fell back to the trial license."""
58+
errorMessage = (result.error_message or "").lower()
59+
unavailablePatterns = (
60+
"shareable license is not available",
61+
"shareable license users exceeded",
62+
"maximum number of shareable license users exceeded",
63+
"trial license",
64+
)
65+
return any(pattern in errorMessage for pattern in unavailablePatterns)
66+
67+
68+
def _compileWithDymolaOrSkip(
69+
*,
70+
mo: Path,
71+
outdir: Path,
72+
fmiType: str,
73+
fmiVersion: str,
74+
) -> CompilationResult:
75+
"""Compile with Dymola, skipping the test when no floating seat is available."""
76+
result = compileFmu(
77+
mo=mo,
78+
outdir=outdir,
79+
backend="dymola",
80+
fmiType=fmiType,
81+
fmiVersion=fmiVersion,
82+
verbose=True,
83+
force=True,
84+
dymolaConfig=_get_dymola_config(),
85+
)
86+
if _hasNoShareableLicense(result):
87+
pytest.skip(f"Dymola compile license not available:\n{result.error_message}")
88+
return result
89+
90+
5591
# =============================================================================
5692
# FMPy Simulation Tests
5793
# =============================================================================
@@ -69,15 +105,11 @@ def test_simulate_cosimulation_fmu(self, simpleOdeModel: Path, tmp_path: Path) -
69105

70106
# Compile the model to Co-Simulation FMU
71107
outdir = tmp_path / "output_cs"
72-
result = compileFmu(
108+
result = _compileWithDymolaOrSkip(
73109
mo=simpleOdeModel,
74110
outdir=outdir,
75-
backend="dymola",
76111
fmiType="cs",
77112
fmiVersion="2",
78-
verbose=True,
79-
force=True,
80-
dymolaConfig=_get_dymola_config(),
81113
)
82114

83115
assert result.success, f"Compilation failed: {result.error_message}"
@@ -109,15 +141,11 @@ def test_simulate_model_exchange_fmu(self, simpleOdeModel: Path, tmp_path: Path)
109141

110142
# Compile the model to Model Exchange FMU
111143
outdir = tmp_path / "output_me"
112-
result = compileFmu(
144+
result = _compileWithDymolaOrSkip(
113145
mo=simpleOdeModel,
114146
outdir=outdir,
115-
backend="dymola",
116147
fmiType="me",
117148
fmiVersion="2",
118-
verbose=True,
119-
force=True,
120-
dymolaConfig=_get_dymola_config(),
121149
)
122150

123151
assert result.success, f"Compilation failed: {result.error_message}"
@@ -159,15 +187,11 @@ def test_simulate_model_exchange_sinusoidal(
159187

160188
# Compile to Model Exchange
161189
outdir = tmp_path / "output_sin_me"
162-
result = compileFmu(
190+
result = _compileWithDymolaOrSkip(
163191
mo=odeSinusoidalModel,
164192
outdir=outdir,
165-
backend="dymola",
166193
fmiType="me",
167194
fmiVersion="2",
168-
verbose=True,
169-
force=True,
170-
dymolaConfig=_get_dymola_config(),
171195
)
172196

173197
assert result.success, f"Compilation failed: {result.error_message}"
@@ -198,29 +222,21 @@ def test_compare_cs_and_me_results(self, simpleOdeModel: Path, tmp_path: Path) -
198222

199223
# Compile Co-Simulation FMU
200224
outdir_cs = tmp_path / "output_cs"
201-
result_cs = compileFmu(
225+
result_cs = _compileWithDymolaOrSkip(
202226
mo=simpleOdeModel,
203227
outdir=outdir_cs,
204-
backend="dymola",
205228
fmiType="cs",
206229
fmiVersion="2",
207-
verbose=True,
208-
force=True,
209-
dymolaConfig=_get_dymola_config(),
210230
)
211231
assert result_cs.success
212232

213233
# Compile Model Exchange FMU
214234
outdir_me = tmp_path / "output_me"
215-
result_me = compileFmu(
235+
result_me = _compileWithDymolaOrSkip(
216236
mo=simpleOdeModel,
217237
outdir=outdir_me,
218-
backend="dymola",
219238
fmiType="me",
220239
fmiVersion="2",
221-
verbose=True,
222-
force=True,
223-
dymolaConfig=_get_dymola_config(),
224240
)
225241
assert result_me.success
226242

@@ -261,15 +277,11 @@ def test_simulate_fmi3_model_exchange(self, simpleOdeModel: Path, tmp_path: Path
261277

262278
# Compile to FMI 3.0 Model Exchange
263279
outdir = tmp_path / "output_fmi3_me"
264-
result = compileFmu(
280+
result = _compileWithDymolaOrSkip(
265281
mo=simpleOdeModel,
266282
outdir=outdir,
267-
backend="dymola",
268283
fmiType="me",
269284
fmiVersion="3",
270-
verbose=True,
271-
force=True,
272-
dymolaConfig=_get_dymola_config(),
273285
)
274286

275287
# FMI 3.0 may not be supported by all Dymola versions
@@ -301,15 +313,11 @@ def test_simulate_fmi3_cosimulation(self, simpleOdeModel: Path, tmp_path: Path)
301313

302314
# Compile to FMI 3.0 Co-Simulation
303315
outdir = tmp_path / "output_fmi3_cs"
304-
result = compileFmu(
316+
result = _compileWithDymolaOrSkip(
305317
mo=simpleOdeModel,
306318
outdir=outdir,
307-
backend="dymola",
308319
fmiType="cs",
309320
fmiVersion="3",
310-
verbose=True,
311-
force=True,
312-
dymolaConfig=_get_dymola_config(),
313321
)
314322

315323
# FMI 3.0 may not be supported
@@ -344,15 +352,11 @@ def test_validate_fmu(self, simpleOdeModel: Path, tmp_path: Path) -> None:
344352

345353
# Compile the model
346354
outdir = tmp_path / "output"
347-
result = compileFmu(
355+
result = _compileWithDymolaOrSkip(
348356
mo=simpleOdeModel,
349357
outdir=outdir,
350-
backend="dymola",
351358
fmiType="cs",
352359
fmiVersion="2",
353-
verbose=True,
354-
force=True,
355-
dymolaConfig=_get_dymola_config(),
356360
)
357361

358362
assert result.success
@@ -377,15 +381,11 @@ def test_read_model_variables(self, simpleOdeModel: Path, tmp_path: Path) -> Non
377381

378382
# Compile the model
379383
outdir = tmp_path / "output"
380-
result = compileFmu(
384+
result = _compileWithDymolaOrSkip(
381385
mo=simpleOdeModel,
382386
outdir=outdir,
383-
backend="dymola",
384387
fmiType="me",
385388
fmiVersion="2",
386-
verbose=True,
387-
force=True,
388-
dymolaConfig=_get_dymola_config(),
389389
)
390390

391391
assert result.success
@@ -420,15 +420,11 @@ class TestBouncingBallSimulation:
420420
def test_compile_bouncing_ball_fmi2(self, bouncingBallModel: Path, tmp_path: Path) -> None:
421421
"""Test compiling bouncing ball to FMI 2.0."""
422422
outdir = tmp_path / "output_bb_fmi2"
423-
result = compileFmu(
423+
result = _compileWithDymolaOrSkip(
424424
mo=bouncingBallModel,
425425
outdir=outdir,
426-
backend="dymola",
427426
fmiType="me",
428427
fmiVersion="2",
429-
verbose=True,
430-
force=True,
431-
dymolaConfig=_get_dymola_config(),
432428
)
433429

434430
assert result.success, f"Compilation failed: {result.error_message}"
@@ -443,15 +439,11 @@ def test_simulate_bouncing_ball_cosimulation(
443439

444440
# Compile to Co-Simulation
445441
outdir = tmp_path / "output_bb_cs"
446-
result = compileFmu(
442+
result = _compileWithDymolaOrSkip(
447443
mo=bouncingBallModel,
448444
outdir=outdir,
449-
backend="dymola",
450445
fmiType="cs",
451446
fmiVersion="2",
452-
verbose=True,
453-
force=True,
454-
dymolaConfig=_get_dymola_config(),
455447
)
456448

457449
assert result.success, f"Compilation failed: {result.error_message}"
@@ -489,15 +481,11 @@ def test_simulate_bouncing_ball_model_exchange(
489481

490482
# Compile to Model Exchange
491483
outdir = tmp_path / "output_bb_me"
492-
result = compileFmu(
484+
result = _compileWithDymolaOrSkip(
493485
mo=bouncingBallModel,
494486
outdir=outdir,
495-
backend="dymola",
496487
fmiType="me",
497488
fmiVersion="2",
498-
verbose=True,
499-
force=True,
500-
dymolaConfig=_get_dymola_config(),
501489
)
502490

503491
assert result.success, f"Compilation failed: {result.error_message}"
@@ -535,15 +523,11 @@ def test_bouncing_ball_bounce_count(self, bouncingBallModel: Path, tmp_path: Pat
535523

536524
# Compile to Co-Simulation (more reliable for event handling)
537525
outdir = tmp_path / "output_bb_count"
538-
result = compileFmu(
526+
result = _compileWithDymolaOrSkip(
539527
mo=bouncingBallModel,
540528
outdir=outdir,
541-
backend="dymola",
542529
fmiType="cs",
543530
fmiVersion="2",
544-
verbose=True,
545-
force=True,
546-
dymolaConfig=_get_dymola_config(),
547531
)
548532

549533
assert result.success
@@ -570,15 +554,11 @@ def test_bouncing_ball_fmi3_model_exchange(
570554

571555
# Compile to FMI 3.0 Model Exchange
572556
outdir = tmp_path / "output_bb_fmi3_me"
573-
result = compileFmu(
557+
result = _compileWithDymolaOrSkip(
574558
mo=bouncingBallModel,
575559
outdir=outdir,
576-
backend="dymola",
577560
fmiType="me",
578561
fmiVersion="3",
579-
verbose=True,
580-
force=True,
581-
dymolaConfig=_get_dymola_config(),
582562
)
583563

584564
# FMI 3.0 may not be supported
@@ -616,15 +596,11 @@ def test_bouncing_ball_fmi3_cosimulation(self, bouncingBallModel: Path, tmp_path
616596

617597
# Compile to FMI 3.0 Co-Simulation
618598
outdir = tmp_path / "output_bb_fmi3_cs"
619-
result = compileFmu(
599+
result = _compileWithDymolaOrSkip(
620600
mo=bouncingBallModel,
621601
outdir=outdir,
622-
backend="dymola",
623602
fmiType="cs",
624603
fmiVersion="3",
625-
verbose=True,
626-
force=True,
627-
dymolaConfig=_get_dymola_config(),
628604
)
629605

630606
# FMI 3.0 may not be supported
@@ -653,15 +629,11 @@ def test_bouncing_ball_with_wind_input(self, bouncingBallModel: Path, tmp_path:
653629

654630
# Compile to Model Exchange
655631
outdir = tmp_path / "output_bb_wind"
656-
result = compileFmu(
632+
result = _compileWithDymolaOrSkip(
657633
mo=bouncingBallModel,
658634
outdir=outdir,
659-
backend="dymola",
660635
fmiType="me",
661636
fmiVersion="2",
662-
verbose=True,
663-
force=True,
664-
dymolaConfig=_get_dymola_config(),
665637
)
666638

667639
assert result.success

0 commit comments

Comments
 (0)