dragon-ml-toolbox 2.3.0__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dragon-ml-toolbox
3
- Version: 2.3.0
4
- Summary: A collection of tools for data science and machine learning projects
3
+ Version: 3.0.0
4
+ Summary: A collection of tools for data science and machine learning projects.
5
5
  Author-email: Karl Loza <luigiloza@gmail.com>
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/DrAg0n-BoRn/ML_tools
@@ -37,9 +37,11 @@ Requires-Dist: Pillow
37
37
  Provides-Extra: pytorch
38
38
  Requires-Dist: torch; extra == "pytorch"
39
39
  Requires-Dist: torchvision; extra == "pytorch"
40
+ Provides-Extra: gui
41
+ Requires-Dist: FreeSimpleGUI>=5.2; extra == "gui"
40
42
  Dynamic: license-file
41
43
 
42
- # dragon-ml-tools
44
+ # dragon-ml-toolbox
43
45
 
44
46
  A collection of Python utilities for data science and machine learning, structured as a modular package for easy reuse and installation.
45
47
 
@@ -57,7 +59,7 @@ A collection of Python utilities for data science and machine learning, structur
57
59
  Install the latest stable release from PyPI:
58
60
 
59
61
  ```bash
60
- pip install dragon-ml-tools
62
+ pip install dragon-ml-toolbox
61
63
  ```
62
64
 
63
65
  ### Via GitHub (Editable)
@@ -77,16 +79,26 @@ Install from the conda-forge channel:
77
79
  ```bash
78
80
  conda install -c conda-forge dragon-ml-toolbox
79
81
  ```
80
- **Note:** This version is outdated or broken due to dependency incompatibilities.
82
+ **Note:** This version is outdated or broken due to dependency incompatibilities. Use PyPi instead.
81
83
 
82
84
  ## Optional dependencies
83
85
 
84
- **PyTorch**, which provides different builds depending on the **platform** and **hardware acceleration** (e.g., CUDA for NVIDIA GPUs on Linux/Windows, or MPS for Apple Silicon on macOS).
86
+ ### FreeSimpleGUI
87
+
88
+ Wrapper library used to build powerful GUIs. Requires the tkinter backend.
89
+
90
+ ```bash
91
+ pip install dragon-ml-toolbox[gui]
92
+ ```
93
+
94
+ ### PyTorch
95
+
96
+ Different builds available depending on the **platform** and **hardware acceleration** (e.g., CUDA for NVIDIA GPUs on Linux/Windows, or MPS for Apple Silicon on macOS).
85
97
 
86
98
  Install the default CPU-only version with
87
99
 
88
100
  ```bash
89
- pip install dragon-ml-tools[pytorch]
101
+ pip install dragon-ml-toolbox[pytorch]
90
102
  ```
91
103
 
92
104
  To make use of GPU acceleration use the official PyTorch installation instructions:
@@ -108,12 +120,17 @@ from ml_tools.logger import custom_logger
108
120
  data_exploration
109
121
  datasetmaster
110
122
  ensemble_learning
123
+ ETL_engineering
124
+ GUI_tools
111
125
  handle_excel
112
126
  logger
113
127
  MICE_imputation
128
+ ML_callbacks
129
+ ML_evaluation
130
+ ML_trainer
131
+ ML_tutorial
114
132
  PSO_optimization
115
- trainer
133
+ RNN_forecast
116
134
  utilities
117
135
  VIF_factor
