# Copyright (C) 2018-2025 Intel Corporation # SPDX-License-Identifier: Apache-2.0 from collections import defaultdict from datetime import timedelta import enum from openvino import Core, Model, PartialShape, Dimension, Layout, Type, serialize, properties, OVAny from openvino.preprocess import PrePostProcessor from .constants import DEVICE_DURATION_IN_SECS, UNKNOWN_DEVICE_TYPE, \ AUTO_DEVICE_NAME, MULTI_DEVICE_NAME, HETERO_DEVICE_NAME from .logging import logger import json import re import numpy as np def static_vars(**kwargs): def decorate(func): for k in kwargs: setattr(func, k, kwargs[k]) return func return decorate @static_vars(step_id=0) def next_step(additional_info='', step_id=0): step_names = { 1: "Parsing and validating input arguments", 2: "Loading OpenVINO Runtime", 3: "Setting device configuration", 4: "Reading model files", 5: "Resizing model to match image sizes and given batch", 6: "Configuring input of the model", 7: "Loading the model to the device", 8: "Querying optimal runtime parameters", 9: "Creating infer requests and preparing input tensors", 10: "Measuring performance", 11: "Dumping statistics report", } if step_id != 0: next_step.step_id = step_id else: next_step.step_id += 1 if next_step.step_id not in step_names.keys(): raise Exception(f'Step ID {next_step.step_id} is out of total steps number {str(len(step_names))}') step_info_template = '[Step {}/{}] {}' step_name = step_names[next_step.step_id] + (f' ({additional_info})' if additional_info else '') step_info_template = step_info_template.format(next_step.step_id, len(step_names), step_name) print(step_info_template) def get_element_type(precision): format_map = { 'bool' : Type.boolean, 'f16' : Type.f16, 'f32' : Type.f32, 'f64' : Type.f64, 'i8' : Type.i8, 'i16' : Type.i16, 'i32' : Type.i32, 'i64' : Type.i64, 'u8' : Type.u8, 'u16' : Type.u16, 'u32' : Type.u32, 'u64' : Type.u64, } if precision in format_map.keys(): return format_map[precision] raise Exception(f"Undefined precision: '{precision}' !") def fuse_mean_scale(preproc: PrePostProcessor, app_inputs_info): # TODO: remove warning after 23.3 release warned = False warn_msg = 'Mean/scale values are fused into the model. This slows down performance compared to --imean and --iscale which existed before' for input_info in app_inputs_info: if input_info.mean.size: if not warned: logger.warning(warn_msg) warned = True preproc.input(input_info.name).preprocess().convert_element_type(Type.f32).mean(input_info.mean) if input_info.scale.size: if not warned: logger.warning(warn_msg) warned = True preproc.input(input_info.name).preprocess().convert_element_type(Type.f32).scale(input_info.scale) def pre_post_processing(model: Model, app_inputs_info, input_precision: str, output_precision: str, input_output_precision: str): pre_post_processor = PrePostProcessor(model) if input_precision: element_type = get_element_type(input_precision) for i in range(len(model.inputs)): pre_post_processor.input(i).tensor().set_element_type(element_type) app_inputs_info[i].element_type = element_type if output_precision: element_type = get_element_type(output_precision) for i in range(len(model.outputs)): pre_post_processor.output(i).tensor().set_element_type(element_type) user_precision_map = {} if input_output_precision: user_precision_map = parse_input_output_precision(input_output_precision) input_names = get_input_output_names(model.inputs) input_node_names = get_node_names(model.inputs) output_names = get_input_output_names(model.outputs) output_node_names = get_node_names(model.outputs) for node_name, precision in user_precision_map.items(): user_precision_map[node_name] = get_element_type(precision) for name, element_type in user_precision_map.items(): if name in input_names or name in input_node_names: input_index = input_names.index(name) if name in input_names else input_node_names.index(name) app_inputs_info[input_index].element_type = element_type pre_post_processor.input(input_index).tensor().set_element_type(element_type) elif name in output_names or name in output_node_names: if name in output_names: pre_post_processor.output(name).tensor().set_element_type(element_type) else: pre_post_processor.output(output_node_names.index(name)).tensor().set_element_type(element_type) else: raise Exception(f"Node '{name}' does not exist in model") # update app_inputs_info if not input_precision: inputs = model.inputs input_node_names = get_node_names(model.inputs) for i in range(len(inputs)): if app_inputs_info[i].name in user_precision_map: app_inputs_info[i].element_type = user_precision_map[app_inputs_info[i].name] elif input_node_names[i] in user_precision_map: app_inputs_info[i].element_type = user_precision_map[input_node_names[i]] elif app_inputs_info[i].is_image: app_inputs_info[i].element_type = Type.u8 pre_post_processor.input(i).tensor().set_element_type(Type.u8) fuse_mean_scale(pre_post_processor, app_inputs_info) # set layout for model input for info in app_inputs_info: pre_post_processor.input(info.name).model().set_layout(info.layout) model = pre_post_processor.build() def parse_input_output_precision(arg_map: str): arg_map = arg_map.replace(" ", "") pairs = [x.strip() for x in arg_map.split(',')] parsed_map = {} for pair in pairs: key_value = [x.strip() for x in pair.split(':')] name, precision = key_value[0], key_value[1] # input's name can contain ':' if len(key_value) == 3: name = key_value[0] + ':' + key_value[1] precision = key_value[2] parsed_map.update({name:precision}) return parsed_map def print_inputs_and_outputs_info(model: Model): inputs = model.inputs logger.info("Model inputs:") for input in inputs: in_name = " , ".join(input.get_names()) node_name = input.node.get_friendly_name() if in_name=="": in_name = "***NO_NAME***" if node_name=="": node_name = "***NO_NAME***" logger.info(f" {in_name} (node: {node_name}) : {input.element_type.get_type_name()} / " f"{str(input.node.layout)} / {input.partial_shape}") outputs = model.outputs logger.info("Model outputs:") for output in outputs: out_name = " , ".join(output.get_names()) node_name = output.get_node().input(0).get_source_output().get_node().get_friendly_name() if out_name=="": out_name = "***NO_NAME***" if node_name=="": node_name = "***NO_NAME***" logger.info(f" {out_name} (node: {node_name}) : {output.element_type.get_type_name()} / " f"{str(output.node.layout)} / {output.partial_shape}") def get_number_iterations(number_iterations: int, nireq: int, num_shapes: int, api_type: str): niter = number_iterations if api_type == 'async' and niter: if num_shapes > nireq: niter = int(((niter + num_shapes -1) / num_shapes) * num_shapes) if number_iterations != niter: logger.warning('Number of iterations was aligned by number of input shapes ' f'from {number_iterations} to {niter} using number of possible input shapes {num_shapes}') else: niter = int((niter + nireq - 1) / nireq) * nireq if number_iterations != niter: logger.warning('Number of iterations was aligned by request number ' f'from {number_iterations} to {niter} using number of requests {nireq}') return niter def get_duration_seconds(time, number_iterations, device): if time: # time limit return time if not number_iterations: return get_duration_in_secs(device) return 0 class LatencyGroup: def __init__(self, input_names, input_shapes): self.input_names = input_names self.input_shapes = input_shapes self.times = list() self.median = 0. self.avg = 0. self.min = 0. self.max = 0. def __str__(self): return str().join(f" {name}: {str(shape)}" for name, shape in zip(self.input_names, self.input_shapes)) def get_latency_groups(app_input_info): num_groups = max(len(info.shapes) for info in app_input_info) latency_groups = [] for i in range(num_groups): names = list() shapes = list() for info in app_input_info: names.append(info.name) shapes.append(info.shapes[i % len(info.shapes)]) latency_groups.append(LatencyGroup(names, shapes)) return latency_groups def get_duration_in_milliseconds(duration): return duration * 1000 def get_duration_in_secs(target_device): duration = 0 for device in DEVICE_DURATION_IN_SECS: if device in target_device: duration = max(duration, DEVICE_DURATION_IN_SECS[device]) if duration == 0: duration = DEVICE_DURATION_IN_SECS[UNKNOWN_DEVICE_TYPE] logger.warning(f'Default duration {duration} seconds is used for unknown device {target_device}') return duration def check_for_static(app_input_info): for info in app_input_info: if info.is_dynamic: return False return True def can_measure_as_static(app_input_info): for info in app_input_info: if len(info.shapes) > 1: return False return True meta_plugins = [ MULTI_DEVICE_NAME, HETERO_DEVICE_NAME, AUTO_DEVICE_NAME ] def is_virtual_device(device_name) -> bool: return device_name in meta_plugins def is_virtual_device_found(device_names) -> bool: return any(is_virtual_device(device_name) for device_name in device_names) def parse_devices(device_string): result = [] target_device = device_string.partition(":")[0] result.append(target_device) if device_string.find(":") != -1: hw_devices_str = device_string.partition(":")[-1] for hw_device in hw_devices_str.split(','): if hw_device[0] == '-': hw_device = hw_device[1:] result.append(hw_device) return result def parse_value_per_device(devices, values_string, value_type): # Format: :,: or just result = {} if not values_string: return result device_value_strings = values_string.split(',') for device_value_string in device_value_strings: device_value_vec = device_value_string.split(':') if len(device_value_vec) == 2: device_name = device_value_vec[0] value = device_value_vec[1] if device_name in devices: result[device_name] = value else: devices_str = "" for device in devices: devices_str += device + " " devices_str = devices_str.strip() raise Exception(f"Failed to set property to '{device_name}' " \ f"which is not found in the target devices list '{devices_str}'!") elif len(device_value_vec) == 1: value = device_value_vec[0] for device in devices: result[device] = value elif not device_value_vec: raise Exception('Unknown string format: ' + values_string) return result def parse_value_for_virtual_device(device, values_string): isExist = device in values_string.keys() if isExist and len(values_string) > 1: if device == MULTI_DEVICE_NAME: # Remove the element that the key is virtual device MULTI # e.g. MULTI:xxx -nstreams 2 will set nstreams 2 to xxx. values_string.pop(device) elif device == AUTO_DEVICE_NAME or device == HETERO_DEVICE_NAME: # Just keep the element that the key is virtual device AUTO/HETERO # e.g. AUTO:xxx,xxx -nstreams 2 will trigger exception that AUTO plugin didn't support nstream property. value = values_string.get(device) values_string.clear() values_string[device] = value keys = values_string.keys() for key in list(values_string): if device not in list(values_string): values_string[device] = '{' else: values_string[device] += ',' values_string[device] += key + ":" + values_string.get(key) del values_string[key] if device in values_string.keys(): values_string[device] = values_string[device].strip() if values_string[device] != '': values_string[device] += '}' return def process_help_inference_string(benchmark_app, device_number_streams): output_string = f'Start inference {benchmark_app.api_type}hronously' if benchmark_app.api_type == 'async': output_string += f', {benchmark_app.nireq} inference requests' device_ss = '' for device, streams in device_number_streams.items(): device_ss += ', ' if device_ss else '' device_ss += f'{streams} streams for {device}' if device_ss: output_string += ' using ' + device_ss output_string += ', limits: ' if benchmark_app.duration_seconds: output_string += f'{get_duration_in_milliseconds(benchmark_app.duration_seconds)} ms duration' if benchmark_app.niter: if benchmark_app.duration_seconds > 0: output_string += ', ' output_string += f'{benchmark_app.niter} iterations' return output_string def dump_exec_graph(compiled_model, model_path): serialize(compiled_model.get_runtime_model(), model_path) def print_perf_counters_sort(perf_counts_list,sort_flag="sort"): """ Print opts time cost and can be sorted according by each opts time cost """ for ni in range(len(perf_counts_list)): perf_counts = perf_counts_list[ni] total_time = timedelta() total_time_cpu = timedelta() logger.info(f"Performance counts sorted for {ni}-th infer request") for pi in perf_counts: total_time += pi.real_time total_time_cpu += pi.cpu_time total_time = total_time.microseconds total_time_cpu = total_time_cpu.microseconds total_real_time_proportion = 0 total_detail_data=[] for pi in perf_counts: node_name = pi.node_name layerStatus = pi.status layerType = pi.node_type real_time = pi.real_time.microseconds cpu_time = pi.cpu_time.microseconds real_proportion = round(real_time/total_time,4) execType = pi.exec_type tmp_data=[node_name,layerStatus,layerType,real_time,cpu_time,real_proportion,execType] total_detail_data.append(tmp_data) total_real_time_proportion += real_proportion total_detail_data = np.array(total_detail_data) if sort_flag=="sort": total_detail_data = sorted(total_detail_data,key=lambda tmp_data:tmp_data[-4],reverse=True) elif sort_flag=="no_sort": total_detail_data = total_detail_data elif sort_flag=="simple_sort": total_detail_data = sorted(total_detail_data,key=lambda tmp_data:tmp_data[-4],reverse=True) total_detail_data = [tmp_data for tmp_data in total_detail_data if str(tmp_data[1])!="Status.NOT_RUN"] print_detail_result(total_detail_data) print(f'Total time: {total_time / 1000:.3f} milliseconds') print(f'Total CPU time: {total_time_cpu / 1000:.3f} milliseconds') print(f'Total proportion: {"%.2f"%(round(total_real_time_proportion)*100)} % \n') return total_detail_data def print_detail_result(result_list): """ Print_perf_counters_sort result """ max_print_length = 20 for tmp_result in result_list: node_name = tmp_result[0] layerStatus = tmp_result[1] layerType = tmp_result[2] real_time = tmp_result[3] cpu_time = tmp_result[4] real_proportion = "%.2f" % (tmp_result[5] * 100) if real_proportion == "0.00": real_proportion = "N/A" execType = tmp_result[6] print(f"{node_name[:max_print_length - 4] + '...' if (len(node_name) >= max_print_length) else node_name:<20} " f"{str(layerStatus):<20} " f"layerType: {layerType[:max_print_length - 4] + '...' if (len(layerType) >= max_print_length) else layerType:<20} " f"execType: {execType:<20} " f"realTime (ms): {real_time / 1000:<10.3f} " f"cpuTime (ms): {cpu_time / 1000:<10.3f}" f"proportion: {str(real_proportion +'%'):<8}") def print_perf_counters(perf_counts_list): max_print_length = 20 for ni in range(len(perf_counts_list)): perf_counts = perf_counts_list[ni] total_time = timedelta() total_time_cpu = timedelta() logger.info(f"Performance counts for {ni}-th infer request") for pi in perf_counts: print(f"{pi.node_name[:max_print_length - 4] + '...' if (len(pi.node_name) >= max_print_length) else pi.node_name:<20} " f"{str(pi.status):<20} " f"layerType: {pi.node_type[:max_print_length - 4] + '...' if (len(pi.node_type) >= max_print_length) else pi.node_type:<20} " f"execType: {pi.exec_type:<20} " f"realTime (ms): {pi.real_time / timedelta(milliseconds=1):<10.3f} " f"cpuTime (ms): {pi.cpu_time / timedelta(milliseconds=1):<10.3f}") total_time += pi.real_time total_time_cpu += pi.cpu_time print(f'Total time: {total_time / timedelta(milliseconds=1)} milliseconds') print(f'Total CPU time: {total_time_cpu / timedelta(milliseconds=1)} milliseconds\n') def get_command_line_arguments(argv): parameters = [] arg_name = '' arg_value = '' for arg in argv[1:]: if '=' in arg: if arg_name != '': parameters.append((arg_name, arg_value)) arg_name, arg_value = arg.split('=') parameters.append((arg_name, arg_value)) arg_name = '' arg_value = '' else: if arg[0] == '-': if arg_name != '': parameters.append((arg_name, arg_value)) arg_value = '' arg_name = arg else: arg_value = arg if arg_name != '': parameters.append((arg_name, arg_value)) return parameters def get_input_output_names(ports): return [port.any_name if port.get_names() else port.node.get_friendly_name() for port in ports] def get_node_names(ports): return [port.node.friendly_name for port in ports] def get_data_shapes_map(data_shape_string, input_names): # Parse parameter string like "input0[shape1][shape2],input1[shape1]" or "[shape1][shape2]" (applied to all inputs) return_value = {} if data_shape_string: data_shape_string += ',' matches = re.findall(r'(.*?\[.*?\]),', data_shape_string) if matches: for match in matches: input_name = match[:match.find('[')] shapes = re.findall(r'\[(.*?)\]', match[len(input_name):]) if input_name: return_value[input_name] = list(PartialShape(shape_str) for shape_str in shapes) else: data_shapes = list(PartialShape(shape_str) for shape_str in shapes) num_inputs, num_shapes = len(input_names), len(data_shapes) if num_shapes != 1 and num_shapes % num_inputs != 0: raise Exception(f"Number of provided data_shapes is not a multiple of the number of model inputs!") return_value = defaultdict(list) for i in range(max(num_shapes, num_inputs)): return_value[input_names[i % num_inputs]].append(data_shapes[i % num_shapes]) return return_value else: raise Exception(f"Can't parse input parameter: {data_shape_string}") return return_value def parse_input_parameters(parameter_string, input_names): # Parse parameter string like "input0[value0],input1[value1]" or "[value]" (applied to all inputs) return_value = {} if parameter_string: matches = re.findall(r'(.*?)\[(.*?)\],?', parameter_string) if matches: for match in matches: input_name, value = match if input_name != '': return_value[input_name] = value else: return_value = { k:value for k in input_names } break else: raise Exception(f"Can't parse input parameter: {parameter_string}") return return_value def parse_scale_or_mean(parameter_string, input_info): # Parse parameter string like "input0[value0],input1[value1]" or "[value]" (applied to all inputs) return_value = {} if parameter_string: matches = re.findall(r'(.*?)\[(.*?)\],?', parameter_string) if matches: for match in matches: input_name, value = match f_value = np.array(value.split(",")).astype(float) if input_name != '': return_value[input_name] = f_value else: for input in input_info: if input.is_image: return_value[input.name] = f_value else: raise Exception(f"Can't parse input parameter: {parameter_string}") return return_value class AppInputInfo: def __init__(self): self.element_type = None self.layout = Layout() self.partial_shape = None self.data_shapes = [] self.scale = np.empty([0]) self.mean = np.empty([0]) self.name = None self.node_name = None @property def is_image(self): if str(self.layout) not in [ "[N,C,H,W]", "[N,H,W,C]", "[C,H,W]", "[H,W,C]" ]: return False return self.channels == 3 @property def is_image_info(self): if str(self.layout) != "[N,C]": return False return len(self.channels) >= 2 if self.channels.is_static else self.channels.relaxes(Dimension(2)) def getDimensionByLayout(self, character): if self.layout.has_name(character): return self.partial_shape[self.layout.get_index_by_name(character)] else: return Dimension(0) def getDimensionsByLayout(self, character): if self.layout.has_name(character): d_index = self.layout.get_index_by_name(character) dims = [] for shape in self.data_shapes: dims.append(shape[d_index]) return dims else: return [0] * len(self.data_shapes) @property def shapes(self): if self.is_static: return [self.partial_shape.to_shape()] else: return self.data_shapes @property def width(self): return len(self.getDimensionByLayout("W")) @property def widths(self): return self.getDimensionsByLayout("W") @property def height(self): return len(self.getDimensionByLayout("H")) @property def heights(self): return self.getDimensionsByLayout("H") @property def channels(self): return self.getDimensionByLayout("C") @property def is_static(self): return self.partial_shape.is_static @property def is_dynamic(self): return self.partial_shape.is_dynamic def get_inputs_info(shape_string, data_shape_string, layout_string, batch_size, scale_string, mean_string, inputs): input_names = get_input_output_names(inputs) input_node_names = get_node_names(inputs) shape_map = parse_input_parameters(shape_string, input_names) data_shape_map = get_data_shapes_map(data_shape_string, input_names) layout_map = parse_input_parameters(layout_string, input_names) batch_size = Dimension(batch_size) reshape = False batch_found = False input_info = [] for i in range(len(inputs)): info = AppInputInfo() # Input name info.name = input_names[i] # Input node name info.node_name = input_node_names[i] # Input precision info.element_type = inputs[i].element_type # Shape if info.name in shape_map: info.partial_shape = PartialShape(shape_map[info.name]) reshape = True elif info.node_name in shape_map: info.partial_shape = PartialShape(shape_map[info.node_name]) reshape = True else: info.partial_shape = inputs[i].partial_shape # Layout if info.name in layout_map: info.layout = Layout(layout_map[info.name]) elif info.node_name in layout_map: info.layout = Layout(layout_map[info.node_name]) elif inputs[i].node.layout != Layout(): info.layout = inputs[i].node.layout else: image_colors_dim_max = 4 shape = info.partial_shape num_dims = len(shape) if num_dims == 4: if shape[1].get_max_length() <= image_colors_dim_max and shape[3].get_max_length() > image_colors_dim_max: info.layout = Layout("NCHW") elif shape[3].get_max_length() <= image_colors_dim_max and shape[1].get_max_length() > image_colors_dim_max: info.layout = Layout("NHWC") elif num_dims == 3: if shape[0].get_max_length() <= image_colors_dim_max and shape[2].get_max_length() > image_colors_dim_max: info.layout = Layout("CHW") elif shape[2].get_max_length() <= image_colors_dim_max and shape[0].get_max_length() > image_colors_dim_max: info.layout = Layout("HWC") # Update shape with batch if needed if batch_size != 0: if batch_size.is_static and data_shape_map: logger.warning(f"Batch size will be ignored. Provide batch deminsion in data_shape parameter.") else: batch_index = -1 if info.layout.has_name('N'): batch_index = info.layout.get_index_by_name('N') elif info.layout == Layout(): supposed_batch = info.partial_shape[0] if supposed_batch.is_dynamic or supposed_batch in [0, 1]: logger.warning(f"Batch dimension is not specified for input '{info.name}'. " "The first dimension will be interpreted as batch size.") batch_index = 0 info.layout = Layout("N...") if batch_index != -1 and info.partial_shape[batch_index] != batch_size: info.partial_shape[batch_index] = batch_size reshape = True batch_found = True elif batch_index == -1 and not batch_found and i == len(inputs) - 1: raise RuntimeError("-b option is provided in command line, but there's no inputs with batch(B) " \ "dimension in input layout, so batch cannot be set. " \ "You may specify layout explicitly using -layout option.") # Data shape if (info.name in data_shape_map or info.node_name in data_shape_map) and info.is_dynamic: used_name = info.name if info.name in data_shape_map else info.node_name for p_shape in data_shape_map[used_name]: if p_shape.is_dynamic: raise Exception(f"Data shape always should be static, {str(p_shape)} is dynamic.") elif info.partial_shape.compatible(p_shape): info.data_shapes.append(p_shape.to_shape()) else: raise Exception(f"Data shape '{str(p_shape)}' provided for input '{info.name}' " f"is not compatible with partial shape '{str(info.partial_shape)}' for this input.") elif info.name in data_shape_map or input_node_names[i] in data_shape_map: logger.warning(f"Input '{info.name}' has static shape. Provided data shapes for this input will be ignored.") input_info.append(info) # Update scale and mean scale_map = parse_scale_or_mean(scale_string, input_info) mean_map = parse_scale_or_mean(mean_string, input_info) for input in input_info: if input.name in scale_map: input.scale = scale_map[input.name] elif input.node_name in scale_map: input.scale = scale_map[input.node_name] if input.name in mean_map: input.mean = mean_map[input.name] elif input.node_name in mean_map: input.mean = mean_map[input.node_name] return input_info, reshape def get_network_batch_size(inputs_info): null_dimension = Dimension(0) batch_size = null_dimension for info in inputs_info: batch_index = info.layout.get_index_by_name('N') if info.layout.has_name('N') else -1 if batch_index != -1: if batch_size == null_dimension: batch_size = info.partial_shape[batch_index] elif batch_size != info.partial_shape[batch_index]: raise Exception("Can't deterimine batch size: batch is different for different inputs!") if batch_size == null_dimension: batch_size = Dimension(1) return batch_size def show_available_devices(): print("\nAvailable target devices: ", (" ".join(Core().available_devices))) def device_properties_to_string(config): ret = "{" for k, v in config.items(): if isinstance(v, dict): sub_str = "{" for sk, sv in v.items(): if isinstance(sv, bool): sv = "YES" if sv else "NO" sub_str += "{0}:{1},".format(sk, sv) sub_str = sub_str[:-1] sub_str += "}" ret += "{0}:{1},".format(k, sub_str) else: ret += "{0}:{1},".format(k, v) ret = ret[:-1] ret += "}" return ret def string_to_device_properties(device_properties_str): ret = {} if not device_properties_str: return ret if not device_properties_str.startswith("{") or not device_properties_str.endswith("}"): raise Exception( "Failed to parse device properties. Value of device properties should be started with '{' and ended with '}'." "They are actually {} and {}".format(device_properties_str[0], device_properties_str[-1])) pattern = r'(\w+):({.+?}|[^,}]+)' pairs = re.findall(pattern, device_properties_str) for key, value in pairs: if value.startswith("{") and value.endswith("}"): value = value[1:-1] nested_pairs = re.findall(pattern, value) nested_dict = {} for nested_key, nested_value in nested_pairs: nested_dict[nested_key] = nested_value value = nested_dict ret[key] = value return ret def dump_config(filename, config): json_config = {} for device_name, device_config in config.items(): json_config[device_name] = {} for key, value in device_config.items(): if isinstance(value, OVAny) and (isinstance(value.value, dict)): value_string = device_properties_to_string(value.get()) elif isinstance(value, properties.hint.PerformanceMode): value_string = value.name elif isinstance(value, OVAny): value_string = str(value.value) else: value_string = str(value) if isinstance(value, bool): value_string = "YES" if value else "NO" json_config[device_name][key] = value_string with open(filename, 'w') as f: json.dump(json_config, f, indent=4) def load_config(filename, config): with open(filename) as f: original_config = json.load(f) for device in original_config: config[device] = {} for property_name in original_config[device]: property_value = original_config[device][property_name] if property_name == properties.device.properties(): property_value = string_to_device_properties(property_value) elif property_value in ("YES", "NO"): property_value = True if property_value == "YES" else False config[device][property_name] = OVAny(property_value)