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
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-03 04:37 +0000
1"""
2Create submission data for the SPOC challenge.
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]
29>>> the_x2 = submit.from_str(the_str)
30>>> submit.is_equal(the_x, the_x2)
31True
32"""
34from io import StringIO
35from json import dump
36from typing import Any, Final
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
46def __listify(x: Any) -> list | int | float:
47 """
48 Convert a solution to a list.
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
74def _check_str(s: str | None) -> str | None:
75 """
76 Check a string.
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
92def to_submission(challenge_id: str,
93 problem_id: str,
94 x: Any) -> str:
95 """
96 Create a submission file text.
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.
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
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 ]
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=}")
149 # converting numpy datatypes to python datatypes
150 x = __listify(x)
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())
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"
166class SubmissionSpace(Space):
167 """
168 A space that also provides the submission data.
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 """
174 def __init__(self, space: Space,
175 challenge_id: str,
176 problem_id: str) -> None:
177 """
178 Create a submission wrapper space.
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
191 #: the internal space copy
192 self.space: Final[Space] = space
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
205 def to_str(self, x) -> str: # +book
206 """
207 Obtain a textual representation of an instance of the data structure.
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}"
219 def from_str(self, text: str) -> Any: # +book
220 """
221 Transform a string `text` to one element of the space.
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)
231 def __str__(self) -> str:
232 """
233 Get the submission space ID.
235 :return: the submission space ID
236 """
237 return f"s_{self.space.__str__()}"
239 def log_parameters_to(self, logger: KeyValueLogSection) -> None:
240 """
241 Log the parameters to a logger.
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)