Coverage for moptipyapps / spoc / submission.py: 78%

88 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-03 04:37 +0000

1""" 

2Create submission data for the SPOC challenge. 

3 

4>>> from moptipy.spaces.permutations import Permutations 

5>>> perms = Permutations((0, 1, 2, 3)) 

6>>> submit = SubmissionSpace(perms, "The Challenge", "The Problem") 

7>>> submit.initialize() 

8>>> the_x = submit.create() 

9>>> the_x[:] = perms.blueprint[:] 

10>>> the_str = submit.to_str(the_x) 

11>>> print(the_str) 

120;1;2;3 

13<BLANKLINE> 

14----------- SUBMISSION ----------- 

15<BLANKLINE> 

16[ 

17 { 

18 "challenge": "The Challenge", 

19 "problem": "The Problem", 

20 "decisionVector": [ 

21 0, 

22 1, 

23 2, 

24 3 

25 ] 

26 } 

27] 

28 

29>>> the_x2 = submit.from_str(the_str) 

30>>> submit.is_equal(the_x, the_x2) 

31True 

32""" 

33 

34from io import StringIO 

35from json import dump 

36from typing import Any, Final 

37 

38import numpy as np 

39from moptipy.api.space import Space 

40from moptipy.utils.logger import KeyValueLogSection 

41from pycommons.math.int_math import try_int 

42from pycommons.strings.chars import NEWLINE 

43from pycommons.types import type_error 

44 

45 

46def __listify(x: Any) -> list | int | float: 

47 """ 

48 Convert a solution to a list. 

49 

50 :param x: the x value 

51 :return: the list 

52 """ 

53 if isinstance(x, bool): 

54 return int(x) 

55 if isinstance(x, int): 

56 return x 

57 if isinstance(x, float): 

58 return try_int(x) 

59 if isinstance(x, np.number): 

60 if isinstance(x, np.integer): 

61 return int(x) 

62 if isinstance(x, np.floating): 

63 return try_int(float(x)) 

64 raise type_error(x, "x", (np.integer, np.floating)) 

65 if isinstance(x, np.ndarray): 

66 x = np.array(x).tolist() 

67 elif not isinstance(x, list): 

68 x = list(x) 

69 for i, y in enumerate(x): 

70 x[i] = __listify(y) 

71 return x 

72 

73 

74def _check_str(s: str | None) -> str | None: 

75 """ 

76 Check a string. 

77 

78 :param s: the string 

79 :return: the fixed string 

80 """ 

81 use: str | None = s 

82 if use is not None: 

83 use = str.strip(use) 

84 if str.__len__(use) <= 0: 

85 return None 

86 for c in NEWLINE: 

87 if c in use: 

88 raise ValueError(f"{s=} forbidden, contains {c!r}.") 

89 return use 

90 

91 

92def to_submission(challenge_id: str, 

93 problem_id: str, 

94 x: Any) -> str: 

95 """ 

96 Create a submission file text. 

97 

98 This function follows the specification given in 

99 <https://api.optimize.esa.int/data/tools/submission_helper.py>, with the 

100 exception that it tries to convert floating points to integers, where 

101 possible without loss of precision. 

102 

103 :param challenge_id: a string of the challenge identifier (found on the 

104 corresponding problem page) 

105 :param problem_id: a string of the problem identifier (found on the 

106 corresponding problem page) 

107 :param x: the result data 

108 

109 >>> print(to_submission("a", "b", (1, 2, 3))) 

110 [ 

111 { 

112 "challenge": "a", 

113 "problem": "b", 

114 "decisionVector": [ 

115 1, 

116 2, 

117 3 

118 ] 

119 } 

120 ] 

121 

122 >>> print(to_submission("a", "b", np.array(((1, 2, 3), (0.2, 4, 4.3))))) 

123 [ 

124 { 

125 "challenge": "a", 

126 "problem": "b", 

127 "decisionVector": [ 

128 [ 

129 1, 

130 2, 

131 3 

132 ], 

133 [ 

134 0.2, 

135 4, 

136 4.3 

137 ] 

138 ] 

139 } 

140 ] 

141 """ 