118
- vision_helpers
119
136
  ```
@@ -0,0 +1,25 @@
1
+ dragon_ml_toolbox-3.0.0.dist-info/licenses/LICENSE,sha256=2uUFNy7D0TLgHim1K5s3DIJ4q_KvxEXVilnU20cWliY,1066
2
+ dragon_ml_toolbox-3.0.0.dist-info/licenses/LICENSE-THIRD-PARTY.md,sha256=6cfpIeQ6D4Mcs10nkogQrkVyq1T7i2qXjjNHFoUMOyE,1892
3
+ ml_tools/ETL_engineering.py,sha256=SRiloWhSpopS4ay8mzUu0H4e9-37Ox_jDHzODqsQ8pc,31642
4
+ ml_tools/GUI_tools.py,sha256=uFx6zIrQZzDPSTtOSHz8ptz-fxZiQz-lXHcrqwuYV_E,20385
5
+ ml_tools/MICE_imputation.py,sha256=ed-YeQkEAeHxTNkWIHs09T4YeYNF0aqAnrUTcdIEp9E,11372
6
+ ml_tools/ML_callbacks.py,sha256=gHZk-lyzAax6iEtG26zHuoobdAZCFJ6BmI6pWoXkOrw,13189
7
+ ml_tools/ML_evaluation.py,sha256=3xOqVXLJDhbioKZ922yxFnSuO4VDQ-HFzZyZZ1MskVM,10054
8
+ ml_tools/ML_trainer.py,sha256=zRs3crz_z4B285iJhmY7m4AFwnvvq4urOyl4zDuCLtA,14456
9
+ ml_tools/ML_tutorial.py,sha256=-9tJO9ISPxEjRINVaF_Bu7tiiJ2W3zznQ4gNlZeP1HQ,12238
10
+ ml_tools/PSO_optimization.py,sha256=RCvIFGyf28voo2mpbRKC6LfDzKslzY-aYoPwgv9F4Bg,25458
11
+ ml_tools/RNN_forecast.py,sha256=IZLcPs3by0Chei7ill_Grjxs7BBUnzau0Oavi3dWiyE,1886
12
+ ml_tools/VIF_factor.py,sha256=5GVAldH69Vkei3WRUZN1uPBMzGoOOeEOA-bgmZXbbUw,10301
13
+ ml_tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ ml_tools/_particle_swarm_optimization.py,sha256=b_eNNkA89Y40hj76KauivT8KLScH1B9wF2IXptOqkOw,22220
15
+ ml_tools/_pytorch_models.py,sha256=bpWZsrSwCvHJQkR6UfoPpElsMv9AvmiNErNHC8NYB_I,10132
16
+ ml_tools/data_exploration.py,sha256=Fzbz_DKZ7F2e3-JbahLqKr3aP6lt9aCK9rNOHvR7nlA,23665
17
+ ml_tools/datasetmaster.py,sha256=N-uwfzWnl_qnoAqjbfS98I1pVNra5u6rhKLdWbFIReA,30122
18
+ ml_tools/ensemble_learning.py,sha256=PPtBBLgLvaYOdY-MlcjXuxWWXf3JQavLNEysFgzjc_s,37470
19
+ ml_tools/handle_excel.py,sha256=lwds7rDLlGSCWiWGI7xNg-Z7kxAepogp0lstSFa0590,12949
20
+ ml_tools/logger.py,sha256=jC4Q2OqmDm8ZO9VpuZqBSWdXryqaJvLscqVJ6caNMOk,6009
21
+ ml_tools/utilities.py,sha256=opNR-ACH6BnLkWAKcb19ef5tFxfx22TI6E2o0RYwiGA,21021
22
+ dragon_ml_toolbox-3.0.0.dist-info/METADATA,sha256=nmhUu0bwN4z1letePaDzGIQlmDUaBQ32esqGB-OasU4,3273
23
+ dragon_ml_toolbox-3.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
+ dragon_ml_toolbox-3.0.0.dist-info/top_level.txt,sha256=wm-oxax3ciyez6VoO4zsFd-gSok2VipYXnbg3TH9PtU,9
25
+ dragon_ml_toolbox-3.0.0.dist-info/RECORD,,
@@ -3,17 +3,18 @@ import re
3
3
  from typing import Literal, Union, Optional, Any, Callable, List, Dict
4
4
  from .utilities import _script_info
5
5
  import pandas as pd
6
+ from .logger import _LOGGER
6
7
 
7
8
 
8
9
  __all__ = [
9
10
  "ColumnCleaner",
10
- "DataFrameCleaner"
11
+ "DataFrameCleaner",
11
12
  "TransformationRecipe",
12
13
  "DataProcessor",
13
14
  "KeywordDummifier",
14
15
  "NumberExtractor",
15
16
  "MultiNumberExtractor",
16
- "RatioCalculator"
17
+ "RatioCalculator",
17
18
  "CategoryMapper",
18
19
  "RegexMapper",
19
20
  "ValueBinner",
@@ -251,7 +252,7 @@ class DataProcessor:
251
252
  raise TypeError(f"Invalid 'transform' action for '{input_col_name}': {transform_action}")
252
253
 
253
254
  if not processed_columns:
254
- print("Warning: The transformation resulted in an empty DataFrame.")
255
+ _LOGGER.warning("The transformation resulted in an empty DataFrame.")
255
256
  return pl.DataFrame()
256
257
 
257
258
  return pl.DataFrame(processed_columns)
@@ -403,7 +404,7 @@ class NumberExtractor:
403
404
  if not isinstance(round_digits, int):
404
405
  raise TypeError("round_digits must be an integer.")
405
406
  if dtype == "int":
406
- print(f"Warning: 'round_digits' is specified but dtype is 'int'. Rounding will be ignored.")
407
+ _LOGGER.warning(f"'round_digits' is specified but dtype is 'int'. Rounding will be ignored.")
407
408
 
408
409
  self.regex_pattern = regex_pattern
409
410
  self.dtype = dtype
@@ -561,9 +562,9 @@ class RatioCalculator:
561
562
  denominator = groups.struct.field("group_2").cast(pl.Float64, strict=False)
562
563
 
563
564
  # Safely perform division, returning null if denominator is 0
564
- return pl.when(denominator != 0).then(
565
- numerator / denominator
566
- ).otherwise(None)
565
+ final_expr = pl.when(denominator != 0).then(numerator / denominator).otherwise(None)
566
+
567
+ return pl.select(final_expr).to_series()
567
568
 
568
569
 
569
570
  class CategoryMapper:
ml_tools/GUI_tools.py ADDED
@@ -0,0 +1,495 @@
1
+ import configparser
2
+ from pathlib import Path
3
+ from typing import Optional, Callable, Any
4
+ import traceback
5
+ import FreeSimpleGUI as sg
6
+ from functools import wraps
7
+ from typing import Any, Dict, Tuple, List
8
+ from .utilities import _script_info
9
+ import numpy as np
10
+ from .logger import _LOGGER
11
+
12
+
13
+ __all__ = [
14
+ "PathManager",
15
+ "ConfigManager",
16
+ "GUIFactory",
17
+ "catch_exceptions",
18
+ "prepare_feature_vector",
19
+ "update_target_fields"
20
+ ]
21
+
22
+
23
+ # --- Path Management ---
24
+ class PathManager:
25
+ """
26
+ Manages paths for a Python application, supporting both development mode and bundled mode via Briefcase.
27
+ """
28
+ def __init__(self, anchor_file: str):
29
+ """
30
+ Initializes the PathManager. The package name is automatically inferred
31
+ from the parent directory of the anchor file.
32
+
33
+ Args:
34
+ anchor_file (str): The absolute path to a file within the project's
35
+ package, typically `__file__` from a module inside
36
+ that package (paths.py).
37
+
38
+ Note:
39
+ This inference assumes that the anchor file's parent directory
40
+ has the same name as the package (e.g., `.../src/my_app/paths.py`).
41
+ This is a standard and recommended project structure.
42
+ """
43
+ resolved_anchor_path = Path(anchor_file).resolve()
44
+ self.package_name = resolved_anchor_path.parent.name
45
+ self._is_bundled, self._resource_path_func = self._check_bundle_status()
46
+
47
+ if self._is_bundled:
48
+ # In a Briefcase bundle, resource_path gives an absolute path
49
+ # to the resource directory.
50
+ self.package_root = self._resource_path_func(self.package_name, "") # type: ignore
51
+ else:
52
+ # In development mode, the package root is the directory
53
+ # containing the anchor file.
54
+ self.package_root = resolved_anchor_path.parent
55
+
56
+ def _check_bundle_status(self) -> tuple[bool, Optional[Callable]]:
57
+ """Checks if the app is running in a bundled environment."""
58
+ try:
59
+ # This is the function Briefcase provides in a bundled app
60
+ from briefcase.platforms.base import resource_path # type: ignore
61
+ return True, resource_path
62
+ except ImportError:
63
+ return False, None
64
+
65
+ def get_path(self, relative_path: str | Path) -> Path:
66
+ """
67
+ Gets the absolute path for a given resource file or directory
68
+ relative to the package root.
69
+
70
+ Args:
71
+ relative_path (str | Path): The path relative to the package root (e.g., 'helpers/icon.png').
72
+
73
+ Returns:
74
+ Path: The absolute path to the resource.
75
+ """
76
+ if self._is_bundled:
77
+ # Briefcase's resource_path handles resolving the path within the app bundle
78
+ return self._resource_path_func(self.package_name, str(relative_path)) # type: ignore
79
+ else:
80
+ # In dev mode, join package root with the relative path.
81
+ return self.package_root / relative_path
82
+
83
+
84
+ # --- Configuration Management ---
85
+ class _SectionProxy:
86
+ """A helper class to represent a section of the .ini file as an object."""
87
+ def __init__(self, parser: configparser.ConfigParser, section_name: str):
88
+ for option, value in parser.items(section_name):
89
+ setattr(self, option.lower(), self._process_value(value))
90
+
91
+ def _process_value(self, value_str: str) -> Any:
92
+ """Automatically converts string values to appropriate types."""
93
+ # Handle None
94
+ if value_str is None or value_str.lower() == 'none':
95
+ return None
96
+ # Handle Booleans
97
+ if value_str.lower() in ['true', 'yes', 'on']:
98
+ return True
99
+ if value_str.lower() in ['false', 'no', 'off']:
100
+ return False
101
+ # Handle Integers
102
+ try:
103
+ return int(value_str)
104
+ except ValueError:
105
+ pass
106
+ # Handle Floats
107
+ try:
108
+ return float(value_str)
109
+ except ValueError:
110
+ pass
111
+ # Handle 'width,height' tuples
112
+ if ',' in value_str:
113
+ try:
114
+ return tuple(map(int, value_str.split(",")))
115
+ except (ValueError, TypeError):
116
+ pass
117
+ # Fallback to the original string
118
+ return value_str
119
+
120
+ class ConfigManager:
121
+ """
122
+ Loads a .ini file and provides access to its values as object attributes.
123
+ Includes a method to generate a default configuration template.
124
+ """
125
+ def __init__(self, config_path: str | Path):
126
+ """
127
+ Initializes the ConfigManager and dynamically creates attributes
128
+ based on the .ini file's sections and options.
129
+ """
130
+ config_path = Path(config_path)
131
+ if not config_path.exists():
132
+ raise FileNotFoundError(f"Configuration file not found at: {config_path}")
133
+
134
+ parser = configparser.ConfigParser(comment_prefixes=('#', ';'), inline_comment_prefixes=('#', ';'))
135
+ parser.read(config_path)
136
+
137
+ for section in parser.sections():
138
+ setattr(self, section.lower(), _SectionProxy(parser, section))
139
+
140
+ @staticmethod
141
+ def generate_template(file_path: str | Path, force_overwrite: bool = False):
142
+ """
143
+ Generates a complete, commented .ini template file that works with the GUIFactory.
144
+
145
+ Args:
146
+ file_path (str | Path): The path where the .ini file will be saved.
147
+ force_overwrite (bool): If True, overwrites the file if it already exists.
148
+ """
149
+ path = Path(file_path)
150
+ if path.exists() and not force_overwrite:
151
+ _LOGGER.warning(f"Configuration file already exists at {path}. Aborting.")
152
+ return
153
+
154
+ config = configparser.ConfigParser()
155
+
156
+ config['General'] = {
157
+ '; The overall theme for the GUI. Find more at https://www.pysimplegui.org/en/latest/call%20reference/#themes-automatic-coloring-of-elements': '',
158
+ 'theme': 'LightGreen6',
159
+ '; Default font for the application.': '',
160
+ 'font_family': 'Helvetica',
161
+ '; Title of the main window.': '',
162
+ 'window_title': 'My Application',
163
+ '; Can the user resize the window? (true/false)': '',
164
+ 'resizable_window': 'false',
165
+ '; Optional minimum window size (width,height). Leave blank for no minimum.': '',
166
+ 'min_size': '800,600',
167
+ '; Optional maximum window size (width,height). Leave blank for no maximum.': '',
168
+ 'max_size': ''
169
+ }
170
+ config['Layout'] = {
171
+ '; Default size for continuous input boxes (width,height in characters).': '',
172
+ 'input_size_cont': '16,1',
173
+ '; Default size for combo/binary boxes (width,height in characters).': '',
174
+ 'input_size_binary': '14,1',
175
+ '; Default size for buttons (width,height in characters).': '',
176
+ 'button_size': '15,2'
177
+ }
178
+ config['Fonts'] = {
179
+ '; Font settings. Style can be "bold", "italic", "underline", or a combination.': '',
180
+ 'label_size': '11',
181
+ 'label_style': 'bold',
182
+ 'range_size': '9',
183
+ 'range_style': '',
184
+ 'button_size': '14',
185
+ 'button_style': 'bold',
186
+ 'frame_size': '14',
187
+ 'frame_style': ''
188
+ }
189
+ config['Colors'] = {
190
+ '; Use standard hex codes (e.g., #FFFFFF) or color names (e.g., white).': '',
191
+ '; Color for the text inside a disabled target/output box.': '',
192
+ 'target_text': '#0000D0',
193
+ '; Background color for a disabled target/output box.': '',
194
+ 'target_background': '#E0E0E0',
195
+ '; Color for the text on a button.': '',
196
+ 'button_text': '#FFFFFF',
197
+ '; Background color for a button.': '',
198
+ 'button_background': '#3c8a7e',
199
+ '; Background color when the mouse is over a button.': '',
200
+ 'button_background_hover': '#5499C7'
201
+ }
202
+ config['Meta'] = {
203
+ '; Optional application version, displayed in the window title.': '',
204
+ 'version': '1.0.0'
205
+ }
206
+
207
+ with open(path, 'w') as configfile:
208
+ config.write(configfile)
209
+ _LOGGER.info(f"Successfully generated config template at: '{path}'")
210
+
211
+
212
+ # --- GUI Factory ---
213
+ class GUIFactory:
214
+ """
215
+ Builds styled FreeSimpleGUI elements and layouts using a "building block"
216
+ approach, driven by a ConfigManager instance.
217
+ """
218
+ def __init__(self, config: ConfigManager):
219
+ """
220
+ Initializes the factory with a configuration object.
221
+ """
222
+ self.config = config
223
+ sg.theme(self.config.general.theme) # type: ignore
224
+ sg.set_options(font=(self.config.general.font_family, 12)) # type: ignore
225
+
226
+ # --- Atomic Element Generators ---
227
+ def make_button(self, text: str, key: str, **kwargs) -> sg.Button:
228
+ """
229
+ Creates a single, styled action button.
230
+
231
+ Args:
232
+ text (str): The text displayed on the button.
233
+ key (str): The key for the button element.
234
+ **kwargs: Override default styles or add other sg.Button parameters
235
+ (e.g., `tooltip='Click me'`, `disabled=True`).
236
+ """
237
+ cfg = self.config
238
+ font = (cfg.fonts.font_family, cfg.fonts.button_size, cfg.fonts.button_style) # type: ignore
239
+
240
+ style_args = {
241
+ "size": cfg.layout.button_size, # type: ignore
242
+ "font": font,
243
+ "button_color": (cfg.colors.button_text, cfg.colors.button_background), # type: ignore
244
+ "mouseover_colors": (cfg.colors.button_text, cfg.colors.button_background_hover), # type: ignore
245
+ "border_width": 0,
246
+ **kwargs
247
+ }
248
+ return sg.Button(text.title(), key=key, **style_args)
249
+
250
+ def make_frame(self, title: str, layout: List[List[sg.Element]], **kwargs) -> sg.Frame:
251
+ """
252
+ Creates a styled frame around a given layout.
253
+
254
+ Args:
255
+ title (str): The title displayed on the frame's border.
256
+ layout (list): The layout to enclose within the frame.
257
+ **kwargs: Override default styles or add other sg.Frame parameters
258
+ (e.g., `title_color='red'`, `relief=sg.RELIEF_SUNKEN`).
259
+ """
260
+ cfg = self.config
261
+ font = (cfg.fonts.font_family, cfg.fonts.frame_size) # type: ignore
262
+
263
+ style_args = {
264
+ "font": font,
265
+ "expand_x": True,
266
+ "background_color": sg.theme_background_color(),
267
+ **kwargs
268
+ }
269
+ return sg.Frame(title, layout, **style_args)
270
+
271
+ # --- General-Purpose Layout Generators ---
272
+ def generate_continuous_layout(
273
+ self,
274
+ data_dict: Dict[str, Tuple[float, float]],
275
+ is_target: bool = False,
276
+ layout_mode: str = 'grid',
277
+ columns_per_row: int = 4
278
+ ) -> List[List[sg.Column]]:
279
+ """
280
+ Generates a layout for continuous features or targets.
281
+
282
+ Args:
283
+ data_dict (dict): Keys are feature names, values are (min, max) tuples.
284
+ is_target (bool): If True, creates disabled inputs for displaying results.
285
+ layout_mode (str): 'grid' for a multi-row grid layout, or 'row' for a single horizontal row.
286
+ columns_per_row (int): Number of feature columns per row when layout_mode is 'grid'.
287
+
288
+ Returns:
289
+ A list of lists of sg.Column elements, ready to be used in a window layout.
290
+ """
291
+ cfg = self.config
292
+ bg_color = sg.theme_background_color()
293
+ label_font = (cfg.fonts.font_family, cfg.fonts.label_size, cfg.fonts.label_style) # type: ignore
294
+
295
+ columns = []
296
+ for name, (val_min, val_max) in data_dict.items():
297
+ key = f"TARGET_{name}" if is_target else name
298
+ default_text = "" if is_target else str(val_max)
299
+
300
+ label = sg.Text(name, font=label_font, background_color=bg_color, key=f"_text_{name}")
301
+
302
+ input_style = {"size": cfg.layout.input_size_cont, "justification": "center"} # type: ignore
303
+ if is_target:
304
+ input_style["text_color"] = cfg.colors.target_text # type: ignore
305
+ input_style["disabled_readonly_background_color"] = cfg.colors.target_background # type: ignore
306
+
307
+ element = sg.Input(default_text, key=key, disabled=is_target, **input_style)
308
+
309
+ if is_target:
310
+ layout = [[label], [element]]
311
+ else:
312
+ range_font = (cfg.fonts.font_family, cfg.fonts.range_size) # type: ignore
313
+ range_text = sg.Text(f"Range: {int(val_min)}-{int(val_max)}", font=range_font, background_color=bg_color)
314
+ layout = [[label], [element], [range_text]]
315
+
316
+ layout.append([sg.Text(" ", font=(cfg.fonts.font_family, 2), background_color=bg_color)]) # type: ignore
317
+ columns.append(sg.Column(layout, background_color=bg_color))
318
+
319
+ if layout_mode == 'row':
320
+ return [columns] # A single row containing all columns
321
+
322
+ # Default to 'grid' layout
323
+ return [columns[i:i + columns_per_row] for i in range(0, len(columns), columns_per_row)]
324
+
325
+ def generate_combo_layout(
326
+ self,
327
+ data_dict: Dict[str, List[Any]],
328
+ layout_mode: str = 'grid',
329
+ columns_per_row: int = 4
330
+ ) -> List[List[sg.Column]]:
331
+ """
332
+ Generates a layout for categorical or binary features using Combo boxes.
333
+
334
+ Args:
335
+ data_dict (dict): Keys are feature names, values are lists of options.
336
+ layout_mode (str): 'grid' for a multi-row grid layout, or 'row' for a single horizontal row.
337
+ columns_per_row (int): Number of feature columns per row when layout_mode is 'grid'.
338
+
339
+ Returns:
340
+ A list of lists of sg.Column elements, ready to be used in a window layout.
341
+ """
342
+ cfg = self.config
343
+ bg_color = sg.theme_background_color()
344
+ label_font = (cfg.fonts.font_family, cfg.fonts.label_size, cfg.fonts.label_style) # type: ignore
345
+
346
+ columns = []
347
+ for name, values in data_dict.items():
348
+ label = sg.Text(name, font=label_font, background_color=bg_color, key=f"_text_{name}")
349
+ element = sg.Combo(
350
+ values, default_value=values[0], key=name,
351
+ size=cfg.layout.input_size_binary, readonly=True # type: ignore
352
+ )
353
+ layout = [[label], [element]]
354
+ layout.append([sg.Text(" ", font=(cfg.fonts.font_family, 2), background_color=bg_color)]) # type: ignore
355
+ columns.append(sg.Column(layout, background_color=bg_color))
356
+
357
+ if layout_mode == 'row':
358
+ return [columns] # A single row containing all columns
359
+
360
+ # Default to 'grid' layout
361
+ return [columns[i:i + columns_per_row] for i in range(0, len(columns), columns_per_row)]
362
+
363
+ # --- Window Creation ---
364
+ def create_window(self, title: str, layout: List[List[sg.Element]], **kwargs) -> sg.Window:
365
+ """
366
+ Creates and finalizes the main application window.
367
+
368
+ Args:
369
+ title (str): The title for the window.
370
+ layout (list): The final, assembled layout for the window.
371
+ **kwargs: Additional arguments to pass to the sg.Window constructor
372
+ (e.g., `location=(100, 100)`, `keep_on_top=True`).
373
+ """
374
+ cfg = self.config.general # type: ignore
375
+ version = getattr(self.config.meta, 'version', None) # type: ignore
376
+ full_title = f"{title} v{version}" if version else title
377
+
378
+ window_args = {
379
+ "resizable": cfg.resizable_window,
380
+ "finalize": True,
381
+ "background_color": sg.theme_background_color(),
382
+ **kwargs
383
+ }
384
+ window = sg.Window(full_title, layout, **window_args)
385
+
386
+ if cfg.min_size: window.TKroot.minsize(*cfg.min_size)
387
+ if cfg.max_size: window.TKroot.maxsize(*cfg.max_size)
388
+
389
+ return window
390
+
391
+
392
+ # --- Exception Handling Decorator ---
393
+ def catch_exceptions(show_popup: bool = True):
394
+ """
395
+ A decorator that wraps a function in a try-except block.
396
+ If an exception occurs, it's caught and displayed in a popup window.
397
+ """
398
+ def decorator(func):
399
+ @wraps(func)
400
+ def wrapper(*args, **kwargs):
401
+ try:
402
+ return func(*args, **kwargs)
403
+ except Exception as e:
404
+ # Format the full traceback to give detailed error info
405
+ error_msg = traceback.format_exc()
406
+ if show_popup:
407
+ sg.popup_error("An error occurred:", error_msg, title="Error")
408
+ else:
409
+ # Fallback for non-GUI contexts or if popup is disabled
410
+ _LOGGER.error(error_msg)
411
+ return wrapper
412
+ return decorator
413
+
414
+
415
+ # --- Inference Helpers ---
416
+ def _default_categorical_processor(feature_name: str, chosen_value: Any) -> List[float]:
417
+ """
418
+ Default processor for binary 'True'/'False' strings.
419
+ Returns a list containing a single float.
420
+ """
421
+ return [1.0] if str(chosen_value) == 'True' else [0.0]
422
+
423
+ def prepare_feature_vector(
424
+ values: Dict[str, Any],
425
+ feature_order: List[str],
426
+ continuous_features: List[str],
427
+ categorical_features: List[str],
428
+ categorical_processor: Optional[Callable[[str, Any], List[float]]] = None
429
+ ) -> np.ndarray:
430
+ """
431
+ Validates and converts GUI values into a numpy array for a model.
432
+ This function supports label encoding and one-hot encoding via the processor.
433
+
434
+ Args:
435
+ values (dict): The values dictionary from a `window.read()` call.
436
+ feature_order (list): A list of all feature names that have a GUI element.
437
+ For one-hot encoding, this should be the name of the
438
+ single GUI element (e.g., 'material_type'), not the
439
+ expanded feature names (e.g., 'material_is_steel').
440
+ continuous_features (list): A list of names for continuous features.
441
+ categorical_features (list): A list of names for categorical features.
442
+ categorical_processor (callable, optional): A function to process categorical
443
+ values. It should accept (feature_name, chosen_value) and return a
444
+ list of floats (e.g., [1.0] for label encoding, [0.0, 1.0, 0.0] for one-hot).
445
+ If None, a default 'True'/'False' processor is used.
446
+
447
+ Returns:
448
+ A 1D numpy array ready for model inference.
449
+ """
450
+ processed_values: List[float] = []
451
+
452
+ # Use the provided processor or the default one
453
+ processor = categorical_processor or _default_categorical_processor
454
+
455
+ # Create sets for faster lookups
456
+ cont_set = set(continuous_features)
457
+ cat_set = set(categorical_features)
458
+
459
+ for name in feature_order:
460
+ chosen_value = values.get(name)
461
+
462
+ if chosen_value is None or chosen_value == '':
463
+ raise ValueError(f"Feature '{name}' is missing a value.")
464
+
465
+ if name in cont_set:
466
+ try:
467
+ processed_values.append(float(chosen_value))
468
+ except (ValueError, TypeError):
469
+ raise ValueError(f"Invalid input for '{name}'. Please enter a valid number.")
470
+
471
+ elif name in cat_set:
472
+ # The processor returns a list of values (one for label, multiple for one-hot)
473
+ numeric_values = processor(name, chosen_value)
474
+ processed_values.extend(numeric_values)
475
+
476
+ return np.array(processed_values, dtype=np.float32)
477
+
478
+
479
+ def update_target_fields(window: sg.Window, results_dict: Dict[str, Any]):
480
+ """
481
+ Updates the GUI's target fields with inference results.
482
+
483
+ Args:
484
+ window (sg.Window): The application's window object.
485
+ results_dict (dict): A dictionary where keys are target names (without the
486
+ 'TARGET_' prefix) and values are the predicted results.
487
+ """
488
+ for target_name, result in results_dict.items():
489
+ # Format numbers to 2 decimal places, leave other types as-is
490
+ display_value = f"{result:.2f}" if isinstance(result, (int, float)) else result
491
+ window[f'TARGET_{target_name}'].update(display_value)
492
+
493
+
494
+ def info():
495
+ _script_info(__all__)