Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changes/1805.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Bugfix in comparing model versions in integration tests to decide which tests should run.
Bugfix in getting model parameter values with units for the case of value-unit pairs consisting of lists with None entries.
34 changes: 29 additions & 5 deletions src/simtools/model/model_parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,31 @@ def get_parameter_value(self, par_name, parameter_dict=None):

return _parameter

def _create_quantity_for_value(self, value, unit):
"""
Create an astropy quantity for a single value and unit.

Parameters
----------
value: numeric or str
The value to create a quantity for.
unit: str or None
The unit string or None.

Returns
-------
astropy.Quantity or original value
Astropy quantity for numeric values with units,
original value for non-numeric values.
"""
if not isinstance(value, int | float):
return value

if unit is None or unit == "null":
return value * u.dimensionless_unscaled

return value * u.Unit(unit)

def get_parameter_value_with_unit(self, par_name):
"""
Get the value of an existing parameter of the model as an Astropy Quantity with its unit.
Expand Down Expand Up @@ -226,13 +251,12 @@ def get_parameter_value_with_unit(self, par_name):
if (isinstance(_value, (int | float))) or (len(_value) > len(_unit)):
return _value * u.Unit(_unit[0])

# entries with 'null' units should be returned as dimensionless
_astropy_units = [
u.Unit(item) if item != "null" else u.dimensionless_unscaled for item in _unit
# Create list of quantities for multiple values with different units
return [
self._create_quantity_for_value(_value[i], _unit[i] if i < len(_unit) else None)
for i in range(len(_value))
]

return [_value[i] * _astropy_units[i] for i in range(len(_value))]

except (KeyError, TypeError, AttributeError) as exc:
self._logger.debug(
f"{exc} encountered for parameter {par_name}, returning only value without units."
Expand Down
2 changes: 1 addition & 1 deletion src/simtools/simtel/simulator_light_emission.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def __init__(self, light_emission_config, db_config=None, label=None):
simtel_path=light_emission_config.get("simtel_path"), label=label, corsika_config=None
)

self.output_directory = self.io_handler.get_output_directory(label)
self.output_directory = self.io_handler.get_output_directory()

self.telescope_model, self.site_model, self.calibration_model = (
initialize_simulation_models(
Expand Down
21 changes: 17 additions & 4 deletions src/simtools/testing/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pathlib import Path

import simtools.utils.general as gen
import simtools.version as simtools_version
from simtools.io import ascii_handler

_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -130,11 +131,23 @@ def configure(config, tmp_test_directory, request):


def _skip_test_for_model_version(config, model_version_requested):
"""Skip test if model version requested is not supported."""
if config.get("model_version_use_current") is None or model_version_requested is None:
"""
Skip test if model version requested is not supported.

Compares full and major.minor version strings.
"""
if not (config.get("model_version_use_current") and model_version_requested):
return
model_version_config = config["configuration"]["model_version"]
if model_version_requested != model_version_config:
model_version_config = str(config["configuration"]["model_version"])

if (
simtools_version.compare_versions(
model_version_requested,
model_version_config,
level=simtools_version.version_kind(model_version_requested),
)
!= 0
):
raise VersionError(
f"Model version requested {model_version_requested} not supported for this test"
)
Expand Down
61 changes: 61 additions & 0 deletions src/simtools/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

from packaging.version import InvalidVersion, Version

MAJOR_MINOR_PATCH = "major.minor.patch"
MAJOR_MINOR = "major.minor"

try:
try:
from ._dev_version import version
Expand Down Expand Up @@ -128,3 +131,61 @@ def sort_versions(version_list, reverse=False):
return [str(v) for v in sorted(map(Version, version_list), reverse=reverse)]
except InvalidVersion as exc:
raise ValueError(f"Invalid version in list: {version_list}") from exc


def version_kind(version_string):
"""
Determine the kind of version string.

Parameters
----------
version_string : str
The version string to analyze.

Returns
-------
str
The kind of version string ("major.minor", "major.minor.patch", or "major").
"""
try:
ver = Version(version_string)
except InvalidVersion as exc:
raise ValueError(f"Invalid version string: {version_string}") from exc
if ver.release and len(ver.release) >= 3:
return MAJOR_MINOR_PATCH
if len(ver.release) == 2:
return MAJOR_MINOR
return "major"


def compare_versions(version_string_1, version_string_2, level=MAJOR_MINOR_PATCH):
"""
Compare two versions at the given level: "major", "major.minor", "major.minor.patch".