142 cid: str | None = _check_str(challenge_id) 

143 if cid is None: 

144 raise ValueError(f"{challenge_id=}") 

145 pid: str | None = _check_str(problem_id) 

146 if pid is None: 

147 raise ValueError(f"{problem_id=}") 

148 

149 # converting numpy datatypes to python datatypes 

150 x = __listify(x) 

151 

152 with StringIO() as io: 

153 dump([{"challenge": cid, 

154 "problem": pid, 

155 "decisionVector": x}], 

156 fp=io, indent=6) 

157 return str.strip(io.getvalue()) 

158 

159 

160#: the inner submission separator 

161_SUBMISSION_SEPARATOR_INNER: Final[str] = "----------- SUBMISSION -----------" 

162#: the submission separator 

163_SUBMISSION_SEPARATOR: Final[str] = f"\n\n{_SUBMISSION_SEPARATOR_INNER}\n\n" 

164 

165 

166class SubmissionSpace(Space): 

167 """ 

168 A space that also provides the submission data. 

169 

170 This space is designed to wrap around an existing space type and to 

171 generate the SPOC submission text when textifying space elements. 

172 """ 

173 

174 def __init__(self, space: Space, 

175 challenge_id: str, 

176 problem_id: str) -> None: 

177 """ 

178 Create a submission wrapper space. 

179 

180 :param space: the space to wrap 

181 :param challenge_id: the challenge identifier 

182 :param problem_id: the problem identifier 

183 """ 

184 self.create = space.create # type: ignore 

185 self.copy = space.copy # type: ignore 

186 self.is_equal = space.is_equal # type: ignore 

187 self.validate = space.validate # type: ignore 

188 self.n_points = space.n_points # type: ignore 

189 self.initialize = space.initialize # type: ignore 

190 

191 #: the internal space copy 

192 self.space: Final[Space] = space 

193 

194 pid = _check_str(problem_id) 

195 if pid is None: 

196 raise ValueError(f"{problem_id=}") 

197 #: the problem ID 

198 self.problem_id: Final[str] = pid 

199 cid = _check_str(challenge_id) 

200 if cid is None: 

201 raise ValueError(f"{challenge_id=}") 

202 #: the challenge ID 

203 self.challenge_id: Final[str] = cid 

204 

205 def to_str(self, x) -> str: # +book 

206 """ 

207 Obtain a textual representation of an instance of the data structure. 

208 

209 :param x: the instance 

210 :return: the string representation of x 

211 """ 

212 raw: Final[str] = str.strip(self.space.to_str(x)) 

213 if _SUBMISSION_SEPARATOR_INNER in raw: 

214 raise ValueError( 

215 f"{_SUBMISSION_SEPARATOR_INNER} not allowed in text.") 

216 sub: Final[str] = to_submission(self.challenge_id, self.problem_id, x) 

217 return f"{raw}{_SUBMISSION_SEPARATOR}{sub}" 

218 

219 def from_str(self, text: str) -> Any: # +book 

220 """ 

221 Transform a string `text` to one element of the space. 

222 

223 :param text: the input string 

224 :return: the element in the space corresponding to `text` 

225 """ 

226 i = str.find(text, _SUBMISSION_SEPARATOR_INNER) 

227 if i > 0: 

228 text = str.strip(text[:i]) 

229 return self.space.from_str(text) 

230 

231 def __str__(self) -> str: 

232 """ 

233 Get the submission space ID. 

234 

235 :return: the submission space ID 

236 """ 

237 return f"s_{self.space.__str__()}" 

238 

239 def log_parameters_to(self, logger: KeyValueLogSection) -> None: 

240 """ 

241 Log the parameters to a logger. 

242 

243 :param logger: the logger 

244 """ 

245 super().log_parameters_to(logger) 

246 logger.key_value("problemId", self.problem_id) 

247 logger.key_value("challengeId", self.challenge_id) 

248 with logger.scope("i") as i: 

249 self.space.log_parameters_to(i)