pcod-common v1.0.0
Shared preprocessing and postprocessing for point-cloud object detection
Loading...
Searching...
No Matches
model_manifest.cpp
Go to the documentation of this file.
1// Copyright Institute for Automotive Engineering (ika), RWTH Aachen University
2// SPDX-License-Identifier: Apache-2.0
3
6
7#include <filesystem>
8#include <initializer_list>
9#include <sstream>
10#include <stdexcept>
11#include <string>
12#include <vector>
13
14#include <yaml-cpp/yaml.h>
15
16namespace pcod_common {
17
18namespace {
19
20void EnsureMap(const YAML::Node& node, const std::string& field) {
21 if (!node || !node.IsMap()) {
22 throw std::runtime_error("Manifest key '" + field + "' must be a mapping");
23 }
24}
25
26void EnsureSequence(const YAML::Node& node, const std::string& field) {
27 if (!node || !node.IsSequence()) {
28 throw std::runtime_error("Manifest field '" + field + "' must be a sequence");
29 }
30}
31
32void EnsureKnownKeys(const YAML::Node& node, const std::string& scope, std::initializer_list<const char*> allowed) {
33 EnsureMap(node, scope);
34 for (const auto& entry : node) {
35 const std::string key = entry.first.as<std::string>();
36 bool supported = false;
37 for (const char* candidate : allowed) {
38 if (key == candidate) {
39 supported = true;
40 break;
41 }
42 }
43 if (!supported) {
44 throw std::runtime_error("Manifest " + scope + " contains unsupported key '" + key + "'");
45 }
46 }
47}
48
49YAML::Node RequireChild(const YAML::Node& parent, const std::string& key, const std::string& field) {
50 EnsureMap(parent, field);
51 YAML::Node child = parent[key];
52 if (!child) {
53 throw std::runtime_error("Manifest field '" + field + "' is missing");
54 }
55 return child;
56}
57
58YAML::Node LoadOptionalChild(const YAML::Node& parent, const std::string& key) {
59 if (!parent || !parent.IsMap()) {
60 return {};
61 }
62 return parent[key];
63}
64
65std::string RequireScalarText(const YAML::Node& node, const std::string& field) {
66 if (!node || !node.IsScalar()) {
67 throw std::runtime_error("Manifest field '" + field + "' must be a scalar");
68 }
69 return node.Scalar();
70}
71
72template <typename T>
73T RequireScalarValue(const YAML::Node& node, const std::string& field) {
74 if (!node || !node.IsScalar()) {
75 throw std::runtime_error("Manifest field '" + field + "' must be a scalar");
76 }
77 try {
78 return node.as<T>();
79 } catch (const YAML::Exception& exc) {
80 throw std::runtime_error("Manifest field '" + field + "' is invalid: " + std::string(exc.what()));
81 }
82}
83
84template <typename T>
85T LoadOptionalScalarValue(const YAML::Node& node, const std::string& field, const T& default_value) {
86 if (!node) {
87 return default_value;
88 }
89 return RequireScalarValue<T>(node, field);
90}
91
92std::string LoadOptionalText(const YAML::Node& node, const std::string& field) {
93 if (!node) {
94 return "";
95 }
96 return RequireScalarText(node, field);
97}
98
99std::string RequireNonEmptyString(const std::string& value, const std::string& field) {
100 if (value.empty()) {
101 throw std::runtime_error("Manifest field '" + field + "' must not be empty");
102 }
103 return value;
104}
105
106std::string RequireBundleRelativePath(const std::string& value, const std::string& field) {
107 RequireNonEmptyString(value, field);
108 const std::filesystem::path bundle_path(value);
109 if (bundle_path.is_absolute()) {
110 throw std::runtime_error("Manifest field '" + field + "' must be a bundle-relative path");
111 }
112 for (const auto& part : bundle_path) {
113 if (part == "..") {
114 throw std::runtime_error("Manifest field '" + field + "' must not escape the bundle root");
115 }
116 }
117 return value;
118}
119
120std::array<float, 2> RequireRange2(const YAML::Node& node, const std::string& field) {
121 EnsureSequence(node, field);
122 if (node.size() != 2) {
123 throw std::runtime_error("Manifest field '" + field + "' must be a 2-element sequence");
124 }
125 return {node[0].as<float>(), node[1].as<float>()};
126}
127
128std::vector<int> RequirePositiveIntSequence(const YAML::Node& node, const std::string& field) {
129 EnsureSequence(node, field);
130 if (node.size() == 0) {
131 throw std::runtime_error("Manifest field '" + field + "' must be a non-empty sequence");
132 }
133 std::vector<int> values;
134 values.reserve(node.size());
135 for (std::size_t i = 0; i < node.size(); ++i) {
136 const int value = node[i].as<int>();
137 if (value <= 0) {
138 throw std::runtime_error("Manifest field '" + field + "[" + std::to_string(i) + "]' must be positive");
139 }
140 values.push_back(value);
141 }
142 return values;
143}
144
145std::vector<std::string> RequireStringSequence(const YAML::Node& node, const std::string& field) {
146 EnsureSequence(node, field);
147 if (node.size() == 0) {
148 throw std::runtime_error("Manifest field '" + field + "' must be a non-empty sequence");
149 }
150 std::vector<std::string> values;
151 values.reserve(node.size());
152 for (std::size_t i = 0; i < node.size(); ++i) {
153 values.push_back(RequireNonEmptyString(RequireScalarText(node[i], field + "[" + std::to_string(i) + "]"),
154 field + "[" + std::to_string(i) + "]"));
155 }
156 return values;
157}
158
159std::vector<float> RequireScoreThresholds(const YAML::Node& node, const std::string& field) {
160 std::vector<float> values;
161 if (node.IsSequence()) {
162 if (node.size() == 0) {
163 throw std::runtime_error("Manifest field '" + field + "' must not be empty");
164 }
165 values.reserve(node.size());
166 for (std::size_t i = 0; i < node.size(); ++i) {
167 values.push_back(node[i].as<float>());
168 }
169 return values;
170 }
171 if (!node.IsScalar()) {
172 throw std::runtime_error("Manifest field '" + field + "' must be a scalar or sequence");
173 }
174 values.push_back(node.as<float>());
175 return values;
176}
177
178std::vector<ArtifactConfig::Tensor> RequireTensorList(const YAML::Node& node, const std::string& field) {
179 EnsureSequence(node, field);
180 if (node.size() == 0) {
181 throw std::runtime_error("Manifest field '" + field + "' must be a non-empty sequence");
182 }
183
184 std::vector<ArtifactConfig::Tensor> tensors;
185 tensors.reserve(node.size());
186 for (std::size_t i = 0; i < node.size(); ++i) {
187 const std::string item_scope = field + "[" + std::to_string(i) + "]";
188 YAML::Node entry = node[i];
189 EnsureKnownKeys(entry, item_scope, {"name", "dtype", "shape", "description"});
190 YAML::Node name = RequireChild(entry, "name", item_scope + ".name");
191 YAML::Node dtype = RequireChild(entry, "dtype", item_scope + ".dtype");
192 YAML::Node shape = RequireChild(entry, "shape", item_scope + ".shape");
193
194 ArtifactConfig::Tensor tensor;
195 tensor.name = RequireNonEmptyString(RequireScalarText(name, item_scope + ".name"), item_scope + ".name");
196 tensor.dtype = RequireNonEmptyString(RequireScalarText(dtype, item_scope + ".dtype"), item_scope + ".dtype");
197
198 EnsureSequence(shape, item_scope + ".shape");
199 if (shape.size() == 0) {
200 throw std::runtime_error("Manifest field '" + item_scope + ".shape' must be a non-empty sequence");
201 }
202 tensor.shape.reserve(shape.size());
203 for (std::size_t dim_idx = 0; dim_idx < shape.size(); ++dim_idx) {
204 tensor.shape.push_back(RequireScalarText(shape[dim_idx], item_scope + ".shape[" + std::to_string(dim_idx) + "]"));
205 }
206
207 if (YAML::Node description = LoadOptionalChild(entry, "description")) {
208 RequireNonEmptyString(RequireScalarText(description, item_scope + ".description"), item_scope + ".description");
209 }
210 tensors.push_back(std::move(tensor));
211 }
212 return tensors;
213}
214
215std::vector<std::array<float, 3>> LoadOptionalSizePriors(const YAML::Node& node, const std::string& field) {
216 if (!node) {
217 return {};
218 }
219 EnsureSequence(node, field);
220 std::vector<std::array<float, 3>> priors;
221 priors.reserve(node.size());
222 for (std::size_t i = 0; i < node.size(); ++i) {
223 const std::string row_field = field + "[" + std::to_string(i) + "]";
224 EnsureSequence(node[i], row_field);
225 if (node[i].size() != 3) {
226 throw std::runtime_error("Manifest field '" + row_field + "' must be a 3-element sequence");
227 }
228 priors.push_back({node[i][0].as<float>(), node[i][1].as<float>(), node[i][2].as<float>()});
229 }
230 return priors;
231}
232
233} // namespace
234
235ModelManifest LoadModelManifest(const std::string& path) {
236 try {
237 const YAML::Node root = YAML::LoadFile(path);
238 EnsureKnownKeys(root, "root", {"schema_version", "artifact", "frozen_contract", "runtime_defaults"});
239
240 const YAML::Node artifact = RequireChild(root, "artifact", "artifact");
241 EnsureKnownKeys(artifact, "artifact",
242 {"bundle_name", "export_format", "backend", "precision", "device", "head_name", "export_timestamp_utc",
243 "files", "triton", "inputs", "outputs", "size_priors", "size_priors_source"});
244
245 const YAML::Node files = RequireChild(artifact, "files", "artifact.files");
246 EnsureKnownKeys(files, "artifact.files",
247 {"model", "checkpoint", "resolved_training_config", "triton_repository", "triton_config", "triton_model"});
248
249 const YAML::Node triton = RequireChild(artifact, "triton", "artifact.triton");
250 EnsureKnownKeys(triton, "artifact.triton", {"enabled", "model_name", "model_version"});
251
252 const YAML::Node inputs = RequireChild(artifact, "inputs", "artifact.inputs");
253 const YAML::Node outputs = RequireChild(artifact, "outputs", "artifact.outputs");
254
255 const YAML::Node frozen_contract = RequireChild(root, "frozen_contract", "frozen_contract");
256 EnsureKnownKeys(frozen_contract, "frozen_contract", {"preprocessing", "postprocessing", "model"});
257
258 const YAML::Node preprocessing = RequireChild(frozen_contract, "preprocessing", "frozen_contract.preprocessing");
259 EnsureKnownKeys(preprocessing, "frozen_contract.preprocessing",
260 {"max_num_points", "num_point_features", "point_cloud_range", "voxel_size", "point_feature_normalization"});
261
262 const YAML::Node point_cloud_range =
263 RequireChild(preprocessing, "point_cloud_range", "frozen_contract.preprocessing.point_cloud_range");
264 EnsureKnownKeys(point_cloud_range, "frozen_contract.preprocessing.point_cloud_range", {"x", "y", "z"});
265
266 const YAML::Node voxel_size = RequireChild(preprocessing, "voxel_size", "frozen_contract.preprocessing.voxel_size");
267 EnsureKnownKeys(voxel_size, "frozen_contract.preprocessing.voxel_size", {"x", "y", "z"});
268
269 const YAML::Node normalization =
270 RequireChild(preprocessing, "point_feature_normalization", "frozen_contract.preprocessing.point_feature_normalization");
271 EnsureKnownKeys(normalization, "frozen_contract.preprocessing.point_feature_normalization",
272 {"type", "min_value", "max_value", "epsilon"});
273
274 const YAML::Node postprocessing = RequireChild(frozen_contract, "postprocessing", "frozen_contract.postprocessing");
275 EnsureKnownKeys(postprocessing, "frozen_contract.postprocessing", {"grid_size", "num_classes", "class_names"});
276
277 const YAML::Node grid_size = RequireChild(postprocessing, "grid_size", "frozen_contract.postprocessing.grid_size");
278 EnsureKnownKeys(grid_size, "frozen_contract.postprocessing.grid_size", {"x", "y"});
279
280 const YAML::Node model = RequireChild(frozen_contract, "model", "frozen_contract.model");
281 EnsureKnownKeys(model, "frozen_contract.model",
282 {"stride", "up_stride", "first_up_stride", "pillar_map_size", "pillar_map_range"});
283
284 const YAML::Node runtime_defaults = RequireChild(root, "runtime_defaults", "runtime_defaults");
285 EnsureKnownKeys(runtime_defaults, "runtime_defaults", {"preprocessing", "postprocessing"});
286
287 const YAML::Node runtime_preprocessing = RequireChild(runtime_defaults, "preprocessing", "runtime_defaults.preprocessing");
288 EnsureKnownKeys(runtime_preprocessing, "runtime_defaults.preprocessing", {"point_feature"});
289
290 const YAML::Node point_feature =
291 RequireChild(runtime_preprocessing, "point_feature", "runtime_defaults.preprocessing.point_feature");
292 EnsureKnownKeys(point_feature, "runtime_defaults.preprocessing.point_feature", {"value_threshold"});
293
294 const YAML::Node runtime_postprocessing = RequireChild(runtime_defaults, "postprocessing", "runtime_defaults.postprocessing");
295 EnsureKnownKeys(runtime_postprocessing, "runtime_defaults.postprocessing", {"class_score_threshold", "nms"});
296
297 const YAML::Node nms = RequireChild(runtime_postprocessing, "nms", "runtime_defaults.postprocessing.nms");
298 EnsureKnownKeys(nms, "runtime_defaults.postprocessing.nms", {"score_threshold", "iou_threshold", "max_num_objects"});
299
300 ModelManifest manifest;
301
302 manifest.schema_version = RequireScalarText(RequireChild(root, "schema_version", "schema_version"), "schema_version");
303
304 manifest.artifact.bundle_name =
305 RequireScalarText(RequireChild(artifact, "bundle_name", "artifact.bundle_name"), "artifact.bundle_name");
306 manifest.artifact.export_format =
307 RequireScalarText(RequireChild(artifact, "export_format", "artifact.export_format"), "artifact.export_format");
308 manifest.artifact.backend = RequireScalarText(RequireChild(artifact, "backend", "artifact.backend"), "artifact.backend");
309 manifest.artifact.precision =
310 RequireScalarText(RequireChild(artifact, "precision", "artifact.precision"), "artifact.precision");
311 manifest.artifact.device = RequireScalarText(RequireChild(artifact, "device", "artifact.device"), "artifact.device");
312 manifest.artifact.head_name = LoadOptionalText(LoadOptionalChild(artifact, "head_name"), "artifact.head_name");
313 manifest.artifact.export_timestamp_utc =
314 LoadOptionalText(LoadOptionalChild(artifact, "export_timestamp_utc"), "artifact.export_timestamp_utc");
315
316 manifest.artifact.files.model = LoadOptionalText(LoadOptionalChild(files, "model"), "artifact.files.model");
317 manifest.artifact.files.checkpoint =
318 RequireScalarText(RequireChild(files, "checkpoint", "artifact.files.checkpoint"), "artifact.files.checkpoint");
319 manifest.artifact.files.resolved_training_config =
320 RequireScalarText(RequireChild(files, "resolved_training_config", "artifact.files.resolved_training_config"),
321 "artifact.files.resolved_training_config");
322 manifest.artifact.files.triton_repository =
323 LoadOptionalText(LoadOptionalChild(files, "triton_repository"), "artifact.files.triton_repository");
324 manifest.artifact.files.triton_config =
325 LoadOptionalText(LoadOptionalChild(files, "triton_config"), "artifact.files.triton_config");
326 manifest.artifact.files.triton_model =
327 LoadOptionalText(LoadOptionalChild(files, "triton_model"), "artifact.files.triton_model");
328
329 manifest.artifact.triton.enabled =
330 RequireScalarValue<bool>(RequireChild(triton, "enabled", "artifact.triton.enabled"), "artifact.triton.enabled");
331 manifest.artifact.triton.model_name = LoadOptionalText(LoadOptionalChild(triton, "model_name"), "artifact.triton.model_name");
332 manifest.artifact.triton.model_version =
333 LoadOptionalText(LoadOptionalChild(triton, "model_version"), "artifact.triton.model_version");
334
335 manifest.artifact.inputs = RequireTensorList(inputs, "artifact.inputs");
336 manifest.artifact.outputs = RequireTensorList(outputs, "artifact.outputs");
337 manifest.artifact.size_priors = LoadOptionalSizePriors(LoadOptionalChild(artifact, "size_priors"), "artifact.size_priors");
338 manifest.artifact.size_priors_source =
339 LoadOptionalText(LoadOptionalChild(artifact, "size_priors_source"), "artifact.size_priors_source");
340
341 manifest.frozen_contract.preprocessing.max_num_points =
342 RequireScalarValue<int>(RequireChild(preprocessing, "max_num_points", "frozen_contract.preprocessing.max_num_points"),
343 "frozen_contract.preprocessing.max_num_points");
344 manifest.frozen_contract.preprocessing.num_point_features = RequireScalarValue<int>(
345 RequireChild(preprocessing, "num_point_features", "frozen_contract.preprocessing.num_point_features"),
346 "frozen_contract.preprocessing.num_point_features");
347 manifest.frozen_contract.preprocessing.x_range =
348 RequireRange2(RequireChild(point_cloud_range, "x", "frozen_contract.preprocessing.point_cloud_range.x"),
349 "frozen_contract.preprocessing.point_cloud_range.x");
350 manifest.frozen_contract.preprocessing.y_range =
351 RequireRange2(RequireChild(point_cloud_range, "y", "frozen_contract.preprocessing.point_cloud_range.y"),
352 "frozen_contract.preprocessing.point_cloud_range.y");
353 manifest.frozen_contract.preprocessing.z_range =
354 RequireRange2(RequireChild(point_cloud_range, "z", "frozen_contract.preprocessing.point_cloud_range.z"),
355 "frozen_contract.preprocessing.point_cloud_range.z");
356 manifest.frozen_contract.preprocessing.voxel_x =
357 RequireScalarValue<float>(RequireChild(voxel_size, "x", "frozen_contract.preprocessing.voxel_size.x"),
358 "frozen_contract.preprocessing.voxel_size.x");
359 manifest.frozen_contract.preprocessing.voxel_y =
360 RequireScalarValue<float>(RequireChild(voxel_size, "y", "frozen_contract.preprocessing.voxel_size.y"),
361 "frozen_contract.preprocessing.voxel_size.y");
362 manifest.frozen_contract.preprocessing.voxel_z =
363 RequireScalarValue<float>(RequireChild(voxel_size, "z", "frozen_contract.preprocessing.voxel_size.z"),
364 "frozen_contract.preprocessing.voxel_size.z");
365 manifest.frozen_contract.preprocessing.point_feature_normalization.type =
366 RequireScalarText(RequireChild(normalization, "type", "frozen_contract.preprocessing.point_feature_normalization.type"),
367 "frozen_contract.preprocessing.point_feature_normalization.type");
368 manifest.frozen_contract.preprocessing.point_feature_normalization.epsilon = RequireScalarValue<float>(
369 RequireChild(normalization, "epsilon", "frozen_contract.preprocessing.point_feature_normalization.epsilon"),
370 "frozen_contract.preprocessing.point_feature_normalization.epsilon");
371 if (manifest.frozen_contract.preprocessing.point_feature_normalization.type == "min_max") {
372 manifest.frozen_contract.preprocessing.point_feature_normalization.min_value = RequireScalarValue<float>(
373 RequireChild(normalization, "min_value", "frozen_contract.preprocessing.point_feature_normalization.min_value"),
374 "frozen_contract.preprocessing.point_feature_normalization.min_value");
375 manifest.frozen_contract.preprocessing.point_feature_normalization.max_value = RequireScalarValue<float>(
376 RequireChild(normalization, "max_value", "frozen_contract.preprocessing.point_feature_normalization.max_value"),
377 "frozen_contract.preprocessing.point_feature_normalization.max_value");
378 }
379
380 manifest.frozen_contract.postprocessing.grid_x = RequireScalarValue<int>(
381 RequireChild(grid_size, "x", "frozen_contract.postprocessing.grid_size.x"), "frozen_contract.postprocessing.grid_size.x");
382 manifest.frozen_contract.postprocessing.grid_y = RequireScalarValue<int>(
383 RequireChild(grid_size, "y", "frozen_contract.postprocessing.grid_size.y"), "frozen_contract.postprocessing.grid_size.y");
384 manifest.frozen_contract.postprocessing.num_classes =
385 RequireScalarValue<int>(RequireChild(postprocessing, "num_classes", "frozen_contract.postprocessing.num_classes"),
386 "frozen_contract.postprocessing.num_classes");
387 manifest.frozen_contract.postprocessing.class_names =
388 RequireStringSequence(RequireChild(postprocessing, "class_names", "frozen_contract.postprocessing.class_names"),
389 "frozen_contract.postprocessing.class_names");
390
391 manifest.frozen_contract.model.stride =
392 RequirePositiveIntSequence(RequireChild(model, "stride", "frozen_contract.model.stride"), "frozen_contract.model.stride");
393 manifest.frozen_contract.model.up_stride = RequirePositiveIntSequence(
394 RequireChild(model, "up_stride", "frozen_contract.model.up_stride"), "frozen_contract.model.up_stride");
395 manifest.frozen_contract.model.first_up_stride = RequireScalarValue<int>(
396 RequireChild(model, "first_up_stride", "frozen_contract.model.first_up_stride"), "frozen_contract.model.first_up_stride");
397
398 {
399 const YAML::Node pillar_map_size = RequireChild(model, "pillar_map_size", "frozen_contract.model.pillar_map_size");
400 EnsureSequence(pillar_map_size, "frozen_contract.model.pillar_map_size");
401 if (pillar_map_size.size() != 2) {
402 throw std::runtime_error("Manifest field 'frozen_contract.model.pillar_map_size' must be a 2-element sequence");
403 }
404 manifest.frozen_contract.model.pillar_map_size = {pillar_map_size[0].as<int>(), pillar_map_size[1].as<int>()};
405 }
406
407 {
408 const YAML::Node pillar_map_range = RequireChild(model, "pillar_map_range", "frozen_contract.model.pillar_map_range");
409 EnsureSequence(pillar_map_range, "frozen_contract.model.pillar_map_range");
410 if (pillar_map_range.size() != 3) {
411 throw std::runtime_error("Manifest field 'frozen_contract.model.pillar_map_range' must contain three ranges");
412 }
413 manifest.frozen_contract.model.pillar_map_range = {
414 RequireRange2(pillar_map_range[0], "frozen_contract.model.pillar_map_range[0]"),
415 RequireRange2(pillar_map_range[1], "frozen_contract.model.pillar_map_range[1]"),
416 RequireRange2(pillar_map_range[2], "frozen_contract.model.pillar_map_range[2]")};
417 }
418
419 manifest.runtime_defaults.preprocessing.point_feature.value_threshold =
420 LoadOptionalScalarValue<float>(LoadOptionalChild(point_feature, "value_threshold"),
421 "runtime_defaults.preprocessing.point_feature.value_threshold", 0.0F);
422 manifest.runtime_defaults.postprocessing.class_score_threshold = RequireScalarValue<float>(
423 RequireChild(runtime_postprocessing, "class_score_threshold", "runtime_defaults.postprocessing.class_score_threshold"),
424 "runtime_defaults.postprocessing.class_score_threshold");
425 manifest.runtime_defaults.postprocessing.nms_score_thresholds =
426 RequireScoreThresholds(RequireChild(nms, "score_threshold", "runtime_defaults.postprocessing.nms.score_threshold"),
427 "runtime_defaults.postprocessing.nms.score_threshold");
428 manifest.runtime_defaults.postprocessing.nms_iou_threshold =
429 RequireScalarValue<float>(RequireChild(nms, "iou_threshold", "runtime_defaults.postprocessing.nms.iou_threshold"),
430 "runtime_defaults.postprocessing.nms.iou_threshold");
431 manifest.runtime_defaults.postprocessing.max_detections =
432 RequireScalarValue<int>(RequireChild(nms, "max_num_objects", "runtime_defaults.postprocessing.nms.max_num_objects"),
433 "runtime_defaults.postprocessing.nms.max_num_objects");
434
435 ValidateModelManifest(manifest);
436 return manifest;
437 } catch (const YAML::Exception& exc) {
438 throw std::runtime_error("Failed to parse manifest '" + path + "': " + std::string(exc.what()));
439 }
440}
441
443 if (manifest.schema_version.empty()) {
444 throw std::runtime_error("Manifest schema_version is required");
445 }
447 throw std::runtime_error("Unsupported manifest schema_version: " + manifest.schema_version);
448 }
449
450 RequireNonEmptyString(manifest.artifact.bundle_name, "artifact.bundle_name");
451 RequireNonEmptyString(manifest.artifact.export_format, "artifact.export_format");
452 RequireNonEmptyString(manifest.artifact.backend, "artifact.backend");
453 RequireNonEmptyString(manifest.artifact.precision, "artifact.precision");
454 RequireNonEmptyString(manifest.artifact.device, "artifact.device");
455 if (!manifest.artifact.head_name.empty()) {
456 RequireNonEmptyString(manifest.artifact.head_name, "artifact.head_name");
457 }
458 if (!manifest.artifact.export_timestamp_utc.empty()) {
459 RequireNonEmptyString(manifest.artifact.export_timestamp_utc, "artifact.export_timestamp_utc");
460 }
461
462 if (!manifest.artifact.files.model.empty()) {
463 RequireBundleRelativePath(manifest.artifact.files.model, "artifact.files.model");
464 }
465 RequireBundleRelativePath(manifest.artifact.files.checkpoint, "artifact.files.checkpoint");
466 RequireBundleRelativePath(manifest.artifact.files.resolved_training_config, "artifact.files.resolved_training_config");
467 if (!manifest.artifact.files.triton_repository.empty()) {
468 RequireBundleRelativePath(manifest.artifact.files.triton_repository, "artifact.files.triton_repository");
469 }
470 if (!manifest.artifact.files.triton_config.empty()) {
471 RequireBundleRelativePath(manifest.artifact.files.triton_config, "artifact.files.triton_config");
472 }
473 if (!manifest.artifact.files.triton_model.empty()) {
474 RequireBundleRelativePath(manifest.artifact.files.triton_model, "artifact.files.triton_model");
475 }
476
477 if (manifest.artifact.triton.enabled) {
478 RequireNonEmptyString(manifest.artifact.triton.model_name, "artifact.triton.model_name");
479 RequireNonEmptyString(manifest.artifact.triton.model_version, "artifact.triton.model_version");
480 if (manifest.artifact.files.triton_repository.empty() || manifest.artifact.files.triton_config.empty() ||
481 manifest.artifact.files.triton_model.empty()) {
482 throw std::runtime_error("artifact.files must declare Triton repository, config, and model paths");
483 }
484 } else if (manifest.artifact.files.model.empty()) {
485 throw std::runtime_error("artifact.files.model is required for non-Triton exports");
486 }
487
488 if (manifest.artifact.inputs.empty()) {
489 throw std::runtime_error("artifact.inputs must not be empty");
490 }
491 for (std::size_t i = 0; i < manifest.artifact.inputs.size(); ++i) {
492 const auto& tensor = manifest.artifact.inputs[i];
493 if (tensor.name.empty() || tensor.dtype.empty() || tensor.shape.empty()) {
494 throw std::runtime_error("artifact.inputs[" + std::to_string(i) + "] must define name, dtype, and shape");
495 }
496 }
497 if (manifest.artifact.outputs.empty()) {
498 throw std::runtime_error("artifact.outputs must not be empty");
499 }
500 for (std::size_t i = 0; i < manifest.artifact.outputs.size(); ++i) {
501 const auto& tensor = manifest.artifact.outputs[i];
502 if (tensor.name.empty() || tensor.dtype.empty() || tensor.shape.empty()) {
503 throw std::runtime_error("artifact.outputs[" + std::to_string(i) + "] must define name, dtype, and shape");
504 }
505 }
506
507 if (manifest.frozen_contract.preprocessing.max_num_points <= 0) {
508 throw std::runtime_error("frozen_contract.preprocessing.max_num_points must be > 0");
509 }
511 throw std::runtime_error("frozen_contract.preprocessing.num_point_features must be > 0");
512 }
513 if (manifest.frozen_contract.postprocessing.num_classes <= 0) {
514 throw std::runtime_error("frozen_contract.postprocessing.num_classes must be > 0");
515 }
516 if (manifest.frozen_contract.postprocessing.class_names.size() !=
517 static_cast<std::size_t>(manifest.frozen_contract.postprocessing.num_classes)) {
518 throw std::runtime_error("frozen_contract.postprocessing.class_names must contain one entry per class");
519 }
520 if (manifest.frozen_contract.model.pillar_map_size[0] <= 0 || manifest.frozen_contract.model.pillar_map_size[1] <= 0) {
521 throw std::runtime_error("frozen_contract.model.pillar_map_size must be positive");
522 }
523 if (manifest.frozen_contract.model.stride.empty()) {
524 throw std::runtime_error("frozen_contract.model.stride must not be empty");
525 }
526 if (manifest.frozen_contract.model.up_stride.empty()) {
527 throw std::runtime_error("frozen_contract.model.up_stride must not be empty");
528 }
529 if (manifest.frozen_contract.model.first_up_stride <= 0) {
530 throw std::runtime_error("frozen_contract.model.first_up_stride must be > 0");
531 }
533 throw std::runtime_error("frozen_contract.preprocessing.point_feature_normalization.epsilon must be > 0");
534 }
535
537 if (norm.type == "value_threshold") {
539 throw std::runtime_error(
540 "runtime_defaults.preprocessing.point_feature.value_threshold must be > 0 when value_threshold normalization is used");
541 }
542 } else if (norm.type == "min_max") {
543 if (!(norm.min_value < norm.max_value)) {
544 throw std::runtime_error("frozen_contract.preprocessing.point_feature_normalization requires min_value < max_value");
545 }
546 } else if (norm.type == "z_score") {
547 // ok
548 } else if (norm.type != "none") {
549 throw std::runtime_error("frozen_contract.preprocessing.point_feature_normalization.type is invalid");
550 }
551
554 throw std::runtime_error("runtime_defaults.postprocessing.class_score_threshold must be within [0.0, 1.0]");
555 }
558 throw std::runtime_error("runtime_defaults.postprocessing.nms.iou_threshold must be within [0.0, 1.0]");
559 }
561 throw std::runtime_error("runtime_defaults.postprocessing.nms.max_num_objects must be zero or positive");
562 }
564 throw std::runtime_error("runtime_defaults.postprocessing.nms.score_threshold must not be empty");
565 }
566 const std::size_t score_count = manifest.runtime_defaults.postprocessing.nms_score_thresholds.size();
567 if (score_count != 1 && score_count != static_cast<std::size_t>(manifest.frozen_contract.postprocessing.num_classes)) {
568 throw std::runtime_error("runtime_defaults.postprocessing.nms.score_threshold must contain one value or one value per class");
569 }
570 for (float threshold : manifest.runtime_defaults.postprocessing.nms_score_thresholds) {
571 if (threshold < 0.0F || threshold > 1.0F) {
572 throw std::runtime_error("runtime_defaults.postprocessing.nms.score_threshold entries must be within [0.0, 1.0]");
573 }
574 }
575}
576
577} // namespace pcod_common
constexpr const char * kManifestSchemaVersion
Definition version.hpp:10
void ValidateModelManifest(const ModelManifest &manifest)
ModelManifest LoadModelManifest(const std::string &path)
std::string triton_config
Triton model configuration path.
std::string triton_model
Triton model artifact path.
std::string model
Exported model path.
std::string checkpoint
Source checkpoint path.
std::string resolved_training_config
Resolved training configuration path.
std::string triton_repository
Triton repository path.
std::string model_version
Triton model version.
bool enabled
Whether Triton artifacts were exported.
std::string export_timestamp_utc
UTC export timestamp.
std::string head_name
Detection head identifier.
std::vector< Tensor > outputs
Ordered output tensor contracts.
std::string precision
Numeric precision.
std::string bundle_name
Human-readable bundle name.
std::string device
Export target device.
TritonDeployment triton
Triton deployment metadata.
std::string export_format
Model serialization format.
std::string backend
Runtime backend.
Files files
Bundle-relative files.
std::vector< Tensor > inputs
Ordered input tensor contracts.
FrozenModelConfig model
Model architecture contract.
FrozenPreprocessConfig preprocessing
Preprocessing contract.
FrozenPostprocessConfig postprocessing
Postprocessing contract.
std::array< int, 2 > pillar_map_size
Base pillar-map dimensions.
int first_up_stride
First decoder upsampling factor.
std::vector< int > stride
Backbone strides.
std::vector< int > up_stride
Decoder upsampling strides.
std::vector< std::string > class_names
Class names in output order.
int num_classes
Number of semantic classes.
PointFeatureNormalizationContract point_feature_normalization
Scalar feature contract.
int num_point_features
Features expected per point.
int max_num_points
Maximum input point count.
RuntimeDefaults runtime_defaults
Overridable runtime defaults.
std::string schema_version
Manifest schema version.
FrozenContract frozen_contract
Non-overridable inference contract.
ArtifactConfig artifact
Artifact metadata.
int max_detections
Maximum final detection count.
float nms_iou_threshold
Rotated NMS IoU threshold.
std::vector< float > nms_score_thresholds
NMS class score thresholds.
float class_score_threshold
Decoder confidence threshold.
float value_threshold
Value-threshold normalization divisor.
PointFeature point_feature
Additional point-feature defaults.
Postprocessing postprocessing
Postprocessing defaults.
Preprocessing preprocessing
Preprocessing defaults.