Parameters
----------
version_string_1 : str
First version string to compare.
version_string_2 : str
Second version string to compare.
level : str, optional
Level of comparison: "major", "major.minor", or "major.minor.patch"

Returns
-------
int
-1 if version_string_1 < version_string_2
0 if version_string_1 == version_string_2
1 if version_string_1 > version_string_2
"""
ver1 = Version(version_string_1).release
ver2 = Version(version_string_2).release

if level == "major":
ver1, ver2 = ver1[:1], ver2[:1]
elif level == MAJOR_MINOR:
ver1, ver2 = ver1[:2], ver2[:2]
elif level != MAJOR_MINOR_PATCH:
raise ValueError(f"Unknown level: {level}")

return (ver1 > ver2) - (ver1 < ver2)
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ applications:
- 0.
- 0.
- -1.
model_version: 6.0.0
model_version: 6.0
output_path: simtools-output/illuminator-variable
site: North
telescope: MSTN-04
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ applications:
configuration:
light_source: ILLN-01
model_version: 6.0
output_path: simtools-output/illuminator-layout
output_path: simtools-output
site: North
telescope: MSTN-04
test: true
Expand Down
6 changes: 6 additions & 0 deletions tests/unit_tests/model/test_model_parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,9 @@ def test_add_additional_models(telescope_model_lst, mocker):
models_dict = {"model1": mock_model, "model2": mock_model2}
telescope_copy._add_additional_models(models_dict)
assert telescope_copy.parameters["param2"] == "value2"


def test__create_quantity_for_value(telescope_model_lst):
assert telescope_model_lst._create_quantity_for_value("abc", "m") == "abc"
assert telescope_model_lst._create_quantity_for_value(5, "m") == 5 * u.m
assert telescope_model_lst._create_quantity_for_value(5, None) == 5
45 changes: 45 additions & 0 deletions tests/unit_tests/test_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,48 @@ def test_sort_versions():
invalid_versions = ["1.0.0", "not_a_version", "2.0.0"]
with pytest.raises(ValueError, match=r"Invalid version in list"):
version.sort_versions(invalid_versions)


def test_version_kind():
assert version.version_kind("6.0.0") == version.MAJOR_MINOR_PATCH
assert version.version_kind("6.0") == version.MAJOR_MINOR
assert version.version_kind("6") == "major"
with pytest.raises(ValueError, match=r"Invalid version string"):
version.version_kind("no_version")


def test_compare_versions():
# Test exact equality
assert version.compare_versions("1.0.0", "1.0.0") == 0
assert version.compare_versions("2.5.3", "2.5.3") == 0

# Test major version comparison
assert version.compare_versions("2.0.0", "1.0.0") == 1
assert version.compare_versions("1.0.0", "2.0.0") == -1

# Test minor version comparison
assert version.compare_versions("1.2.0", "1.1.0") == 1
assert version.compare_versions("1.1.0", "1.2.0") == -1

# Test patch version comparison
assert version.compare_versions("1.0.2", "1.0.1") == 1
assert version.compare_versions("1.0.1", "1.0.2") == -1

# Test level parameter - major only
assert version.compare_versions("1.9.9", "1.0.0", level="major") == 0
assert version.compare_versions("2.0.0", "1.9.9", level="major") == 1
assert version.compare_versions("1.0.0", "2.9.9", level="major") == -1

# Test level parameter - major.minor
assert version.compare_versions("1.2.9", "1.2.0", level=version.MAJOR_MINOR) == 0
assert version.compare_versions("1.3.0", "1.2.9", level=version.MAJOR_MINOR) == 1
assert version.compare_versions("1.2.0", "1.3.9", level=version.MAJOR_MINOR) == -1

# Test level parameter - major.minor.patch (default)
assert version.compare_versions("1.2.3", "1.2.3", level=version.MAJOR_MINOR_PATCH) == 0
assert version.compare_versions("1.2.4", "1.2.3", level=version.MAJOR_MINOR_PATCH) == 1
assert version.compare_versions("1.2.3", "1.2.4", level=version.MAJOR_MINOR_PATCH) == -1

# Test invalid level
with pytest.raises(ValueError, match=r"Unknown level"):
version.compare_versions("1.0.0", "1.0.0", level="invalid")