Coverage for pydelica/model.py: 84%

1 import collections.abc

2 import os

3 import pathlib

4

5 from typing import Any, Iterator

6

7 import defusedxml.ElementTree as ET

8

9

10 class Model(collections.abc.MutableMapping):

11 """Connects a model source file with its associated XML parameter file"""

12

13 def __init__(

14 self,

15 modelica_file: pathlib.Path,

16 xml_model_file: pathlib.Path | None = None,

17 ) -> None:

18 """Initialise a new model instance from a modelica file

19

20 Parameters

21 ----------

22 modelica_file : str

23 Modelica model source file

24 xml_model_file : str, optional

25 XML file containing model parameters, by default None

26

27 Raises

28 ------

29 FileNotFoundError

30 if specified XML file does not exist

31 """

32 self._model_source = modelica_file

33 if xml_model_file:

34 self._model_xml = xml_model_file

35

36 if not os.path.exists(xml_model_file):

37 raise FileNotFoundError(

38 "Could not extract parameters, " f"no such file '{xml_model_file}"

39 )

40

41 self._parameters = {}

42

43 _xml_obj = ET.parse(xml_model_file).getroot()

44 _vars = list(_xml_obj.iterfind("ModelVariables"))[0]

45

46 for i, var in enumerate(_vars):

47 # Do not store derivatives etc

48 if "(" in var.attrib["name"]:

49 continue

50 _type_info = list(var)[0].attrib

51 _om_type = list(var)[0].tag

52

53 _value = _type_info["start"] if "start" in _type_info else None

54

55 _type, _value = self._get_type(_value, _om_type)

56

57 self._parameters[var.attrib["name"]] = {

58 "id": i,

59 "value": _value,

60 "type": _type,

61 }

62

63 def _get_type(self, value, om_type):

64 if om_type == "Boolean":

65 return bool, value.title() == "True" if value else value

66 elif om_type == "Integer":

67 return int, int(value) if value else value

68 elif om_type == "Real":

69 return float, float(value) if value else value

70 elif om_type == "String":

71 return str, value or value

72 else:

73 # TODO: Definitely other types

74 return None, value

75

76 def get_source_path(self) -> pathlib.Path:

77 """Retrieve the Modelica source file path

78

79 Returns

80 -------

81 pathlib.Path

82 path to the Modelica source file

83 """

84 return self._model_source

85

86 def write_params(self) -> None:

87 """Write parameter values to the XML file"""

88 _xml_obj = ET.parse(self._model_xml)

89 _iter_obj = _xml_obj.findall("ModelVariables/ScalarVariable")

90

91 for i, item in enumerate(_iter_obj):

92 # Do not write derivatives/functions

93 if "(" in item.attrib["name"]:

94 continue

95

96 _name = item.attrib["name"]

97

98 if (

99 not self._parameters[_name]["value"]

100 and self._parameters[_name]["value"] != 0

101 ):

102 continue

103

104 # Booleans are lower case in the XML

105 if self._parameters[_name]["type"] == bool:

106 _new_val = str(self._parameters[_name]["value"]).lower()

107 else:

108 _new_val = str(self._parameters[_name]["value"])

109

110 if _new_val:

111 _xml_obj.findall("ModelVariables/ScalarVariable")[i][0].set(

112 "start", _new_val

113 )

114 _xml_obj.write(self._model_xml)

115

116 def set_parameter(self, param_name: str, value: Any) -> None:

117 """Set parameter in XML to a given value

118

119 Parameters

120 ----------

121 param_name : str

122 name of parameter within XML file

123 value : Any

124 new value for the parameter

125 """

126 self._parameters[param_name]["value"] = value

127

128 def get_parameter(self, param_name: str) -> Any:

129 """Retrieve a parameter value by name

130

131 Parameters

132 ----------

133 param_name : str

134 name of the parameter to retrieve the value of

135

136 Returns

137 -------

138 Any

139 value of the given parameter

140

141 Raises

142 ------

143 AssertionError

144 If the returned value is a dictionary as opposed to numeric/string

145 """

146 if isinstance(self._parameters[param_name]["value"], dict):

147 raise AssertionError(

148 "Expected non-mutable value for requested parameter"

149 f"'{param_name}' but got type 'dict'"

150 )

151 return self._parameters[param_name]["value"]

152

153 def get_om_parameter_type(self, param_name: str):

154 """Returns parameter OM Type as Python type.

155

156 E.g. "Boolean" -> bool

157

158 Parameters

159 ----------

160 param_name : str

161 name of parameter to search for

162 """

163 return self._parameters[param_name]["type"]

164

165 def __iter__(self) -> Iterator:

166 return iter(self._parameters)

167

168 def __len__(self) -> int:

169 return len(self._parameters)

170

171 def __getitem__(self, key) -> Any:

172 return self._parameters[key]

173

174 def __setitem__(self, key, value) -> None:

175 self._parameters[key] = value

176

177 def __str__(self) -> str:

178 return self.__repr__()

179

180 def __delitem__(self, key) -> None:

181 del self._parameters[key]

182

183 def __repr__(self) -> str:

184 _params = {k: v["value"] for k, v in self._parameters.items()}

185 return f"OMModelProperties(params={_params})"

186

187 def update(self, other: "Model") -> None:

188 self._parameters.update(other._parameters)