From c2a6cf8e06abff52d6ac04b09c71b7fa5a65819b Mon Sep 17 00:00:00 2001 From: M Platypus Date: Thu, 26 Feb 2026 07:12:34 -0500 Subject: [PATCH 01/10] Replace print() with logging module across tinyml-modelmaker Co-Authored-By: Claude Opus 4.6 --- .../common/datasets/dataset_utils.py | 5 ++- .../ai_modules/timeseries/runner.py | 37 ++++++++++--------- .../timeseries/training/__init__.py | 7 +++- .../ai_modules/vision/runner.py | 35 ++++++++++-------- .../ai_modules/vision/training/__init__.py | 7 +++- .../run_tinyml_modelmaker.py | 13 ++++--- .../tinyml_modelmaker/utils/download_utils.py | 15 +++++--- .../tinyml_modelmaker/utils/misc_utils.py | 5 ++- 8 files changed, 74 insertions(+), 50 deletions(-) diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/common/datasets/dataset_utils.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/common/datasets/dataset_utils.py index cdb3635b..b5c10e69 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/common/datasets/dataset_utils.py +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/common/datasets/dataset_utils.py @@ -33,6 +33,7 @@ import copy import glob import json +import logging import os import random import re @@ -52,6 +53,8 @@ from .... import utils +logger = logging.getLogger(__name__) + def create_filelist(input_data_path: str, output_dir: str, ignore_str_list=None) -> str: ''' @@ -492,7 +495,7 @@ def dataset_split(dataset, split_factor, split_names, random_seed=1): dataset_splits[split_name]['annotations'].extend(annotations) image_count_split[split_name] += 1 # - print('dataset split sizes', image_count_split) + logger.info('dataset split sizes %s', image_count_split) return dataset_splits diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/runner.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/runner.py index 3eba2149..26900837 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/runner.py +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/runner.py @@ -30,6 +30,7 @@ import copy import datetime +import logging import os from zipfile import ZipFile @@ -41,6 +42,7 @@ from .params import init_params from tinyml_torchmodelopt.quantization import TinyMLQuantizationVersion +logger = logging.getLogger(__name__) class ModelRunner(): @@ -55,9 +57,10 @@ def init_params(self, *args, **kwargs): def __init__(self, *args, verbose=True, **kwargs): self.params = self.init_params(*args, **kwargs) - # print the runner params + # log the runner params if verbose: - [print(key, ':', value) for key, value in vars(self.params).items()] + for key, value in vars(self.params).items(): + logger.info('%s : %s', key, value) # # normalize the paths if not self.params.dataset.dataset_name: @@ -111,14 +114,14 @@ def __init__(self, *args, verbose=True, **kwargs): inference_time_us_list = {k:v.get('inference_time_us') for k,v in self.params.training.target_devices.items()} sram_usage_list = {k: v.get('sram') for k, v in self.params.training.target_devices.items()} flash_usage_list = {k: v.get('flash') for k, v in self.params.training.target_devices.items()} - print('---------------------------------------------------------------------') - print(f'Run Name: {self.params.common.run_name}') - print(f'- Model: {self.params.training.model_name}') - print(f'- TargetDevices & Estimated Inference Times (us): {inference_time_us_list}') - print(f'- TargetDevices & Estimated SRAM Usage (bytes): {sram_usage_list}') - print(f'- TargetDevices & Estimated Flash Usage (bytes): {flash_usage_list}') - print('- This model can be compiled for the above device(s).') - print('---------------------------------------------------------------------') + logger.info('---------------------------------------------------------------------') + logger.info(f'Run Name: {self.params.common.run_name}') + logger.info(f'- Model: {self.params.training.model_name}') + logger.info(f'- TargetDevices & Estimated Inference Times (us): {inference_time_us_list}') + logger.info(f'- TargetDevices & Estimated SRAM Usage (bytes): {sram_usage_list}') + logger.info(f'- TargetDevices & Estimated Flash Usage (bytes): {flash_usage_list}') + logger.info('- This model can be compiled for the above device(s).') + logger.info('---------------------------------------------------------------------') # ##################################################################### @@ -127,9 +130,9 @@ def __init__(self, *args, verbose=True, **kwargs): auto_data_dir = constants.get_default_data_dir_for_task(self.params.common.task_category) self.params.dataset.data_dir = auto_data_dir if verbose: - print(f"Auto-detected data_dir='{auto_data_dir}' for task_category='{self.params.common.task_category}'") + logger.info(f"Auto-detected data_dir='{auto_data_dir}' for task_category='{self.params.common.task_category}'") elif verbose: - print(f"Using user-specified data_dir='{self.params.dataset.data_dir}'") + logger.info(f"Using user-specified data_dir='{self.params.dataset.data_dir}'") # def resolve_run_name(self, run_name, model_name): @@ -234,9 +237,9 @@ def run(self): self.package_trained_model(model_training_package_files, self.params.training.model_packaged_path) if not utils.misc_utils.str2bool(self.params.testing.skip_train): if self.params.training.training_path_quantization: - print(f'\nTrained model is at: {self.params.training.training_path_quantization}\n') + logger.info(f'Trained model is at: {self.params.training.training_path_quantization}') else: - print(f'\nTrained model is at: {self.params.training.training_path}\n') + logger.info(f'Trained model is at: {self.params.training.training_path}') # we are done with training with open(self.params.training.log_file_path, 'a') as lfp: lfp.write('\nSUCCESS: ModelMaker - Training completed.') @@ -252,7 +255,7 @@ def run(self): self.model_compilation.clear() exit_flag = self.model_compilation.run() if exit_flag: - print(f'Compilation failed') + logger.error('Compilation failed') with open(self.params.compilation.log_file_path, 'a') as lfp: lfp.write('FAILURE: ModelMaker - Compilation failed.') return self.params @@ -278,7 +281,7 @@ def run(self): self.package_trained_model(model_compilation_package_files, self.params.compilation.model_packaged_path) - print(f'Compiled model is at: {self.params.compilation.compilation_path}') + logger.info(f'Compiled model is at: {self.params.compilation.compilation_path}') with open(self.params.compilation.log_file_path, 'a') as lfp: lfp.write('\nSUCCESS: ModelMaker - Compilation completed.') if self.params.testing.device_inference: @@ -287,7 +290,7 @@ def run(self): run_params_file = os.path.join(self.params.common.project_run_path, 'run.yaml') test_golden_vector(run_params_file, True) except ImportError as e: - print(f"Device Inference cannot be done due to an exception: {e}") + logger.error(f"Device Inference cannot be done due to an exception: {e}") return self.params diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/training/__init__.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/training/__init__.py index d28f4410..243edca1 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/training/__init__.py +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/training/__init__.py @@ -29,10 +29,13 @@ ################################################################################# import copy +import logging import sys from .. import constants +logger = logging.getLogger(__name__) + # list all the modules here to add pretrained models _model_descriptions = {} _training_module_descriptions = {} @@ -81,13 +84,13 @@ def get_target_module(backend_name, task_category): try: backend_package = getattr(this_module, backend_name) except Exception as e: - print(f"get_target_module(): The requested module could not be found: {backend_name}. {str(e)}") + logger.error(f"get_target_module(): The requested module could not be found: {backend_name}. {str(e)}") return None # try: target_module = getattr(backend_package, task_category) except Exception as e: - print(f"get_target_module(): The task_category {task_category} could not be found in the module {backend_name}. {str(e)}") + logger.error(f"get_target_module(): The task_category {task_category} could not be found in the module {backend_name}. {str(e)}") return None # return target_module diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/runner.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/runner.py index 85b1e5c6..c09118f0 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/runner.py +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/runner.py @@ -30,6 +30,7 @@ import copy import datetime +import logging import os from zipfile import ZipFile @@ -41,6 +42,7 @@ from .params import init_params from tinyml_torchmodelopt.quantization import TinyMLQuantizationVersion +logger = logging.getLogger(__name__) class ModelRunner(): @@ -55,9 +57,10 @@ def init_params(self, *args, **kwargs): def __init__(self, *args, verbose=True, **kwargs): self.params = self.init_params(*args, **kwargs) - # print the runner params + # log the runner params if verbose: - [print(key, ':', value) for key, value in vars(self.params).items()] + for key, value in vars(self.params).items(): + logger.info('%s : %s', key, value) # # normalize the paths if not self.params.dataset.dataset_name: @@ -111,14 +114,14 @@ def __init__(self, *args, verbose=True, **kwargs): inference_time_us_list = {k:v['inference_time_us'] for k,v in self.params.training.target_devices.items()} sram_usage_list = {k: v['sram'] for k, v in self.params.training.target_devices.items()} flash_usage_list = {k: v['flash'] for k, v in self.params.training.target_devices.items()} - print('---------------------------------------------------------------------') - print(f'Run Name: {self.params.common.run_name}') - print(f'- Model: {self.params.training.model_name}') - print(f'- TargetDevices & Estimated Inference Times (us): {inference_time_us_list}') - print(f'- TargetDevices & Estimated SRAM Usage (bytes): {sram_usage_list}') - print(f'- TargetDevices & Estimated Flash Usage (bytes): {flash_usage_list}') - print('- This model can be compiled for the above device(s).') - print('---------------------------------------------------------------------') + logger.info('---------------------------------------------------------------------') + logger.info(f'Run Name: {self.params.common.run_name}') + logger.info(f'- Model: {self.params.training.model_name}') + logger.info(f'- TargetDevices & Estimated Inference Times (us): {inference_time_us_list}') + logger.info(f'- TargetDevices & Estimated SRAM Usage (bytes): {sram_usage_list}') + logger.info(f'- TargetDevices & Estimated Flash Usage (bytes): {flash_usage_list}') + logger.info('- This model can be compiled for the above device(s).') + logger.info('---------------------------------------------------------------------') # ##################################################################### @@ -127,9 +130,9 @@ def __init__(self, *args, verbose=True, **kwargs): auto_data_dir = constants.get_default_data_dir_for_task(self.params.common.task_category) self.params.dataset.data_dir = auto_data_dir if verbose: - print(f"Auto-detected data_dir='{auto_data_dir}' for task_category='{self.params.common.task_category}'") + logger.info(f"Auto-detected data_dir='{auto_data_dir}' for task_category='{self.params.common.task_category}'") elif verbose: - print(f"Using user-specified data_dir='{self.params.dataset.data_dir}'") + logger.info(f"Using user-specified data_dir='{self.params.dataset.data_dir}'") # def resolve_run_name(self, run_name, model_name): @@ -233,9 +236,9 @@ def run(self): self.package_trained_model(model_training_package_files, self.params.training.model_packaged_path) if not utils.misc_utils.str2bool(self.params.testing.skip_train): if self.params.training.training_path_quantization: - print(f'\nTrained model is at: {self.params.training.training_path_quantization}\n') + logger.info(f'Trained model is at: {self.params.training.training_path_quantization}') else: - print(f'\nTrained model is at: {self.params.training.training_path}\n') + logger.info(f'Trained model is at: {self.params.training.training_path}') # we are done with training with open(self.params.training.log_file_path, 'a') as lfp: lfp.write('\nSUCCESS: ModelMaker - Training completed.') @@ -251,7 +254,7 @@ def run(self): self.model_compilation.clear() exit_flag = self.model_compilation.run() if exit_flag: - print(f'Compilation failed') + logger.error('Compilation failed') with open(self.params.compilation.log_file_path, 'a') as lfp: lfp.write('FAILURE: ModelMaker - Compilation failed.') return self.params @@ -276,7 +279,7 @@ def run(self): self.package_trained_model(model_compilation_package_files, self.params.compilation.model_packaged_path) - print(f'Compiled model is at: {self.params.compilation.compilation_path}') + logger.info(f'Compiled model is at: {self.params.compilation.compilation_path}') with open(self.params.compilation.log_file_path, 'a') as lfp: lfp.write('\nSUCCESS: ModelMaker - Compilation completed.') diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/training/__init__.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/training/__init__.py index 3ebec022..288ed1da 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/training/__init__.py +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/training/__init__.py @@ -29,10 +29,13 @@ ################################################################################# import copy +import logging import sys from .. import constants +logger = logging.getLogger(__name__) + # list all the modules here to add pretrained models _model_descriptions = {} _training_module_descriptions = {} @@ -72,13 +75,13 @@ def get_target_module(backend_name, task_category): try: backend_package = getattr(this_module, backend_name) except Exception as e: - print(f"get_target_module(): The requested module could not be found: {backend_name}. {str(e)}") + logger.error(f"get_target_module(): The requested module could not be found: {backend_name}. {str(e)}") return None # try: target_module = getattr(backend_package, task_category) except Exception as e: - print(f"get_target_module(): The task_category {task_category} could not be found in the module {backend_name}. {str(e)}") + logger.error(f"get_target_module(): The task_category {task_category} could not be found in the module {backend_name}. {str(e)}") return None # return target_module diff --git a/tinyml-modelmaker/tinyml_modelmaker/run_tinyml_modelmaker.py b/tinyml-modelmaker/tinyml_modelmaker/run_tinyml_modelmaker.py index 607fe594..bcf20431 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/run_tinyml_modelmaker.py +++ b/tinyml-modelmaker/tinyml_modelmaker/run_tinyml_modelmaker.py @@ -30,11 +30,14 @@ import argparse import json +import logging import os import sys import yaml +logger = logging.getLogger(__name__) + def main(config): target_device = config['common']['target_device'] @@ -51,7 +54,7 @@ def main(config): else: target_module = tinyml_modelmaker.get_target_module_from_task_type(task_type) if target_module is None: - print(f"Error: Could not infer target_module from task_type '{task_type}'. Please specify 'target_module' in config.") + logger.error(f"Could not infer target_module from task_type '{task_type}'. Please specify 'target_module' in config.") return False config['common']['target_module'] = target_module @@ -68,7 +71,7 @@ def main(config): model_description = ai_target_module.runner.ModelRunner.get_model_description(model_name) if config.get('training').get('enable', True): if model_description is None and not nas_enabled: - print(f"please check if the given model_name is a supported one: {model_name}") + logger.error(f"please check if the given model_name is a supported one: {model_name}") return False # When NAS is enabled, provide a minimal model description so the pipeline # can locate the correct training module and treat it as a generic model. @@ -102,7 +105,7 @@ def main(config): if 'compile_preset_name' in config['compilation']: compilation_preset_name = config['compilation']['compile_preset_name'] if compilation_preset_name not in preset_descriptions[target_device][task_type].keys(): - print(f'WARNING: Using "default_preset" for compilation since user choice-"{compilation_preset_name}" is unavailable') + logger.warning(f'Using "default_preset" for compilation since user choice-"{compilation_preset_name}" is unavailable') compilation_preset_name = 'default_preset' compilation_preset_description = preset_descriptions[target_device][task_type][compilation_preset_name] @@ -115,7 +118,7 @@ def main(config): # prepare run_params_file = model_runner.prepare() - print(f'Run params is at: {run_params_file}') + logger.info(f'Run params is at: {run_params_file}') # run model_runner.run() @@ -123,7 +126,7 @@ def main(config): if __name__ == '__main__': - print(f'argv: {sys.argv}') + logger.info(f'argv: {sys.argv}') # the cwd must be the root of the repository if os.path.split(os.getcwd())[-1] == 'tinyml_modelmaker': os.chdir('..') diff --git a/tinyml-modelmaker/tinyml_modelmaker/utils/download_utils.py b/tinyml-modelmaker/tinyml_modelmaker/utils/download_utils.py index fedce408..d1f579cb 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/utils/download_utils.py +++ b/tinyml-modelmaker/tinyml_modelmaker/utils/download_utils.py @@ -30,6 +30,7 @@ import copy import gzip +import logging import os import shutil import tarfile @@ -41,6 +42,8 @@ from . import misc_utils +logger = logging.getLogger(__name__) + def copy_file(file_path, file_path_local): if os.path.realpath(file_path) != os.path.realpath(file_path_local): @@ -93,7 +96,7 @@ def download_url(dataset_url, download_root, save_filename=None, progressbar_cre save_filename = save_filename if save_filename else os.path.basename(dataset_url) download_file = os.path.join(download_root, save_filename) if not os.path.exists(download_file): - print(f'downloading from {dataset_url} to {download_file}') + logger.info(f'downloading from {dataset_url} to {download_file}') progressbar_creator = progressbar_creator or misc_utils.ProgressBar resp = requests.get(dataset_url, stream=True, allow_redirects=True) total_size = int(resp.headers.get('content-length')) @@ -111,15 +114,15 @@ def download_url(dataset_url, download_root, save_filename=None, progressbar_cre except urllib.error.URLError as message: download_success = False exception_message = str(message) - print(exception_message) + logger.error(exception_message) except urllib.error.HTTPError as message: download_success = False exception_message = str(message) - print(exception_message) + logger.error(exception_message) except NameError as message: download_success = False exception_message = str(message) - print(exception_message) + logger.error(exception_message) # except Exception as message: # # sometimes getting exception even though download succeeded. # download_path = download_file @@ -184,7 +187,7 @@ def download_files(dataset_urls, download_root, extract_root=None, save_filename if log_writer is not None: success_writer, warning_writer = log_writer[:2] else: - success_writer, warning_writer = print, print + success_writer, warning_writer = logger.info, logger.warning # dataset_urls = dataset_urls if isinstance(dataset_urls, (list,tuple)) else [dataset_urls] save_filenames = save_filenames if isinstance(save_filenames, (list,tuple)) else \ @@ -229,7 +232,7 @@ def download_url_entry(download_entry, download_path=None, download_root=None): return None # elif isinstance(download_entry, str): - print(f'assuming the given download_url is a valid path: {download_entry}') + logger.info(f'assuming the given download_url is a valid path: {download_entry}') else: warnings.warn(f'unrecognized download_url: {download_entry}') # diff --git a/tinyml-modelmaker/tinyml_modelmaker/utils/misc_utils.py b/tinyml-modelmaker/tinyml_modelmaker/utils/misc_utils.py index cc1dc1c5..a0d7ff70 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/utils/misc_utils.py +++ b/tinyml-modelmaker/tinyml_modelmaker/utils/misc_utils.py @@ -37,6 +37,7 @@ import shutil import subprocess import sys +import logging from logging import getLogger import tqdm @@ -44,6 +45,8 @@ from . import config_dict +logger = logging.getLogger(__name__) + def _absolute_path(relpath): if relpath is None: @@ -85,7 +88,7 @@ def remove_if_exists(path): def make_symlink(source, dest): if source is None or (not os.path.exists(source)): - print(f'make_symlink failed - source: {source} is invalid') + logger.error(f'make_symlink failed - source: {source} is invalid') return # remove_if_exists(dest) From 1e75b975b32ed9f46e4a0f7dbdbf8f026592b82d Mon Sep 17 00:00:00 2001 From: M Platypus Date: Thu, 26 Feb 2026 07:18:22 -0500 Subject: [PATCH 02/10] Extract duplicated path resolution from ModelRunner into shared resolve_paths() Co-Authored-By: Claude Opus 4.6 --- .../ai_modules/timeseries/runner.py | 64 +---------- .../ai_modules/vision/runner.py | 64 +---------- .../tinyml_modelmaker/utils/misc_utils.py | 101 ++++++++++++++++++ 3 files changed, 105 insertions(+), 124 deletions(-) diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/runner.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/runner.py index 26900837..780c9ce5 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/runner.py +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/runner.py @@ -29,7 +29,6 @@ ################################################################################# import copy -import datetime import logging import os @@ -40,7 +39,6 @@ from ... import utils from . import constants, datasets, descriptions from .params import init_params -from tinyml_torchmodelopt.quantization import TinyMLQuantizationVersion logger = logging.getLogger(__name__) @@ -62,53 +60,8 @@ def __init__(self, *args, verbose=True, **kwargs): for key, value in vars(self.params).items(): logger.info('%s : %s', key, value) # - # normalize the paths - if not self.params.dataset.dataset_name: - self.params.dataset.dataset_name = os.path.splitext(os.path.basename(self.params.dataset.input_data_path))[0] - self.params.dataset.input_data_path = utils.absolute_path(self.params.dataset.input_data_path) - self.params.dataset.input_annotation_path = utils.absolute_path(self.params.dataset.input_annotation_path) - - self.params.common.run_name = self.resolve_run_name(self.params.common.run_name, self.params.training.model_name) - self.params.dataset.extract_path = self.params.dataset.dataset_path - - if self.params.training.train_output_path: - self.params.common.projects_path = utils.absolute_path(self.params.training.train_output_path) - self.params.common.project_path = os.path.join(self.params.common.projects_path)# , self.params.dataset.dataset_name) - self.params.dataset.dataset_path = os.path.join(self.params.common.project_path, 'dataset') - self.params.common.project_run_path = self.params.common.projects_path - self.params.training.training_path = utils.absolute_path(os.path.join(self.params.training.train_output_path, 'training_base')) - if self.params.training.quantization != TinyMLQuantizationVersion.NO_QUANTIZATION: - self.params.training.training_path_quantization = utils.absolute_path(os.path.join(self.params.training.train_output_path, 'training_quantization')) - self.params.training.model_packaged_path = os.path.join(self.params.training.train_output_path, - '_'.join(os.path.split(self.params.common.run_name))+'.zip') - else: - self.params.common.projects_path = utils.absolute_path(self.params.common.projects_path) - self.params.common.project_path = os.path.join(self.params.common.projects_path, self.params.dataset.dataset_name) - self.params.common.project_run_path = os.path.join(self.params.common.project_path, 'run', self.params.common.run_name) - self.params.dataset.dataset_path = os.path.join(self.params.common.project_path, 'dataset') - self.params.training.training_path = utils.absolute_path(os.path.join(self.params.common.project_run_path, 'training', 'base')) - if self.params.training.quantization != TinyMLQuantizationVersion.NO_QUANTIZATION: - self.params.training.training_path_quantization = utils.absolute_path(os.path.join(self.params.common.project_run_path, 'training', 'quantization')) - self.params.training.model_packaged_path = os.path.join(self.params.training.training_path, - '_'.join(os.path.split(self.params.common.run_name))+'.zip') - - assert self.params.common.target_device in constants.TARGET_DEVICES_ALL, f'common.target_device must be set to one of: {constants.TARGET_DEVICES_ALL}' - # target_device_compilation_folder = self.params.common.target_device - - if self.params.compilation.compile_output_path: - if self.params.training.enable == False and self.params.compilation.enable == True: - self.params.common.projects_path = utils.absolute_path(self.params.compilation.compile_output_path) - self.params.common.project_run_path = self.params.common.projects_path - self.params.compilation.compilation_path = utils.absolute_path(self.params.compilation.compile_output_path) - self.params.compilation.model_packaged_path = os.path.join(self.params.compilation.compile_output_path, - '_'.join(os.path.split( - self.params.common.run_name)) + f'_{self.params.common.target_device}.zip') - else: - # self.params.compilation.compilation_path = utils.absolute_path(os.path.join(self.params.common.project_run_path, 'compilation', target_device_compilation_folder)) - self.params.compilation.compilation_path = utils.absolute_path(os.path.join(self.params.common.project_run_path, 'compilation')) - self.params.compilation.model_packaged_path = os.path.join(self.params.compilation.compilation_path, - '_'.join(os.path.split( - self.params.common.run_name)) + f'_{self.params.common.target_device}.zip') + # resolve and normalize all paths + utils.misc_utils.resolve_paths(self.params, constants.TARGET_DEVICES_ALL) if self.params.common.target_device in self.params.training.target_devices: inference_time_us_list = {k:v.get('inference_time_us') for k,v in self.params.training.target_devices.items()} @@ -135,19 +88,6 @@ def __init__(self, *args, verbose=True, **kwargs): logger.info(f"Using user-specified data_dir='{self.params.dataset.data_dir}'") # - def resolve_run_name(self, run_name, model_name): - if not run_name: - return '' - # - # modify or set any parameters here as required. - if '{date-time}' in run_name: - run_name = run_name.replace('{date-time}', datetime.datetime.now().strftime("%Y%m%d-%H%M%S")) - # - if '{model_name}' in run_name: - run_name = run_name.replace('{model_name}', model_name) - # - return run_name - def clear(self): pass diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/runner.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/runner.py index c09118f0..ea372ff9 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/runner.py +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/runner.py @@ -29,7 +29,6 @@ ################################################################################# import copy -import datetime import logging import os @@ -40,7 +39,6 @@ from ... import utils from . import constants, datasets, descriptions from .params import init_params -from tinyml_torchmodelopt.quantization import TinyMLQuantizationVersion logger = logging.getLogger(__name__) @@ -62,53 +60,8 @@ def __init__(self, *args, verbose=True, **kwargs): for key, value in vars(self.params).items(): logger.info('%s : %s', key, value) # - # normalize the paths - if not self.params.dataset.dataset_name: - self.params.dataset.dataset_name = os.path.splitext(os.path.basename(self.params.dataset.input_data_path))[0] - self.params.dataset.input_data_path = utils.absolute_path(self.params.dataset.input_data_path) - self.params.dataset.input_annotation_path = utils.absolute_path(self.params.dataset.input_annotation_path) - - self.params.common.run_name = self.resolve_run_name(self.params.common.run_name, self.params.training.model_name) - self.params.dataset.extract_path = self.params.dataset.dataset_path - - if self.params.training.train_output_path: - self.params.common.projects_path = utils.absolute_path(self.params.training.train_output_path) - self.params.common.project_path = os.path.join(self.params.common.projects_path)# , self.params.dataset.dataset_name) - self.params.dataset.dataset_path = os.path.join(self.params.common.project_path, 'dataset') - self.params.common.project_run_path = self.params.common.projects_path - self.params.training.training_path = utils.absolute_path(os.path.join(self.params.training.train_output_path, 'training_base')) - if self.params.training.quantization != TinyMLQuantizationVersion.NO_QUANTIZATION: - self.params.training.training_path_quantization = utils.absolute_path(os.path.join(self.params.training.train_output_path, 'training_quantization')) - self.params.training.model_packaged_path = os.path.join(self.params.training.train_output_path, - '_'.join(os.path.split(self.params.common.run_name))+'.zip') - else: - self.params.common.projects_path = utils.absolute_path(self.params.common.projects_path) - self.params.common.project_path = os.path.join(self.params.common.projects_path, self.params.dataset.dataset_name) - self.params.common.project_run_path = os.path.join(self.params.common.project_path, 'run', self.params.common.run_name) - self.params.dataset.dataset_path = os.path.join(self.params.common.project_path, 'dataset') - self.params.training.training_path = utils.absolute_path(os.path.join(self.params.common.project_run_path, 'training', 'base')) - if self.params.training.quantization != TinyMLQuantizationVersion.NO_QUANTIZATION: - self.params.training.training_path_quantization = utils.absolute_path(os.path.join(self.params.common.project_run_path, 'training', 'quantization')) - self.params.training.model_packaged_path = os.path.join(self.params.training.training_path, - '_'.join(os.path.split(self.params.common.run_name))+'.zip') - - assert self.params.common.target_device in constants.TARGET_DEVICES_ALL, f'common.target_device must be set to one of: {constants.TARGET_DEVICES_ALL}' - # target_device_compilation_folder = self.params.common.target_device - - if self.params.compilation.compile_output_path: - if self.params.training.enable == False and self.params.compilation.enable == True: - self.params.common.projects_path = utils.absolute_path(self.params.compilation.compile_output_path) - self.params.common.project_run_path = self.params.common.projects_path - self.params.compilation.compilation_path = utils.absolute_path(self.params.compilation.compile_output_path) - self.params.compilation.model_packaged_path = os.path.join(self.params.compilation.compile_output_path, - '_'.join(os.path.split( - self.params.common.run_name)) + f'_{self.params.common.target_device}.zip') - else: - # self.params.compilation.compilation_path = utils.absolute_path(os.path.join(self.params.common.project_run_path, 'compilation', target_device_compilation_folder)) - self.params.compilation.compilation_path = utils.absolute_path(os.path.join(self.params.common.project_run_path, 'compilation')) - self.params.compilation.model_packaged_path = os.path.join(self.params.compilation.compilation_path, - '_'.join(os.path.split( - self.params.common.run_name)) + f'_{self.params.common.target_device}.zip') + # resolve and normalize all paths + utils.misc_utils.resolve_paths(self.params, constants.TARGET_DEVICES_ALL) if self.params.common.target_device in self.params.training.target_devices: inference_time_us_list = {k:v['inference_time_us'] for k,v in self.params.training.target_devices.items()} @@ -135,19 +88,6 @@ def __init__(self, *args, verbose=True, **kwargs): logger.info(f"Using user-specified data_dir='{self.params.dataset.data_dir}'") # - def resolve_run_name(self, run_name, model_name): - if not run_name: - return '' - # - # modify or set any parameters here as required. - if '{date-time}' in run_name: - run_name = run_name.replace('{date-time}', datetime.datetime.now().strftime("%Y%m%d-%H%M%S")) - # - if '{model_name}' in run_name: - run_name = run_name.replace('{model_name}', model_name) - # - return run_name - def clear(self): pass diff --git a/tinyml-modelmaker/tinyml_modelmaker/utils/misc_utils.py b/tinyml-modelmaker/tinyml_modelmaker/utils/misc_utils.py index a0d7ff70..699c9e06 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/utils/misc_utils.py +++ b/tinyml-modelmaker/tinyml_modelmaker/utils/misc_utils.py @@ -64,6 +64,107 @@ def absolute_path(relpath): return _absolute_path(relpath) +def resolve_run_name(run_name, model_name): + """Expand {date-time} and {model_name} placeholders in *run_name*.""" + import datetime + if not run_name: + return '' + if '{date-time}' in run_name: + run_name = run_name.replace('{date-time}', datetime.datetime.now().strftime("%Y%m%d-%H%M%S")) + if '{model_name}' in run_name: + run_name = run_name.replace('{model_name}', model_name) + return run_name + + +def resolve_paths(params, target_devices_all): + """Resolve and normalize all paths in the runner params. + + Computes absolute paths for dataset, training, and compilation directories + based on whether custom output paths are provided. Modifies params in-place. + + Args: + params: ConfigDict with common, dataset, training, compilation sections. + target_devices_all: Collection of valid target device identifiers. + + Returns: + The params object (modified in-place). + + Raises: + ValueError: If target_device is not in target_devices_all. + """ + from tinyml_torchmodelopt.quantization import TinyMLQuantizationVersion + + # --- dataset name fallback --- + if not params.dataset.dataset_name: + params.dataset.dataset_name = os.path.splitext( + os.path.basename(params.dataset.input_data_path))[0] + + # --- normalize input paths --- + params.dataset.input_data_path = absolute_path(params.dataset.input_data_path) + params.dataset.input_annotation_path = absolute_path(params.dataset.input_annotation_path) + + # --- resolve run name templates --- + params.common.run_name = resolve_run_name(params.common.run_name, params.training.model_name) + params.dataset.extract_path = params.dataset.dataset_path + + # --- training path resolution --- + if params.training.train_output_path: + # custom output: flat structure under train_output_path + params.common.projects_path = absolute_path(params.training.train_output_path) + params.common.project_path = os.path.join(params.common.projects_path) + params.dataset.dataset_path = os.path.join(params.common.project_path, 'dataset') + params.common.project_run_path = params.common.projects_path + params.training.training_path = absolute_path( + os.path.join(params.training.train_output_path, 'training_base')) + if params.training.quantization != TinyMLQuantizationVersion.NO_QUANTIZATION: + params.training.training_path_quantization = absolute_path( + os.path.join(params.training.train_output_path, 'training_quantization')) + params.training.model_packaged_path = os.path.join( + params.training.train_output_path, + '_'.join(os.path.split(params.common.run_name)) + '.zip') + else: + # default: nested structure under projects_path/dataset_name/run/run_name + params.common.projects_path = absolute_path(params.common.projects_path) + params.common.project_path = os.path.join( + params.common.projects_path, params.dataset.dataset_name) + params.common.project_run_path = os.path.join( + params.common.project_path, 'run', params.common.run_name) + params.dataset.dataset_path = os.path.join(params.common.project_path, 'dataset') + params.training.training_path = absolute_path( + os.path.join(params.common.project_run_path, 'training', 'base')) + if params.training.quantization != TinyMLQuantizationVersion.NO_QUANTIZATION: + params.training.training_path_quantization = absolute_path( + os.path.join(params.common.project_run_path, 'training', 'quantization')) + params.training.model_packaged_path = os.path.join( + params.training.training_path, + '_'.join(os.path.split(params.common.run_name)) + '.zip') + + # --- target device validation --- + if params.common.target_device not in target_devices_all: + raise ValueError( + f'common.target_device must be set to one of: {target_devices_all}') + + # --- compilation path resolution --- + if params.compilation.compile_output_path: + if params.training.enable is False and params.compilation.enable is True: + params.common.projects_path = absolute_path(params.compilation.compile_output_path) + params.common.project_run_path = params.common.projects_path + params.compilation.compilation_path = absolute_path(params.compilation.compile_output_path) + params.compilation.model_packaged_path = os.path.join( + params.compilation.compile_output_path, + '_'.join(os.path.split(params.common.run_name)) + + f'_{params.common.target_device}.zip') + else: + params.compilation.compilation_path = absolute_path( + os.path.join(params.common.project_run_path, 'compilation')) + params.compilation.model_packaged_path = os.path.join( + params.compilation.compilation_path, + '_'.join(os.path.split(params.common.run_name)) + + f'_{params.common.target_device}.zip') + + return params + + def is_junction(path: str) -> bool: try: return bool(os.readlink(path)) From 7af3396da4cbda44bcf0336924657501f1dcb8bb Mon Sep 17 00:00:00 2001 From: M Platypus Date: Thu, 26 Feb 2026 14:23:24 -0500 Subject: [PATCH 03/10] Replace magic strings with named constants Add TRAINING_BACKEND_TINYML_TINYVERSE, DATA_DIR_CLASSES, DATA_DIR_FILES, DATA_DIR_IMAGES constants. Use existing TRAINING_DEVICE_CUDA in params.py defaults instead of bare 'cuda' strings. Co-Authored-By: Claude Opus 4.6 --- .../ai_modules/timeseries/constants.py | 13 ++++++++++--- .../ai_modules/timeseries/params.py | 2 +- .../training/tinyml_tinyverse/timeseries_base.py | 2 +- .../ai_modules/vision/constants.py | 6 ++++++ .../tinyml_modelmaker/ai_modules/vision/params.py | 2 +- 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/constants.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/constants.py index b6d47d28..d8124117 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/constants.py +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/constants.py @@ -162,11 +162,11 @@ def get_default_data_dir_for_task(task_category): str: 'classes' for classification/anomaly tasks, 'files' for regression/forecasting """ if task_category in [TASK_CATEGORY_TS_CLASSIFICATION, TASK_CATEGORY_TS_ANOMALYDETECTION]: - return 'classes' + return DATA_DIR_CLASSES elif task_category in [TASK_CATEGORY_TS_REGRESSION, TASK_CATEGORY_TS_FORECASTING]: - return 'files' + return DATA_DIR_FILES else: - return 'classes' # Safe fallback + return DATA_DIR_CLASSES # Safe fallback # target_device @@ -242,6 +242,13 @@ def get_default_data_dir_for_task(task_category): TARGET_DEVICE_TYPE_MCU ] +# training backend +TRAINING_BACKEND_TINYML_TINYVERSE = 'tinyml_tinyverse' + +# data directory names +DATA_DIR_CLASSES = 'classes' +DATA_DIR_FILES = 'files' + # training_device TRAINING_DEVICE_CPU = 'cpu' TRAINING_DEVICE_CUDA = 'cuda' diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/params.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/params.py index 1d1d9a1f..3aa2a9f1 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/params.py +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/params.py @@ -104,7 +104,7 @@ def init_params(*args, **kwargs): optimizer='sgd', weight_decay=1e-4, lr_scheduler='cosineannealinglr', - training_device='cuda', # 'cpu', 'cuda' + training_device=constants.TRAINING_DEVICE_CUDA, num_gpus=1, # 0,1 distributed=True, training_master_port=29500, diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/training/tinyml_tinyverse/timeseries_base.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/training/tinyml_tinyverse/timeseries_base.py index 65a9cdcd..c6d233ef 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/training/tinyml_tinyverse/timeseries_base.py +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/training/tinyml_tinyverse/timeseries_base.py @@ -527,7 +527,7 @@ def create_template_model_description(task_category, task_type, dataset_loader=N """ training_dict = dict( quantization=TinyMLQuantizationVersion.QUANTIZATION_TINPU, - training_backend='tinyml_tinyverse', + training_backend=constants.TRAINING_BACKEND_TINYML_TINYVERSE, model_training_id='', model_name='', learning_rate=2e-3, diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/constants.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/constants.py index 09c1487e..73c3ff61 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/constants.py +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/constants.py @@ -120,6 +120,12 @@ def get_default_data_dir_for_task(task_category): TARGET_DEVICE_TYPE_MCU ] +# training backend +TRAINING_BACKEND_TINYML_TINYVERSE = 'tinyml_tinyverse' + +# data directory names +DATA_DIR_IMAGES = 'images' + # training_device TRAINING_DEVICE_CPU = 'cpu' TRAINING_DEVICE_CUDA = 'cuda' diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/params.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/params.py index 0b5a532a..34aba2a3 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/params.py +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/params.py @@ -104,7 +104,7 @@ def init_params(*args, **kwargs): optimizer='sgd', weight_decay=1e-4, lr_scheduler='cosineannealinglr', - training_device='cuda', # 'cpu', 'cuda' + training_device=constants.TRAINING_DEVICE_CUDA, num_gpus=1, # 0,1 distributed=True, training_master_port=29500, From da1837c39831b410dba41febe716898e512800c6 Mon Sep 17 00:00:00 2001 From: M Platypus Date: Thu, 26 Feb 2026 14:25:35 -0500 Subject: [PATCH 04/10] Replace assert/sys.exit/raise-string with proper exceptions across tinyml-modelmaker Co-Authored-By: Claude Opus 4.6 --- .../ai_modules/common/datasets/__init__.py | 15 +++++---- .../common/datasets/dataset_utils.py | 33 ++++++++++++------- .../ai_modules/timeseries/descriptions.py | 14 +++++--- .../ai_modules/vision/descriptions.py | 14 +++++--- .../run_tinyml_modelmaker.py | 2 +- .../tinyml_modelmaker/utils/config_dict.py | 5 +-- .../tinyml_modelmaker/utils/misc_utils.py | 3 +- 7 files changed, 55 insertions(+), 31 deletions(-) diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/common/datasets/__init__.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/common/datasets/__init__.py index dea17f58..eb280157 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/common/datasets/__init__.py +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/common/datasets/__init__.py @@ -47,7 +47,7 @@ def get_datasets_list(task_type=None): elif task_type == 'audio_classification': return ['SpeechCommands'] # ['oxford_flowers102'] else: - assert False, 'unknown task type for get_datasets_list' + raise ValueError(f'unknown task type for get_datasets_list: {task_type}') def get_target_module(backend_name): @@ -93,7 +93,7 @@ def run(self): extract_root = os.path.dirname(self.params.dataset.input_data_path) extract_success = utils.extract_files(self.params.dataset.input_data_path, extract_root) if not extract_success: - raise "Dataset could not be extracted" + raise RuntimeError("Dataset could not be extracted") self.params.dataset.input_data_path = os.path.dirname(self.params.dataset.input_data_path) for split_name in self.params.dataset.split_names: @@ -184,14 +184,16 @@ def run(self): # self.out_files = dataset_utils.create_simple_split(self.file_list, self.params.common.project_run_path + '/dataset', self.params.dataset.split_names, self.params.dataset.split_factor, shuffle_items=True, random_seed=42) self.logger.info('Splits of the dataset can be found at: {}'.format(self.params.dataset.annotation_path_splits)) else: - assert False, f'invalid dataset provided at {self.params.dataset.input_data_path}' + raise FileNotFoundError(f'invalid dataset provided at {self.params.dataset.input_data_path}') def get_max_num_files(self): if isinstance(self.params.dataset.max_num_files, (list, tuple)): max_num_files = self.params.dataset.max_num_files elif isinstance(self.params.dataset.max_num_files, int): - assert (0.0 < self.params.dataset.split_factor < 1.0), 'split_factor must be between 0 and 1.0' - assert len(self.params.dataset.split_names) > 1, 'split_names must have at least two entries' + if not (0.0 < self.params.dataset.split_factor < 1.0): + raise ValueError('split_factor must be between 0 and 1.0') + if len(self.params.dataset.split_names) <= 1: + raise ValueError('split_names must have at least two entries') max_num_files = [None] * len(self.params.dataset.split_names) for split_id, split_name in enumerate(self.params.dataset.split_names): if split_id == 0: @@ -202,7 +204,8 @@ def get_max_num_files(self): # else: warnings.warn('unrecognized value for max_num_files - must be int, list or tuple') - assert len(self.params.dataset.split_names) > 1, 'split_names must have at least two entries' + if len(self.params.dataset.split_names) <= 1: + raise ValueError('split_names must have at least two entries') max_num_files = [None] * len(self.params.dataset.split_names) # return max_num_files diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/common/datasets/dataset_utils.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/common/datasets/dataset_utils.py index b5c10e69..e48f32da 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/common/datasets/dataset_utils.py +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/common/datasets/dataset_utils.py @@ -88,26 +88,31 @@ def create_inter_file_split(file_list: str, split_list_files: tuple, split_facto :param split_factor: can be a float number or a list of splits e.g [0.2, 0.3] :return: out_files: List containing the paths of files that contain the dataset of the corresponding splits ''' - assert isinstance(split_list_files, (list, tuple)), "split_list_files should be passed as a tuple or list" + if not isinstance(split_list_files, (list, tuple)): + raise TypeError("split_list_files should be passed as a tuple or list") number_of_splits = len(split_list_files) split_factors = [] if type(split_factor) == float: - assert split_factor < 1.0, "split_factor should be less than 1" + if split_factor >= 1.0: + raise ValueError("split_factor should be less than 1") # The default split factor is the fraction for training set. split_factors.append(split_factor) # The remainder of the set will be equally split between val or val/test remainder = 1 - split_factor elif isinstance(split_factor, (list, tuple)): - assert sum(split_factor) <= 1, "The Sum of split factors should be <=1" - assert len(split_factor) <= len(split_list_files), "The number of elements in split factors should be less than/equal to number of split names" + if sum(split_factor) > 1: + raise ValueError("The Sum of split factors should be <=1") + if len(split_factor) > len(split_list_files): + raise ValueError("The number of elements in split factors should be less than/equal to number of split names") split_factors.extend(split_factor) remainder = 1 - sum(split_factor) if number_of_splits > len(split_factor): remainder_fraction = remainder / (number_of_splits - len(split_factor)) [split_factors.append(remainder_fraction) for _ in range(number_of_splits - len(split_factor))] - assert len(split_factor) == len(split_list_files), f"Number of split files: {len(split_list_files)} should be same as length of split factors: {len(split_factor)}" + if len(split_factor) != len(split_list_files): + raise ValueError(f"Number of split files: {len(split_list_files)} should be same as length of split factors: {len(split_factor)}") with open(file_list) as fp: list_of_files = [x.strip() for x in fp.readlines()] # Contains the list of files @@ -143,26 +148,31 @@ def create_intra_file_split(file_list: str, split_list_files: tuple, split_facto :param split_list_files: training_list.txt and validation_list.txt and so on... :param split_factor: can be a float number or a list of splits e.g [0.2, 0.3] ''' - assert isinstance(split_list_files, (list, tuple)), "split_list_files should be passed as a tuple or list" + if not isinstance(split_list_files, (list, tuple)): + raise TypeError("split_list_files should be passed as a tuple or list") number_of_splits = len(split_list_files) split_factors = [] if type(split_factor) == float: - assert split_factor < 1.0, "split_factor should be less than 1" + if split_factor >= 1.0: + raise ValueError("split_factor should be less than 1") # The default split factor is the fraction for training set. split_factors.append(split_factor) # The remainder of the set will be equally split between val or val/test remainder = 1 - split_factor elif isinstance(split_factor, (list, tuple)): - assert sum(split_factor) <= 1, "The Sum of split factors should be <=1" - assert len(split_factor) <= len(split_list_files), "The number of elements in split factors should be less than/equal to number of split names" + if sum(split_factor) > 1: + raise ValueError("The Sum of split factors should be <=1") + if len(split_factor) > len(split_list_files): + raise ValueError("The number of elements in split factors should be less than/equal to number of split names") split_factors.extend(split_factor) remainder = 1 - sum(split_factor) if number_of_splits > len(split_factor): remainder_fraction = remainder / (number_of_splits - len(split_factor)) [split_factors.append(remainder_fraction) for _ in range(number_of_splits - len(split_factor))] - assert len(split_factor) == len(split_list_files), f"Number of split files: {len(split_list_files)} should be same as length of split factors: {len(split_factor)}" + if len(split_factor) != len(split_list_files): + raise ValueError(f"Number of split files: {len(split_list_files)} should be same as length of split factors: {len(split_factor)}") with open(file_list) as fp: # list_of_files = [os.path.join(os.path.dirname(os.path.dirname(file_list)), data_dir, x.strip()) for x in fp.readlines()] # Contains the list of files @@ -366,7 +376,8 @@ def get_color_palette(num_classes): if len(colors_list) < 256: colors_list += [(255,255,255)] * (256-len(colors_list)) # - assert len(colors_list) == 256, f'incorrect length for color palette {len(colors_list)}' + if len(colors_list) != 256: + raise ValueError(f'incorrect length for color palette {len(colors_list)}') return colors_list diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/descriptions.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/descriptions.py index 8f29b563..41f55f5c 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/descriptions.py +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/descriptions.py @@ -227,16 +227,20 @@ def get_model_descriptions(params): def get_model_description(model_name): - assert model_name, 'model_name must be specified for get_model_description().' \ - 'if model_name is not known, use the method get_model_descriptions() that returns supported models.' + if not model_name: + raise ValueError( + 'model_name must be specified for get_model_description(). ' + 'If model_name is not known, use get_model_descriptions() that returns supported models.') model_description = training.get_model_description(model_name) return model_description def set_model_description(params, model_description): - assert model_description is not None, f'could not find pretrained model for {params.training.model_name}' - assert params.common.task_type == model_description['common']['task_type'], \ - f'task_type: {params.common.task_type} does not match the pretrained model' + if model_description is None: + raise ValueError(f'could not find pretrained model for {params.training.model_name}') + if params.common.task_type != model_description['common']['task_type']: + raise ValueError( + f'task_type: {params.common.task_type} does not match the pretrained model') # get pretrained model checkpoint and other details params.update(model_description) return params diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/descriptions.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/descriptions.py index eac5cff6..d46215c1 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/descriptions.py +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/descriptions.py @@ -175,16 +175,20 @@ def get_model_descriptions(params): def get_model_description(model_name): - assert model_name, 'model_name must be specified for get_model_description().' \ - 'if model_name is not known, use the method get_model_descriptions() that returns supported models.' + if not model_name: + raise ValueError( + 'model_name must be specified for get_model_description(). ' + 'If model_name is not known, use get_model_descriptions() that returns supported models.') model_description = training.get_model_description(model_name) return model_description def set_model_description(params, model_description): - assert model_description is not None, f'could not find pretrained model for {params.training.model_name}' - assert params.common.task_type == model_description['common']['task_type'], \ - f'task_type: {params.common.task_type} does not match the pretrained model' + if model_description is None: + raise ValueError(f'could not find pretrained model for {params.training.model_name}') + if params.common.task_type != model_description['common']['task_type']: + raise ValueError( + f'task_type: {params.common.task_type} does not match the pretrained model') # get pretrained model checkpoint and other details params.update(model_description) return params diff --git a/tinyml-modelmaker/tinyml_modelmaker/run_tinyml_modelmaker.py b/tinyml-modelmaker/tinyml_modelmaker/run_tinyml_modelmaker.py index bcf20431..a8e10646 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/run_tinyml_modelmaker.py +++ b/tinyml-modelmaker/tinyml_modelmaker/run_tinyml_modelmaker.py @@ -150,7 +150,7 @@ def main(config): elif args.config_file.endswith('.json'): config = json.load(fp) else: - assert False, f'unrecognized config file extension for {args.config_file}' + raise ValueError(f'unrecognized config file extension for {args.config_file}') # # diff --git a/tinyml-modelmaker/tinyml_modelmaker/utils/config_dict.py b/tinyml-modelmaker/tinyml_modelmaker/utils/config_dict.py index 750201be..ed0e0590 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/utils/config_dict.py +++ b/tinyml-modelmaker/tinyml_modelmaker/utils/config_dict.py @@ -43,7 +43,8 @@ def __init__(self, input=None, *args, **kwargs): settings_file = None if isinstance(input, str): ext = os.path.splitext(input)[1] - assert ext == '.yaml', f'unrecognized file type for: {input}' + if ext != '.yaml': + raise ValueError(f'unrecognized file type for: {input}') with open(input) as fp: input_dict = yaml.safe_load(fp) # @@ -51,7 +52,7 @@ def __init__(self, input=None, *args, **kwargs): elif isinstance(input, dict): input_dict = input elif input is not None: - assert False, 'got invalid input' + raise TypeError('got invalid input') # # override the entries with args for value in args: diff --git a/tinyml-modelmaker/tinyml_modelmaker/utils/misc_utils.py b/tinyml-modelmaker/tinyml_modelmaker/utils/misc_utils.py index 699c9e06..5e454e97 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/utils/misc_utils.py +++ b/tinyml-modelmaker/tinyml_modelmaker/utils/misc_utils.py @@ -246,7 +246,8 @@ def simplify_dict(in_dict): ''' simplify dict so that it can be written using yaml(pyyaml) package ''' - assert isinstance(in_dict, (dict, config_dict.ConfigDict)), 'input must of type dict or ConfigDict' + if not isinstance(in_dict, (dict, config_dict.ConfigDict)): + raise TypeError('input must be of type dict or ConfigDict') d = dict() for k, v in in_dict.items(): if isinstance(v, (dict,config_dict.ConfigDict)): From c9aff81557a6fb3a1a5348c5b6de4eea7bdac72b Mon Sep 17 00:00:00 2001 From: M Platypus Date: Wed, 18 Feb 2026 18:16:09 -0500 Subject: [PATCH 05/10] Add Protocol definitions for component interfaces (ModelRunner, Trainer, etc.) Define typing.Protocol classes that formalize the implicit contracts already followed by ModelRunner, ModelTraining, ModelCompilation, and DatasetHandling. Uses structural subtyping so no existing classes need modification. Enables static type checking and documents the interface contracts for future implementations. Co-Authored-By: Claude Opus 4.6 --- .../tinyml_modelmaker/ai_modules/__init__.py | 1 + .../tinyml_modelmaker/ai_modules/protocols.py | 163 ++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 tinyml-modelmaker/tinyml_modelmaker/ai_modules/protocols.py diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/__init__.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/__init__.py index 9fbdd63c..b678d5e7 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/__init__.py +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/__init__.py @@ -29,6 +29,7 @@ ################################################################################# import sys +from . import protocols from . import timeseries from . import vision from . import audio diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/protocols.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/protocols.py new file mode 100644 index 00000000..050d38f1 --- /dev/null +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/protocols.py @@ -0,0 +1,163 @@ +################################################################################# +# Copyright (c) 2023-2024, Texas Instruments +# All Rights Reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +################################################################################# + +"""Protocol definitions for tinyml-modelmaker component interfaces. + +These protocols document the implicit contracts that ModelRunner, ModelTraining, +ModelCompilation, and DatasetHandling implementations must satisfy. They use +structural subtyping (typing.Protocol) so existing classes conform automatically +without inheriting from them. + +Usage with static type checkers (mypy / pyright):: + + from tinyml_modelmaker.ai_modules.protocols import Trainer + + def start_training(trainer: Trainer) -> None: + trainer.clear() + trainer.run() + +Runtime checks are also supported via ``@runtime_checkable``:: + + isinstance(my_training_obj, Trainer) # True if it has the right methods +""" + +from __future__ import annotations + +from typing import Any, Protocol, runtime_checkable + +from ..utils.config_dict import ConfigDict + + +# --------------------------------------------------------------------------- +# Base protocol shared by all pipeline components +# --------------------------------------------------------------------------- + +@runtime_checkable +class LifecycleComponent(Protocol): + """Base protocol for pipeline components. + + Every component in the tinyml-modelmaker pipeline follows the same + lifecycle: ``init_params()`` -> ``__init__()`` -> ``clear()`` -> + ``run()`` -> ``get_params()``. This protocol captures the subset + of that lifecycle that is common to *all* component types. + + Note: All concrete implementations also store a ``params: ConfigDict`` + instance attribute. It is omitted here so that ``@runtime_checkable`` + ``issubclass()`` checks work (Python disallows non-method members in + runtime-checkable protocol ``issubclass()`` calls). Static type + checkers enforce the attribute via the child protocols' ``__init__`` + signatures. + """ + + @classmethod + def init_params(cls, *args: Any, **kwargs: Any) -> ConfigDict: ... + + def clear(self) -> None: ... + + def get_params(self) -> ConfigDict: ... + + +# --------------------------------------------------------------------------- +# Dataset handling +# --------------------------------------------------------------------------- + +@runtime_checkable +class DatasetHandler(LifecycleComponent, Protocol): + """Protocol for dataset handling components. + + Concrete implementation: ``common.datasets.DatasetHandling`` + """ + + def __init__(self, *args: Any, quit_event: Any = None, **kwargs: Any) -> None: ... + + def run(self) -> None: ... + + +# --------------------------------------------------------------------------- +# Model training +# --------------------------------------------------------------------------- + +@runtime_checkable +class Trainer(LifecycleComponent, Protocol): + """Protocol for model training components. + + Concrete implementations: + - ``timeseries.training.tinyml_tinyverse.timeseries_classification.ModelTraining`` + - ``timeseries.training.tinyml_tinyverse.timeseries_regression.ModelTraining`` + - ``timeseries.training.tinyml_tinyverse.timeseries_anomalydetection.ModelTraining`` + - ``timeseries.training.tinyml_tinyverse.timeseries_forecasting.ModelTraining`` + - ``vision.training.tinyml_tinyverse.image_classification.ModelTraining`` + """ + + def __init__(self, *args: Any, quit_event: Any = None, **kwargs: Any) -> None: ... + + def run(self, **kwargs: Any) -> None: ... + + def stop(self) -> None: ... + + +# --------------------------------------------------------------------------- +# Model compilation +# --------------------------------------------------------------------------- + +@runtime_checkable +class Compiler(LifecycleComponent, Protocol): + """Protocol for model compilation components. + + Concrete implementation: ``common.compilation.tinyml_benchmark.ModelCompilation`` + """ + + def __init__(self, *args: Any, quit_event: Any = None, **kwargs: Any) -> None: ... + + def run(self, **kwargs: Any) -> int: ... + + +# --------------------------------------------------------------------------- +# Top-level model runner +# --------------------------------------------------------------------------- + +@runtime_checkable +class Runner(LifecycleComponent, Protocol): + """Protocol for the top-level model runner. + + Concrete implementations: + - ``timeseries.runner.ModelRunner`` + - ``vision.runner.ModelRunner`` + """ + + def __init__(self, *args: Any, verbose: bool = True, **kwargs: Any) -> None: ... + + def prepare(self) -> str: ... + + def run(self) -> ConfigDict: ... + + def write_status_file(self) -> str: ... + + def package_trained_model(self, input_files: list, compressed_file_name: str) -> int: ... From 8505cd1e92aa31592a2cbc4fd06ececab96ac23b Mon Sep 17 00:00:00 2001 From: M Platypus Date: Tue, 17 Feb 2026 12:49:55 -0500 Subject: [PATCH 06/10] Add ARCHITECTURE.md with codebase documentation, diagram, and improvement analysis Documents the full tinyml-tensorlab architecture including all four sub-repos, pipeline flow, configuration system, model/quantization/NAS subsystems, a Mermaid architecture diagram, and 12 design/implementation improvement recommendations. Co-Authored-By: Claude Opus 4.6 --- ARCHITECTURE.md | 540 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 540 insertions(+) create mode 100644 ARCHITECTURE.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..3047982c --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,540 @@ +# tinyml-tensorlab Architecture + +## Table of Contents + +- [Repository Overview](#repository-overview) +- [Sub-Repository Responsibilities](#sub-repository-responsibilities) +- [Supported Tasks & Target Devices](#supported-tasks--target-devices) +- [Entry Points](#entry-points) +- [Pipeline Flow](#pipeline-flow) +- [Configuration System](#configuration-system) +- [Model Architecture System](#model-architecture-system) +- [Quantization System](#quantization-system) +- [Neural Architecture Search](#neural-architecture-search) +- [Architecture Diagram](#architecture-diagram) +- [Design & Implementation Improvement Analysis](#design--implementation-improvement-analysis) + +--- + +## Repository Overview + +**tinyml-tensorlab** is Texas Instruments' MCU AI Toolchain -- a monorepo containing four sub-repositories that together provide an end-to-end pipeline for training, quantizing, and compiling tiny neural networks for deployment on TI microcontrollers (C2000, MSPM0, CC27xx families). + +| Property | Value | +|---|---| +| Version | 1.2.0 (November 2025) | +| License | BSD 3-Clause | +| Python | 3.10 required | +| ML Framework | PyTorch 2.7.1 | +| Total Python files | ~156 | + +--- + +## Sub-Repository Responsibilities + +| Sub-Repo | Package Name | Role | File Count | +|---|---|---|---| +| `tinyml-modelmaker` | `tinyml_modelmaker` | **Orchestrator** -- YAML-driven pipeline stitching data loading, training, and compilation | ~49 .py files | +| `tinyml-tinyverse` | `tinyml_tinyverse` | **Training Engine** -- Model definitions, datasets, transforms, training scripts, data augmenters | ~65 .py files | +| `tinyml-modeloptimization` | `tinyml_torchmodelopt` | **Optimization** -- Quantization (PTQ/QAT), Neural Architecture Search (NAS), model surgery | ~42 .py files | +| `tinyml-modelzoo` | *(documentation only)* | **Catalog** -- Benchmark results, model catalog, resource usage tables | README + graphs | + +### Dependency Direction + +``` +tinyml-modelmaker ---> tinyml-tinyverse + | | + +----> tinyml-modeloptimization <----+ +``` + +`tinyml-modelmaker` is the top-level orchestrator. It depends on both `tinyml-tinyverse` (for training scripts and model definitions) and `tinyml-modeloptimization` (for quantization). `tinyml-tinyverse` also depends on `tinyml-modeloptimization` for quantization-aware training. + +--- + +## Supported Tasks & Target Devices + +### Tasks + +| Task Category | Task Types | +|---|---| +| Time Series Classification | Arc fault, motor fault, blower imbalance, PIR detection, generic | +| Time Series Regression | Generic | +| Time Series Anomaly Detection | Autoencoder-based | +| Time Series Forecasting | Generic | +| Image Classification | Experimental (MNIST/Fashion-MNIST) | + +### Target Devices + +| Family | Devices | +|---|---| +| C2000 | F280013, F280015, F28003, F28004, F2837, F28P55, F28P65, F29H85 | +| ARM-based | AM263, MSPM0G3507, MSPM0G5187 | +| Connectivity | CC2755 | + +### Compilation Targets + +| Target Name | Platform | +|---|---| +| `m0_soft_int_in_int_out` | Optimized libraries on Arm M0-core | +| `m0_hard_int_in_int_out` | Arm M0-core + TINPU | +| `c28_soft_int_in_int_out` | Optimized libraries on TI C28x DSP | +| `c28_hard_int_in_int_out` | TI C28x DSP + TINPU | +| `c29_soft_int_in_int_out` | Optimized libraries on TI C29x DSP | +| `m33_soft_int_in_int_out` | Optimized libraries on Arm M33-core | +| `m33_cde_int_in_int_out` | Arm M33-core + CDE custom instructions | + +--- + +## Entry Points + +| Method | Command | +|---|---| +| CLI | `python tinyml_modelmaker/run_tinyml_modelmaker.py config.yaml` | +| Shell | `run_tinyml_modelmaker.sh config.yaml` | +| Python API | `import tinyml_modelmaker; tinyml_modelmaker.get_set_go(config)` | +| GUI | Edge AI Studio Model Composer (uses `tinyml-mlbackend` Docker wrapper) | + +--- + +## Pipeline Flow + +The entire pipeline is driven by a single YAML configuration file: + +``` +config.yaml + | + v +run_tinyml_modelmaker.py::main(config) + | + |--> resolve target_module ("timeseries" or "vision") + | via ai_modules.get_target_module() + | + |--> load and layer configuration: + | defaults -> model_description -> dataset_preset + | -> feature_extraction_preset -> compilation_preset -> user YAML + | + |--> ModelRunner(params) + | + |--> prepare() + | 1. download_all() -- fetch datasets / pretrained weights + | 2. DatasetHandling.run() -- split data into train/val/test + | 3. ModelTraining() -- initialize training module + | 4. ModelCompilation() -- initialize compilation module + | + |--> run() + 1. model_training.run() -- train float model + optional QAT/PTQ + 2. package_trained_model() -- zip training artifacts + 3. model_compilation.run() -- compile ONNX -> binary via TI NNC + 4. package_compiled_model() -- zip compiled artifacts +``` + +### Key Source Files + +| File | Purpose | +|---|---| +| `tinyml-modelmaker/tinyml_modelmaker/__init__.py` | Exposes `get_set_go()`, task type mapping | +| `tinyml-modelmaker/tinyml_modelmaker/run_tinyml_modelmaker.py` | CLI entry point, `main(config)` function | +| `tinyml-modelmaker/tinyml_modelmaker/ai_modules/__init__.py` | `get_target_module()` -- routes to timeseries or vision | +| `tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/runner.py` | `ModelRunner` class -- the main pipeline orchestrator | +| `tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/params.py` | Default parameter definitions | +| `tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/constants.py` | Task types, device constants | +| `tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/descriptions.py` | Model catalog, device presets, feature extraction presets | + +--- + +## Configuration System + +### ConfigDict + +The central configuration object is `ConfigDict` (`tinyml-modelmaker/tinyml_modelmaker/utils/config_dict.py`), a `dict` subclass that supports attribute-style access: + +```python +params = ConfigDict(dict(training=dict(model_name='TimeSeries_Generic_4k_t'))) +print(params.training.model_name) # 'TimeSeries_Generic_4k_t' +``` + +Key features: +- Deep-merge via `update()` -- nested dicts are merged recursively, not replaced +- YAML file loading via constructor: `ConfigDict('config.yaml')` +- Include file support via `include_files` key + +### Configuration Layering + +Configs are applied in priority order (later overrides earlier): + +1. **Default params** (`params.py:init_params()`) +2. **Model description** (from `descriptions.py` catalog) +3. **Dataset preset** (predefined dataset configurations) +4. **Feature extraction preset** (FFT, raw, windowing configs) +5. **Compilation preset** (device-specific compilation settings) +6. **User YAML config** (the file passed on the command line) + +--- + +## Model Architecture System + +Models are defined in `tinyml-tinyverse/tinyml_tinyverse/common/models/` using a declarative `model_spec` pattern. + +### Layer Factories (`tinynn.py`) + +Low-level factory functions that return `(layer, output_tensor_size)` tuples: + +- `ConvLayer` / `ConvBNReLULayer` -- Conv2d with optional BatchNorm + ReLU +- `LinearLayer` -- Fully connected layer +- `MaxPoolLayer` / `AvgPoolLayer` / `AdaptiveAvgPoolLayer` +- `BatchNormLayer`, `ReLULayer`, `ReshapeLayer`, `IdentityLayer` + +### Model Classes + +- `generic_classification_models.py` -- CNN_TS_GEN_BASE_{1K,4K,6K,13K} models +- `generic_regression_models.py` -- Regression variants +- `generic_autoencoder_models.py` -- Autoencoder-based anomaly detection +- `generic_forecasting_models.py` -- Forecasting models +- `generic_feature_extraction_models.py` -- Feature extraction networks +- `generic_image_models.py` -- Image classification models + +Each model class produces a `model_spec` dictionary describing the architecture declaratively. The `NeuralNetworkWithPreprocess` wrapper combines preprocessing transforms with the neural network. + +### Available Models + +| Model | Parameters | Use Case | +|---|---|---| +| TimeSeries_Generic_1k_t | ~972 | Smallest, lowest resource usage | +| TimeSeries_Generic_4k_t | ~3,684 | Balanced efficiency | +| TimeSeries_Generic_6k_t | ~5,188 | Good accuracy/size tradeoff | +| TimeSeries_Generic_13k_t | ~12,980 | Highest accuracy | +| ArcFault_model_{200,300,700,1400}_t | 296-1,648 | Specialized arc fault (GUI) | +| MotorFault_model_{1,2,3}_t | 588-2,808 | Specialized motor fault (GUI) | + +--- + +## Quantization System + +Located in `tinyml-modeloptimization/torchmodelopt/tinyml_torchmodelopt/quantization/`. + +### Architecture + +``` +quantization/ + common.py -- TinyMLQuantizationVersion, TinyMLQConfigType + base/fx/ -- TinyMLQuantFxBaseModule (PyTorch FX graph-based) + generic/ -- GenericTinyMLQATFxModule, GenericTinyMLPTQFxModule + tinpu/ -- TINPUTinyMLQATFxModule, TINPUTinyMLPTQFxModule +``` + +### Quantization Modes + +| Version | Constant | Description | +|---|---|---| +| No quantization | `NO_QUANTIZATION = 0` | Float32 model only | +| Generic | `QUANTIZATION_GENERIC = 1` | Standard quantization | +| TINPU | `QUANTIZATION_TINPU = 2` | Optimized for TI NPU hardware | + +### Supported Bit-widths + +| Weight Bits | Activation Bits | Scheme | +|---|---|---| +| 8 | 8 | Per-channel symmetric (weights), per-tensor symmetric (activations), power2 scale | +| 4 | 4 or 8 | Per-channel symmetric, soft_sigmoid rounding | +| 2 | 8 | Per-channel symmetric, ternary weights {-1, 0, 1}, soft_tanh rounding | + +### Methods + +- **QAT** (Quantization-Aware Training) -- Fake quantization nodes inserted during training +- **PTQ** (Post-Training Quantization) -- Calibration-based quantization after training + +--- + +## Neural Architecture Search + +Located in `tinyml-modeloptimization/torchmodelopt/tinyml_torchmodelopt/nas/`. + +Uses a DARTS-style differentiable architecture search approach: + +| File | Purpose | +|---|---| +| `train_cnn_search.py` | Entry point: `search_and_get_model()` | +| `architect.py` | Architecture parameter optimizer | +| `model_search_cnn.py` | Search space definition | +| `model.py` | Network construction from genotype | +| `operations.py` | Primitive operations (conv, pool, etc.) | +| `genotypes.py` | Architecture genotype definitions | + +Generates TINPU-compatible models directly from user datasets. + +--- + +## Architecture Diagram + +```mermaid +graph TB + subgraph "User Interface" + CLI["CLI: run_tinyml_modelmaker.py"] + API["Python API: get_set_go(config)"] + YAML["YAML Config Files"] + GUI["Edge AI Studio
Model Composer GUI"] + end + + subgraph "tinyml-modelmaker (Orchestrator)" + MAIN["main() / get_set_go()"] + AIMOD["ai_modules/__init__.py
get_target_module()"] + + subgraph "ai_modules/timeseries" + TS_RUNNER["runner.py
ModelRunner"] + TS_PARAMS["params.py
init_params()"] + TS_CONST["constants.py
Task types, devices"] + TS_DESC["descriptions.py
Model catalog"] + TS_DS["datasets/
DatasetHandling"] + TS_TRAIN["training/
ModelTraining"] + TS_COMPILE["compilation/
ModelCompilation"] + end + + subgraph "ai_modules/vision" + VIS_RUNNER["runner.py
ModelRunner"] + end + + subgraph "utils" + CFGDICT["ConfigDict
(dict + attr access)"] + MISC["misc_utils, download_utils"] + end + end + + subgraph "tinyml-tinyverse (Training Engine)" + subgraph "references/" + REF_CLS["timeseries_classification/
train.py, test_onnx.py"] + REF_REG["timeseries_regression/
train.py"] + REF_AD["timeseries_anomalydetection/
train.py"] + REF_FC["timeseries_forecasting/
train.py"] + REF_IMG["image_classification/
train.py"] + REF_COMP["common/compilation.py"] + end + + subgraph "common/" + MODELS["models/
tinynn.py layer factories
generic_*_models.py"] + DATASETS["datasets/
GenericTSDataset
ImageDataset"] + TRANSFORMS["transforms/
haar, hadamard, basic"] + AUGMENTERS["augmenters/
noise, drift, crop, warp..."] + TV_UTILS["utils/
misc, data, gof, load_weights"] + end + end + + subgraph "tinyml-modeloptimization (Quantization & NAS)" + subgraph "quantization/" + Q_BASE["base/fx/
TinyMLQuantFxBaseModule"] + Q_GENERIC["generic/
QAT & PTQ Fx Modules"] + Q_TINPU["tinpu/
TINPU QAT & PTQ Fx Modules"] + Q_COMMON["common.py
TinyMLQuantizationVersion
TinyMLQConfigType"] + end + + subgraph "nas/" + NAS_SEARCH["train_cnn_search.py
search_and_get_model()"] + NAS_ARCH["architect.py"] + NAS_MODEL["model.py, model_search_cnn.py"] + NAS_OPS["operations.py, genotypes.py"] + end + + subgraph "surgery/" + SURGERY["surgery.py
Module replacement"] + REPLACER["replacer.py"] + end + end + + subgraph "tinyml-modelzoo (Catalog)" + ZOO_README["README.md
Model benchmarks"] + ZOO_GRAPHS["graphs/
Performance plots"] + end + + subgraph "External / TI Tools" + NNC["TI MCU Neural Network
Compiler (ti_mcu_nnc)"] + C2000["C2000 Codegen Tools"] + ARM_CGT["TI Arm CGT Clang"] + CWARE["C2000Ware / MSPM0 SDK"] + end + + CLI --> MAIN + API --> MAIN + YAML --> CLI + GUI -.->|"Docker wrapper
(tinyml-mlbackend)"| MAIN + + MAIN --> AIMOD + AIMOD --> TS_RUNNER + AIMOD --> VIS_RUNNER + TS_RUNNER --> TS_PARAMS + TS_RUNNER --> TS_DESC + TS_RUNNER --> TS_DS + TS_RUNNER --> TS_TRAIN + TS_RUNNER --> TS_COMPILE + TS_PARAMS --> CFGDICT + + TS_TRAIN --> REF_CLS + TS_TRAIN --> REF_REG + TS_TRAIN --> REF_AD + TS_TRAIN --> REF_FC + VIS_RUNNER --> REF_IMG + + REF_CLS --> MODELS + REF_CLS --> DATASETS + REF_CLS --> AUGMENTERS + REF_CLS --> TRANSFORMS + REF_CLS --> NAS_SEARCH + + REF_CLS --> Q_GENERIC + REF_CLS --> Q_TINPU + Q_GENERIC --> Q_BASE + Q_TINPU --> Q_BASE + Q_BASE --> Q_COMMON + + TS_COMPILE --> REF_COMP + REF_COMP --> NNC + + NNC --> C2000 + NNC --> ARM_CGT + NNC --> CWARE + + classDef orchestrator fill:#4a90d9,stroke:#333,color:#fff + classDef engine fill:#7cb342,stroke:#333,color:#fff + classDef optim fill:#ff8f00,stroke:#333,color:#fff + classDef external fill:#78909c,stroke:#333,color:#fff + classDef user fill:#ab47bc,stroke:#333,color:#fff + classDef zoo fill:#26a69a,stroke:#333,color:#fff + + class MAIN,AIMOD,TS_RUNNER,TS_PARAMS,TS_CONST,TS_DESC,TS_DS,TS_TRAIN,TS_COMPILE,VIS_RUNNER,CFGDICT,MISC orchestrator + class REF_CLS,REF_REG,REF_AD,REF_FC,REF_IMG,REF_COMP,MODELS,DATASETS,TRANSFORMS,AUGMENTERS,TV_UTILS engine + class Q_BASE,Q_GENERIC,Q_TINPU,Q_COMMON,NAS_SEARCH,NAS_ARCH,NAS_MODEL,NAS_OPS,SURGERY,REPLACER optim + class NNC,C2000,ARM_CGT,CWARE external + class CLI,API,YAML,GUI user + class ZOO_README,ZOO_GRAPHS zoo +``` + +### Color Legend + +| Color | Component | +|---|---| +| Purple | User interface entry points | +| Blue | tinyml-modelmaker (orchestrator) | +| Green | tinyml-tinyverse (training engine) | +| Orange | tinyml-modeloptimization (quantization & NAS) | +| Teal | tinyml-modelzoo (catalog) | +| Grey | External TI tools | + +--- + +## Design & Implementation Improvement Analysis + +### Critical Issues + +#### 1. No Test Suite + +- **Finding**: Zero test directories exist anywhere in the repository +- **Impact**: No automated verification of correctness; regressions can ship silently +- **Recommendation**: Add `pytest`-based test suites for each package: + - Unit tests for `ConfigDict`, `model_spec` generation, quantization config creation + - Integration tests for the full pipeline (mock the NNC compiler) + - ONNX export validation tests + - Add CI via GitHub Actions +- **Files affected**: All packages (new `tests/` directories needed) + +#### 2. Monolithic Descriptions File + +- **Finding**: `tinyml-modelmaker/.../timeseries/descriptions.py` is a massive file containing hardcoded model descriptions, device presets, feature extraction presets, compilation presets, GUI metadata, tooltip text, and help strings -- all in one file +- **Impact**: Adding a new model or device requires modifying this monolithic file; high merge conflict risk +- **Recommendation**: + - Split into per-concern files: `model_descriptions.py`, `device_presets.py`, `feature_extraction_presets.py` + - Consider data-driven approach using YAML files for catalogs instead of Python dicts + - Use a registry pattern with decorators for model registration +- **File**: `tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/descriptions.py` + +#### 3. Overloaded ModelRunner Constructor + +- **Finding**: `ModelRunner.__init__()` (lines 57-124 in `runner.py`) contains ~100 lines of complex conditional path resolution logic +- **Impact**: Hard to understand, test, or modify path logic +- **Recommendation**: Extract path resolution into a dedicated `PathResolver` class or utility function +- **File**: `tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/runner.py` + +--- + +### Architectural Improvements + +#### 4. Tight Coupling Between Sub-Repositories + +- **Finding**: `tinyml-modelmaker` imports directly from `tinyml_tinyverse` and `tinyml_torchmodelopt` at multiple levels. Training modules reach deep into tinyverse internals +- **Impact**: Cannot test or evolve packages independently +- **Recommendation**: Define clear interfaces between packages. Modelmaker should interact with tinyverse through a defined training API contract +- **Files**: `tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/training/tinyml_tinyverse/*.py` + +#### 5. Duplicated Code Across Task Types + +- **Finding**: `timeseries_classification.py`, `timeseries_regression.py`, `timeseries_anomalydetection.py`, `timeseries_forecasting.py` in modelmaker's training module share very similar structure (template_model_description, _model_descriptions dict, ModelTraining class) +- **Impact**: Changes to shared behavior must be replicated across 4+ files +- **Recommendation**: Create a base `TimeseriesModelTraining` class with common logic; task-specific subclasses override only what differs +- **Files**: `tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/training/tinyml_tinyverse/timeseries_*.py` + +#### 6. No Abstract Base Classes or Protocols + +- **Finding**: Components like `ModelTraining`, `ModelCompilation`, `ModelRunner` have implicit interfaces but no formal ABC/Protocol definitions +- **Impact**: No compile-time or linting enforcement of interface contracts +- **Recommendation**: Define `Protocol` classes (or ABCs) for `ModelTraining`, `ModelCompilation`, `DatasetHandler` + +--- + +### Code Quality Improvements + +#### 7. Magic Strings + +- **Finding**: Many places use raw strings like `'tinyml_tinyverse'`, `'GenericTSDataset'`, `'train'`/`'val'`/`'test'` instead of constants +- **Impact**: Typos cause silent failures +- **Recommendation**: Use enums consistently; convert `TinyMLQuantizationVersion` to a proper `enum.IntEnum` +- **Files**: Throughout all packages + +#### 8. Commented-Out Code + +- **Finding**: Significant amounts of commented-out code (e.g., `setup.py` has 30+ lines commented, `runner.py` has multiple commented blocks) +- **Impact**: Clutters codebase; unclear what is active +- **Recommendation**: Remove all commented-out code; use version control history if needed +- **Files**: `tinyml-modelmaker/setup.py`, `tinyml-modelmaker/.../runner.py`, `tinyml-modelmaker/.../tinyml_benchmark.py` + +#### 9. Inconsistent Error Handling + +- **Finding**: Mix of `assert` statements (disabled with `-O`), bare `print()` for errors, and occasional `raise`. Compilation's `run()` returns a boolean `exit_flag` instead of raising +- **Impact**: Errors can be silently swallowed; inconsistent error reporting +- **Recommendation**: Create exception hierarchy (`TinyMLError`, `TrainingError`, `CompilationError`). Use `logging` instead of `print()` +- **Files**: Throughout all packages + +#### 10. Incorrect `@classmethod` Usage + +- **Finding**: `ModelRunner.init_params()` and `ModelCompilation.init_params()` use `@classmethod` but name first parameter `self` instead of `cls` +- **Impact**: Misleading to developers; works accidentally +- **Recommendation**: Use `@staticmethod` (these methods don't use the class) or rename to `cls` +- **Files**: `tinyml-modelmaker/.../runner.py`, `tinyml-modelmaker/.../tinyml_benchmark.py` + +#### 11. No Type Annotations + +- **Finding**: The codebase has virtually no type annotations +- **Impact**: Degraded IDE support; no `mypy` checking possible +- **Recommendation**: Add type annotations progressively, starting with public APIs + +#### 12. Print Statements Instead of Logging + +- **Finding**: Uses `print()` for all output throughout the codebase +- **Impact**: Cannot control verbosity; no structured logging +- **Recommendation**: Replace `print()` with Python `logging` module. Some tinyverse files already use `getLogger()` but inconsistently +- **Files**: Throughout all packages + +--- + +### Summary Table + +| # | Category | Issue | Severity | Effort | +|---|---|---|---|---| +| 1 | Testing | No test suite | Critical | High | +| 2 | Architecture | Monolithic descriptions file | Critical | Medium | +| 3 | Architecture | Overloaded constructor | High | Low | +| 4 | Architecture | Tight cross-repo coupling | High | High | +| 5 | Architecture | Duplicated task-type code | Medium | Medium | +| 6 | Architecture | No ABCs/Protocols | Medium | Low | +| 7 | Code Quality | Magic strings | Medium | Low | +| 8 | Code Quality | Commented-out code | Low | Low | +| 9 | Code Quality | Inconsistent error handling | High | Medium | +| 10 | Code Quality | Wrong @classmethod usage | Low | Low | +| 11 | Code Quality | No type annotations | Medium | High | +| 12 | Code Quality | print() instead of logging | Medium | Medium | From faab0376e70c1ed8b67e3ed12550bcddd50ccfc2 Mon Sep 17 00:00:00 2001 From: M Platypus Date: Sun, 19 Jul 2026 14:19:20 -0300 Subject: [PATCH 07/10] fix: address 5 CodeRabbit findings in pr/code-quality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dataset_utils.py: validate scalar split_factor in (0, 1) range; use split_factors list (not raw split_factor param) in post-normalization checks to avoid TypeError on float inputs - misc_utils.py: guard input_data_path before os.path.basename (prevents TypeError when both dataset_name and input_data_path are unset); use absolutized projects_path as base for model_packaged_path; fix archive name for flat run_names (filter(None,...) removes empty leading segment so 'myrun' → myrun.zip instead of _myrun.zip) - vision/runner.py: use v.get() for inference_time_us/sram/flash to avoid KeyError when a device entry is missing a metric Co-Authored-By: Claude Sonnet 4.6 --- .../common/datasets/dataset_utils.py | 28 +++++++++---------- .../ai_modules/vision/runner.py | 6 ++-- .../tinyml_modelmaker/utils/misc_utils.py | 8 ++++-- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/common/datasets/dataset_utils.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/common/datasets/dataset_utils.py index e48f32da..6f0f021d 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/common/datasets/dataset_utils.py +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/common/datasets/dataset_utils.py @@ -93,8 +93,8 @@ def create_inter_file_split(file_list: str, split_list_files: tuple, split_facto number_of_splits = len(split_list_files) split_factors = [] if type(split_factor) == float: - if split_factor >= 1.0: - raise ValueError("split_factor should be less than 1") + if not (0.0 < split_factor < 1.0): + raise ValueError("split_factor must be in the range (0.0, 1.0)") # The default split factor is the fraction for training set. split_factors.append(split_factor) # The remainder of the set will be equally split between val or val/test @@ -108,11 +108,11 @@ def create_inter_file_split(file_list: str, split_list_files: tuple, split_facto split_factors.extend(split_factor) remainder = 1 - sum(split_factor) - if number_of_splits > len(split_factor): - remainder_fraction = remainder / (number_of_splits - len(split_factor)) - [split_factors.append(remainder_fraction) for _ in range(number_of_splits - len(split_factor))] - if len(split_factor) != len(split_list_files): - raise ValueError(f"Number of split files: {len(split_list_files)} should be same as length of split factors: {len(split_factor)}") + if number_of_splits > len(split_factors): + remainder_fraction = remainder / (number_of_splits - len(split_factors)) + [split_factors.append(remainder_fraction) for _ in range(number_of_splits - len(split_factors))] + if len(split_factors) != len(split_list_files): + raise ValueError(f"Number of split files: {len(split_list_files)} should be same as length of split factors: {len(split_factors)}") with open(file_list) as fp: list_of_files = [x.strip() for x in fp.readlines()] # Contains the list of files @@ -153,8 +153,8 @@ def create_intra_file_split(file_list: str, split_list_files: tuple, split_facto number_of_splits = len(split_list_files) split_factors = [] if type(split_factor) == float: - if split_factor >= 1.0: - raise ValueError("split_factor should be less than 1") + if not (0.0 < split_factor < 1.0): + raise ValueError("split_factor must be in the range (0.0, 1.0)") # The default split factor is the fraction for training set. split_factors.append(split_factor) # The remainder of the set will be equally split between val or val/test @@ -168,11 +168,11 @@ def create_intra_file_split(file_list: str, split_list_files: tuple, split_facto split_factors.extend(split_factor) remainder = 1 - sum(split_factor) - if number_of_splits > len(split_factor): - remainder_fraction = remainder / (number_of_splits - len(split_factor)) - [split_factors.append(remainder_fraction) for _ in range(number_of_splits - len(split_factor))] - if len(split_factor) != len(split_list_files): - raise ValueError(f"Number of split files: {len(split_list_files)} should be same as length of split factors: {len(split_factor)}") + if number_of_splits > len(split_factors): + remainder_fraction = remainder / (number_of_splits - len(split_factors)) + [split_factors.append(remainder_fraction) for _ in range(number_of_splits - len(split_factors))] + if len(split_factors) != len(split_list_files): + raise ValueError(f"Number of split files: {len(split_list_files)} should be same as length of split factors: {len(split_factors)}") with open(file_list) as fp: # list_of_files = [os.path.join(os.path.dirname(os.path.dirname(file_list)), data_dir, x.strip()) for x in fp.readlines()] # Contains the list of files diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/runner.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/runner.py index ea372ff9..3f731817 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/runner.py +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/runner.py @@ -64,9 +64,9 @@ def __init__(self, *args, verbose=True, **kwargs): utils.misc_utils.resolve_paths(self.params, constants.TARGET_DEVICES_ALL) if self.params.common.target_device in self.params.training.target_devices: - inference_time_us_list = {k:v['inference_time_us'] for k,v in self.params.training.target_devices.items()} - sram_usage_list = {k: v['sram'] for k, v in self.params.training.target_devices.items()} - flash_usage_list = {k: v['flash'] for k, v in self.params.training.target_devices.items()} + inference_time_us_list = {k: v.get('inference_time_us') for k, v in self.params.training.target_devices.items()} + sram_usage_list = {k: v.get('sram') for k, v in self.params.training.target_devices.items()} + flash_usage_list = {k: v.get('flash') for k, v in self.params.training.target_devices.items()} logger.info('---------------------------------------------------------------------') logger.info(f'Run Name: {self.params.common.run_name}') logger.info(f'- Model: {self.params.training.model_name}') diff --git a/tinyml-modelmaker/tinyml_modelmaker/utils/misc_utils.py b/tinyml-modelmaker/tinyml_modelmaker/utils/misc_utils.py index 5e454e97..8446b5ce 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/utils/misc_utils.py +++ b/tinyml-modelmaker/tinyml_modelmaker/utils/misc_utils.py @@ -96,6 +96,8 @@ def resolve_paths(params, target_devices_all): # --- dataset name fallback --- if not params.dataset.dataset_name: + if not params.dataset.input_data_path: + raise ValueError('dataset.dataset_name or dataset.input_data_path must be set') params.dataset.dataset_name = os.path.splitext( os.path.basename(params.dataset.input_data_path))[0] @@ -120,8 +122,8 @@ def resolve_paths(params, target_devices_all): params.training.training_path_quantization = absolute_path( os.path.join(params.training.train_output_path, 'training_quantization')) params.training.model_packaged_path = os.path.join( - params.training.train_output_path, - '_'.join(os.path.split(params.common.run_name)) + '.zip') + params.common.projects_path, + '_'.join(filter(None, os.path.split(params.common.run_name))) + '.zip') else: # default: nested structure under projects_path/dataset_name/run/run_name params.common.projects_path = absolute_path(params.common.projects_path) @@ -137,7 +139,7 @@ def resolve_paths(params, target_devices_all): os.path.join(params.common.project_run_path, 'training', 'quantization')) params.training.model_packaged_path = os.path.join( params.training.training_path, - '_'.join(os.path.split(params.common.run_name)) + '.zip') + '_'.join(filter(None, os.path.split(params.common.run_name))) + '.zip') # --- target device validation --- if params.common.target_device not in target_devices_all: From 477996ee440487fa0df2a7486e51510ae99c8044 Mon Sep 17 00:00:00 2001 From: M Platypus Date: Sun, 19 Jul 2026 14:22:58 -0300 Subject: [PATCH 08/10] docs: update ARCHITECTURE.md to reflect PR stack improvements Sections 1 (No Test Suite) and 6 (No Abstract Base Classes or Protocols) now accurately reflect what the PR stack addressed: the protocols module in ai_modules/protocols.py and the test_protocols.py test suite. Co-Authored-By: Claude Sonnet 4.6 --- ARCHITECTURE.md | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3047982c..e9838e62 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -423,16 +423,12 @@ graph TB ### Critical Issues -#### 1. No Test Suite +#### 1. Test Suite (Partially Addressed) -- **Finding**: Zero test directories exist anywhere in the repository -- **Impact**: No automated verification of correctness; regressions can ship silently -- **Recommendation**: Add `pytest`-based test suites for each package: - - Unit tests for `ConfigDict`, `model_spec` generation, quantization config creation - - Integration tests for the full pipeline (mock the NNC compiler) - - ONNX export validation tests - - Add CI via GitHub Actions -- **Files affected**: All packages (new `tests/` directories needed) +- **Finding**: Zero test directories existed anywhere in the repository +- **Status**: A `pytest`-based test suite has been added at `tinyml-modelmaker/tests/test_protocols.py`, covering Protocol conformance for all component interfaces (`Runner`, `Trainer`, `Compiler`, `DatasetHandler`, `LifecycleComponent`) +- **Remaining gaps**: No unit tests for `ConfigDict`, `model_spec` generation, or quantization config creation; no integration tests for the full pipeline; no ONNX export validation tests; no CI via GitHub Actions +- **Recommendation**: Expand test coverage to other packages and add CI integration #### 2. Monolithic Descriptions File @@ -469,11 +465,11 @@ graph TB - **Recommendation**: Create a base `TimeseriesModelTraining` class with common logic; task-specific subclasses override only what differs - **Files**: `tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/training/tinyml_tinyverse/timeseries_*.py` -#### 6. No Abstract Base Classes or Protocols +#### 6. Abstract Base Classes or Protocols (Addressed) -- **Finding**: Components like `ModelTraining`, `ModelCompilation`, `ModelRunner` have implicit interfaces but no formal ABC/Protocol definitions -- **Impact**: No compile-time or linting enforcement of interface contracts -- **Recommendation**: Define `Protocol` classes (or ABCs) for `ModelTraining`, `ModelCompilation`, `DatasetHandler` +- **Finding**: Components like `ModelTraining`, `ModelCompilation`, `ModelRunner` had implicit interfaces but no formal ABC/Protocol definitions +- **Status**: `typing.Protocol` classes have been added in `tinyml-modelmaker/tinyml_modelmaker/ai_modules/protocols.py`: `LifecycleComponent`, `DatasetHandler`, `Trainer`, `Compiler`, and `Runner` — all `@runtime_checkable`. Protocol conformance is verified by `tinyml-modelmaker/tests/test_protocols.py` +- **Remaining**: Protocols cover tinyml-modelmaker components only; tinyml-tinyverse and tinyml-torchmodelopt still lack formal interface contracts --- From 1dd7ca3a68bc2fb54810dd12c3c71f7fbdfe8f2c Mon Sep 17 00:00:00 2001 From: M Platypus Date: Mon, 20 Jul 2026 00:29:52 -0300 Subject: [PATCH 09/10] fix: restore TinyMLQuantizationVersion import dropped in resolve_paths refactor 0c1b23d moved path resolution out of ModelRunner and removed the tinyml_torchmodelopt.quantization import along with it, but two uses remain in run() at the packaging step. Any run that reaches model packaging raises NameError: name 'TinyMLQuantizationVersion' is not defined, which makes compilation fail on every platform. Restore the import, matching how params.py, misc_utils.py and timeseries_base.py already reference it. Verified: compiling an exported ONNX for F28P55 now completes and produces artifacts/mod.a, tvmgen_default.h and the deployment zip. All 20 Python files changed on this branch import cleanly. --- .../tinyml_modelmaker/ai_modules/timeseries/runner.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/runner.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/runner.py index 780c9ce5..64fbe1c4 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/runner.py +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/timeseries/runner.py @@ -36,6 +36,8 @@ import yaml +from tinyml_torchmodelopt.quantization import TinyMLQuantizationVersion + from ... import utils from . import constants, datasets, descriptions from .params import init_params From 6a7fb1e1ceaeea672b3f28fe10fb8f302266a8da Mon Sep 17 00:00:00 2001 From: M Platypus Date: Mon, 20 Jul 2026 06:55:29 -0300 Subject: [PATCH 10/10] fix: restore TinyMLQuantizationVersion import in the vision runner too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Companion to 3607b15. The resolve_paths refactor in 0c1b23d dropped this import from both ai_modules/timeseries/runner.py and ai_modules/vision/runner.py; only the timeseries one was restored. vision/runner.py still raises NameError at lines 154 and 207 when a run reaches model packaging. Found by running pyflakes across every file this branch touches — the earlier import-only check could not catch it, since the name is referenced inside run() and never evaluated at import time. --- tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/runner.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/runner.py b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/runner.py index 3f731817..52c6473d 100644 --- a/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/runner.py +++ b/tinyml-modelmaker/tinyml_modelmaker/ai_modules/vision/runner.py @@ -36,6 +36,8 @@ import yaml +from tinyml_torchmodelopt.quantization import TinyMLQuantizationVersion + from ... import utils from . import constants, datasets, descriptions from .params import init_params