flixopt 2.1.11__py3-none-any.whl → 2.2.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.

Potentially problematic release.


This version of flixopt might be problematic. Click here for more details.

flixopt/__init__.py CHANGED
@@ -35,5 +35,3 @@ from .commons import (
35
35
  results,
36
36
  solvers,
37
37
  )
38
-
39
- CONFIG.load_config()
flixopt/calculation.py CHANGED
@@ -91,13 +91,13 @@ class Calculation:
91
91
  model.label_of_element: float(model.size.solution)
92
92
  for component in self.flow_system.components.values()
93
93
  for model in component.model.all_sub_models
94
- if isinstance(model, InvestmentModel) and float(model.size.solution) >= CONFIG.modeling.EPSILON
94
+ if isinstance(model, InvestmentModel) and float(model.size.solution) >= CONFIG.Modeling.epsilon
95
95
  },
96
96
  'Not invested': {
97
97
  model.label_of_element: float(model.size.solution)
98
98
  for component in self.flow_system.components.values()
99
99
  for model in component.model.all_sub_models
100
- if isinstance(model, InvestmentModel) and float(model.size.solution) < CONFIG.modeling.EPSILON
100
+ if isinstance(model, InvestmentModel) and float(model.size.solution) < CONFIG.Modeling.epsilon
101
101
  },
102
102
  },
103
103
  'Buses with excess': [
flixopt/config.py CHANGED
@@ -1,168 +1,345 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- import os
5
- import types
6
- from dataclasses import dataclass, fields, is_dataclass
7
- from typing import Annotated, Literal, get_type_hints
4
+ import warnings
5
+ from logging.handlers import RotatingFileHandler
6
+ from pathlib import Path
7
+ from types import MappingProxyType
8
+ from typing import Literal
8
9
 
9
10
  import yaml
10
11
  from rich.console import Console
11
12
  from rich.logging import RichHandler
13
+ from rich.style import Style
14
+ from rich.theme import Theme
12
15
 
13
- logger = logging.getLogger('flixopt')
14
-
15
-
16
- def merge_configs(defaults: dict, overrides: dict) -> dict:
17
- """
18
- Merge the default configuration with user-provided overrides.
19
- Args:
20
- defaults: Default configuration dictionary.
21
- overrides: User configuration dictionary.
22
- Returns:
23
- Merged configuration dictionary.
24
- """
25
- for key, value in overrides.items():
26
- if isinstance(value, dict) and key in defaults and isinstance(defaults[key], dict):
27
- # Recursively merge nested dictionaries
28
- defaults[key] = merge_configs(defaults[key], value)
29
- else:
30
- # Override the default value
31
- defaults[key] = value
32
- return defaults
33
-
34
-
35
- def dataclass_from_dict_with_validation(cls, data: dict):
36
- """
37
- Recursively initialize a dataclass from a dictionary.
38
- """
39
- if not is_dataclass(cls):
40
- raise TypeError(f'{cls} must be a dataclass')
41
-
42
- # Get resolved type hints to handle postponed evaluation
43
- type_hints = get_type_hints(cls)
44
-
45
- # Build kwargs for the dataclass constructor
46
- kwargs = {}
47
- for field in fields(cls):
48
- field_name = field.name
49
- # Use resolved type from get_type_hints instead of field.type
50
- field_type = type_hints.get(field_name, field.type)
51
- field_value = data.get(field_name)
52
-
53
- # If the field type is a dataclass and the value is a dict, recursively initialize
54
- if is_dataclass(field_type) and isinstance(field_value, dict):
55
- kwargs[field_name] = dataclass_from_dict_with_validation(field_type, field_value)
56
- else:
57
- kwargs[field_name] = field_value # Pass as-is if no special handling is needed
58
-
59
- return cls(**kwargs)
16
+ __all__ = ['CONFIG', 'change_logging_level']
60
17
 
18
+ logger = logging.getLogger('flixopt')
61
19
 
62
- @dataclass()
63
- class ValidatedConfig:
64
- def __setattr__(self, name, value):
65
- if field := self.__dataclass_fields__.get(name):
66
- # Get resolved type hints to handle postponed evaluation
67
- type_hints = get_type_hints(self.__class__, include_extras=True)
68
- field_type = type_hints.get(name, field.type)
69
- if metadata := getattr(field_type, '__metadata__', None):
70
- assert metadata[0](value), f'Invalid value passed to {name!r}: {value=}'
71
- super().__setattr__(name, value)
72
20
 
21
+ # SINGLE SOURCE OF TRUTH - immutable to prevent accidental modification
22
+ _DEFAULTS = MappingProxyType(
23
+ {
24
+ 'config_name': 'flixopt',
25
+ 'logging': MappingProxyType(
26
+ {
27
+ 'level': 'INFO',
28
+ 'file': 'flixopt.log',
29
+ 'rich': False,
30
+ 'console': True,
31
+ 'max_file_size': 10_485_760, # 10MB
32
+ 'backup_count': 5,
33
+ 'date_format': '%Y-%m-%d %H:%M:%S',
34
+ 'format': '%(message)s',
35
+ 'console_width': 120,
36
+ 'show_path': False,
37
+ 'colors': MappingProxyType(
38
+ {
39
+ 'DEBUG': '\033[32m', # Green
40
+ 'INFO': '\033[34m', # Blue
41
+ 'WARNING': '\033[33m', # Yellow
42
+ 'ERROR': '\033[31m', # Red
43
+ 'CRITICAL': '\033[1m\033[31m', # Bold Red
44
+ }
45
+ ),
46
+ }
47
+ ),
48
+ 'modeling': MappingProxyType(
49
+ {
50
+ 'big': 10_000_000,
51
+ 'epsilon': 1e-5,
52
+ 'big_binary_bound': 100_000,
53
+ }
54
+ ),
55
+ }
56
+ )
73
57
 
74
- @dataclass
75
- class LoggingConfig(ValidatedConfig):
76
- level: Annotated[
77
- Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
78
- lambda level: level in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
79
- ]
80
- file: Annotated[str, lambda file: isinstance(file, str)]
81
- rich: Annotated[bool, lambda rich: isinstance(rich, bool)]
82
58
 
59
+ class CONFIG:
60
+ """Configuration for flixopt library.
61
+
62
+ The CONFIG class provides centralized configuration for logging and modeling parameters.
63
+ All changes require calling ``CONFIG.apply()`` to take effect.
64
+
65
+ By default, logging outputs to both console and file ('flixopt.log').
66
+
67
+ Attributes:
68
+ Logging: Nested class containing all logging configuration options.
69
+ Colors: Nested subclass under Logging containing ANSI color codes for log levels.
70
+ Modeling: Nested class containing optimization modeling parameters.
71
+ config_name (str): Name of the configuration (default: 'flixopt').
72
+
73
+ Logging Attributes:
74
+ level (str): Logging level: 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'.
75
+ Default: 'INFO'
76
+ file (str | None): Log file path. Default: 'flixopt.log'.
77
+ Set to None to disable file logging.
78
+ console (bool): Enable console (stdout) logging. Default: True
79
+ rich (bool): Use Rich library for enhanced console output. Default: False
80
+ max_file_size (int): Maximum log file size in bytes before rotation.
81
+ Default: 10485760 (10MB)
82
+ backup_count (int): Number of backup log files to keep. Default: 5
83
+ date_format (str): Date/time format for log messages.
84
+ Default: '%Y-%m-%d %H:%M:%S'
85
+ format (str): Log message format string. Default: '%(message)s'
86
+ console_width (int): Console width for Rich handler. Default: 120
87
+ show_path (bool): Show file paths in log messages. Default: False
88
+
89
+ Colors Attributes:
90
+ DEBUG (str): ANSI color code for DEBUG level. Default: '\\033[32m' (green)
91
+ INFO (str): ANSI color code for INFO level. Default: '\\033[34m' (blue)
92
+ WARNING (str): ANSI color code for WARNING level. Default: '\\033[33m' (yellow)
93
+ ERROR (str): ANSI color code for ERROR level. Default: '\\033[31m' (red)
94
+ CRITICAL (str): ANSI color code for CRITICAL level. Default: '\\033[1m\\033[31m' (bold red)
95
+
96
+ Works with both Rich and standard console handlers.
97
+ Rich automatically converts ANSI codes using Style.from_ansi().
98
+
99
+ Common ANSI codes:
100
+
101
+ - '\\033[30m' - Black
102
+ - '\\033[31m' - Red
103
+ - '\\033[32m' - Green
104
+ - '\\033[33m' - Yellow
105
+ - '\\033[34m' - Blue
106
+ - '\\033[35m' - Magenta
107
+ - '\\033[36m' - Cyan
108
+ - '\\033[37m' - White
109
+ - '\\033[1m\\033[3Xm' - Bold color (replace X with color code 0-7)
110
+ - '\\033[2m\\033[3Xm' - Dim color (replace X with color code 0-7)
111
+
112
+ Examples:
113
+
114
+ - Magenta: '\\033[35m'
115
+ - Bold cyan: '\\033[1m\\033[36m'
116
+ - Dim green: '\\033[2m\\033[32m'
117
+
118
+ Modeling Attributes:
119
+ big (int): Large number for optimization constraints. Default: 10000000
120
+ epsilon (float): Small tolerance value. Default: 1e-5
121
+ big_binary_bound (int): Upper bound for binary variable constraints.
122
+ Default: 100000
123
+
124
+ Examples:
125
+ Basic configuration::
126
+
127
+ from flixopt import CONFIG
128
+
129
+ CONFIG.Logging.console = True
130
+ CONFIG.Logging.level = 'DEBUG'
131
+ CONFIG.apply()
132
+
133
+ Configure log file rotation::
134
+
135
+ CONFIG.Logging.file = 'myapp.log'
136
+ CONFIG.Logging.max_file_size = 5_242_880 # 5 MB
137
+ CONFIG.Logging.backup_count = 3
138
+ CONFIG.apply()
139
+
140
+ Customize log colors::
141
+
142
+ CONFIG.Logging.Colors.INFO = '\\033[35m' # Magenta
143
+ CONFIG.Logging.Colors.DEBUG = '\\033[36m' # Cyan
144
+ CONFIG.Logging.Colors.ERROR = '\\033[1m\\033[31m' # Bold red
145
+ CONFIG.apply()
146
+
147
+ Use Rich handler with custom colors::
148
+
149
+ CONFIG.Logging.console = True
150
+ CONFIG.Logging.rich = True
151
+ CONFIG.Logging.console_width = 100
152
+ CONFIG.Logging.show_path = True
153
+ CONFIG.Logging.Colors.INFO = '\\033[36m' # Cyan
154
+ CONFIG.apply()
155
+
156
+ Load from YAML file::
157
+
158
+ CONFIG.load_from_file('config.yaml')
159
+
160
+ Example YAML config file:
161
+
162
+ .. code-block:: yaml
163
+
164
+ logging:
165
+ level: DEBUG
166
+ console: true
167
+ file: app.log
168
+ rich: true
169
+ max_file_size: 5242880 # 5MB
170
+ backup_count: 3
171
+ date_format: '%H:%M:%S'
172
+ console_width: 100
173
+ show_path: true
174
+ colors:
175
+ DEBUG: "\\033[36m" # Cyan
176
+ INFO: "\\033[32m" # Green
177
+ WARNING: "\\033[33m" # Yellow
178
+ ERROR: "\\033[31m" # Red
179
+ CRITICAL: "\\033[1m\\033[31m" # Bold red
180
+
181
+ modeling:
182
+ big: 20000000
183
+ epsilon: 1e-6
184
+ big_binary_bound: 200000
185
+
186
+ Reset to defaults::
83
187
 
84
- @dataclass
85
- class ModelingConfig(ValidatedConfig):
86
- BIG: Annotated[int, lambda x: isinstance(x, int)]
87
- EPSILON: Annotated[float, lambda x: isinstance(x, float)]
88
- BIG_BINARY_BOUND: Annotated[int, lambda x: isinstance(x, int)]
188
+ CONFIG.reset()
189
+
190
+ Export current configuration::
89
191
 
192
+ config_dict = CONFIG.to_dict()
193
+ import yaml
90
194
 
91
- @dataclass
92
- class ConfigSchema(ValidatedConfig):
93
- config_name: Annotated[str, lambda x: isinstance(x, str)]
94
- logging: LoggingConfig
95
- modeling: ModelingConfig
195
+ with open('my_config.yaml', 'w') as f:
196
+ yaml.dump(config_dict, f)
197
+ """
96
198
 
199
+ class Logging:
200
+ level: str = _DEFAULTS['logging']['level']
201
+ file: str | None = _DEFAULTS['logging']['file']
202
+ rich: bool = _DEFAULTS['logging']['rich']
203
+ console: bool = _DEFAULTS['logging']['console']
204
+ max_file_size: int = _DEFAULTS['logging']['max_file_size']
205
+ backup_count: int = _DEFAULTS['logging']['backup_count']
206
+ date_format: str = _DEFAULTS['logging']['date_format']
207
+ format: str = _DEFAULTS['logging']['format']
208
+ console_width: int = _DEFAULTS['logging']['console_width']
209
+ show_path: bool = _DEFAULTS['logging']['show_path']
210
+
211
+ class Colors:
212
+ DEBUG: str = _DEFAULTS['logging']['colors']['DEBUG']
213
+ INFO: str = _DEFAULTS['logging']['colors']['INFO']
214
+ WARNING: str = _DEFAULTS['logging']['colors']['WARNING']
215
+ ERROR: str = _DEFAULTS['logging']['colors']['ERROR']
216
+ CRITICAL: str = _DEFAULTS['logging']['colors']['CRITICAL']
217
+
218
+ class Modeling:
219
+ big: int = _DEFAULTS['modeling']['big']
220
+ epsilon: float = _DEFAULTS['modeling']['epsilon']
221
+ big_binary_bound: int = _DEFAULTS['modeling']['big_binary_bound']
222
+
223
+ config_name: str = _DEFAULTS['config_name']
97
224
 
98
- class CONFIG:
99
- """
100
- A configuration class that stores global configuration values as class attributes.
101
- """
225
+ @classmethod
226
+ def reset(cls):
227
+ """Reset all configuration values to defaults."""
228
+ for key, value in _DEFAULTS['logging'].items():
229
+ if key == 'colors':
230
+ # Reset nested Colors class
231
+ for color_key, color_value in value.items():
232
+ setattr(cls.Logging.Colors, color_key, color_value)
233
+ else:
234
+ setattr(cls.Logging, key, value)
235
+
236
+ for key, value in _DEFAULTS['modeling'].items():
237
+ setattr(cls.Modeling, key, value)
238
+
239
+ cls.config_name = _DEFAULTS['config_name']
240
+ cls.apply()
102
241
 
103
- config_name: str = None
104
- modeling: ModelingConfig = None
105
- logging: LoggingConfig = None
242
+ @classmethod
243
+ def apply(cls):
244
+ """Apply current configuration to logging system."""
245
+ # Convert Colors class attributes to dict
246
+ colors_dict = {
247
+ 'DEBUG': cls.Logging.Colors.DEBUG,
248
+ 'INFO': cls.Logging.Colors.INFO,
249
+ 'WARNING': cls.Logging.Colors.WARNING,
250
+ 'ERROR': cls.Logging.Colors.ERROR,
251
+ 'CRITICAL': cls.Logging.Colors.CRITICAL,
252
+ }
253
+
254
+ _setup_logging(
255
+ default_level=cls.Logging.level,
256
+ log_file=cls.Logging.file,
257
+ use_rich_handler=cls.Logging.rich,
258
+ console=cls.Logging.console,
259
+ max_file_size=cls.Logging.max_file_size,
260
+ backup_count=cls.Logging.backup_count,
261
+ date_format=cls.Logging.date_format,
262
+ format=cls.Logging.format,
263
+ console_width=cls.Logging.console_width,
264
+ show_path=cls.Logging.show_path,
265
+ colors=colors_dict,
266
+ )
106
267
 
107
268
  @classmethod
108
- def load_config(cls, user_config_file: str | None = None):
109
- """
110
- Initialize configuration using defaults or user-specified file.
111
- """
112
- # Default config file
113
- default_config_path = os.path.join(os.path.dirname(__file__), 'config.yaml')
114
-
115
- if user_config_file is None:
116
- with open(default_config_path) as file:
117
- new_config = yaml.safe_load(file)
118
- elif not os.path.exists(user_config_file):
119
- raise FileNotFoundError(f'Config file not found: {user_config_file}')
120
- else:
121
- with open(user_config_file) as user_file:
122
- new_config = yaml.safe_load(user_file)
269
+ def load_from_file(cls, config_file: str | Path):
270
+ """Load configuration from YAML file and apply it."""
271
+ config_path = Path(config_file)
272
+ if not config_path.exists():
273
+ raise FileNotFoundError(f'Config file not found: {config_file}')
123
274
 
124
- # Convert the merged config to ConfigSchema
125
- config_data = dataclass_from_dict_with_validation(ConfigSchema, new_config)
275
+ with config_path.open() as file:
276
+ config_dict = yaml.safe_load(file)
277
+ cls._apply_config_dict(config_dict)
126
278
 
127
- # Store the configuration in the class as class attributes
128
- cls.logging = config_data.logging
129
- cls.modeling = config_data.modeling
130
- cls.config_name = config_data.config_name
279
+ cls.apply()
131
280
 
132
- setup_logging(default_level=cls.logging.level, log_file=cls.logging.file, use_rich_handler=cls.logging.rich)
281
+ @classmethod
282
+ def _apply_config_dict(cls, config_dict: dict):
283
+ """Apply configuration dictionary to class attributes."""
284
+ for key, value in config_dict.items():
285
+ if key == 'logging' and isinstance(value, dict):
286
+ for nested_key, nested_value in value.items():
287
+ if nested_key == 'colors' and isinstance(nested_value, dict):
288
+ # Handle nested colors under logging
289
+ for color_key, color_value in nested_value.items():
290
+ setattr(cls.Logging.Colors, color_key, color_value)
291
+ else:
292
+ setattr(cls.Logging, nested_key, nested_value)
293
+ elif key == 'modeling' and isinstance(value, dict):
294
+ for nested_key, nested_value in value.items():
295
+ setattr(cls.Modeling, nested_key, nested_value)
296
+ elif hasattr(cls, key):
297
+ setattr(cls, key, value)
133
298
 
134
299
  @classmethod
135
300
  def to_dict(cls):
136
- """
137
- Convert the configuration class into a dictionary for JSON serialization.
138
- Handles dataclasses and simple types like str, int, etc.
139
- """
140
- config_dict = {}
141
- for attribute, value in cls.__dict__.items():
142
- # Only consider attributes (not methods, etc.)
143
- if (
144
- not attribute.startswith('_')
145
- and not isinstance(value, (types.FunctionType, types.MethodType))
146
- and not isinstance(value, classmethod)
147
- ):
148
- if is_dataclass(value):
149
- config_dict[attribute] = value.__dict__
150
- else: # Assuming only basic types here!
151
- config_dict[attribute] = value
152
-
153
- return config_dict
301
+ """Convert the configuration class into a dictionary for JSON serialization."""
302
+ return {
303
+ 'config_name': cls.config_name,
304
+ 'logging': {
305
+ 'level': cls.Logging.level,
306
+ 'file': cls.Logging.file,
307
+ 'rich': cls.Logging.rich,
308
+ 'console': cls.Logging.console,
309
+ 'max_file_size': cls.Logging.max_file_size,
310
+ 'backup_count': cls.Logging.backup_count,
311
+ 'date_format': cls.Logging.date_format,
312
+ 'format': cls.Logging.format,
313
+ 'console_width': cls.Logging.console_width,
314
+ 'show_path': cls.Logging.show_path,
315
+ 'colors': {
316
+ 'DEBUG': cls.Logging.Colors.DEBUG,
317
+ 'INFO': cls.Logging.Colors.INFO,
318
+ 'WARNING': cls.Logging.Colors.WARNING,
319
+ 'ERROR': cls.Logging.Colors.ERROR,
320
+ 'CRITICAL': cls.Logging.Colors.CRITICAL,
321
+ },
322
+ },
323
+ 'modeling': {
324
+ 'big': cls.Modeling.big,
325
+ 'epsilon': cls.Modeling.epsilon,
326
+ 'big_binary_bound': cls.Modeling.big_binary_bound,
327
+ },
328
+ }
154
329
 
155
330
 
156
331
  class MultilineFormater(logging.Formatter):
332
+ """Formatter that handles multi-line messages with consistent prefixes."""
333
+
334
+ def __init__(self, fmt=None, datefmt=None):
335
+ super().__init__(fmt=fmt, datefmt=datefmt)
336
+
157
337
  def format(self, record):
158
338
  message_lines = record.getMessage().split('\n')
159
-
160
- # Prepare the log prefix (timestamp + log level)
161
339
  timestamp = self.formatTime(record, self.datefmt)
162
- log_level = record.levelname.ljust(8) # Align log levels for consistency
340
+ log_level = record.levelname.ljust(8)
163
341
  log_prefix = f'{timestamp} | {log_level} |'
164
342
 
165
- # Format all lines
166
343
  first_line = [f'{log_prefix} {message_lines[0]}']
167
344
  if len(message_lines) > 1:
168
345
  lines = first_line + [f'{log_prefix} {line}' for line in message_lines[1:]]
@@ -173,96 +350,212 @@ class MultilineFormater(logging.Formatter):
173
350
 
174
351
 
175
352
  class ColoredMultilineFormater(MultilineFormater):
176
- # ANSI escape codes for colors
177
- COLORS = {
178
- 'DEBUG': '\033[32m', # Green
179
- 'INFO': '\033[34m', # Blue
180
- 'WARNING': '\033[33m', # Yellow
181
- 'ERROR': '\033[31m', # Red
182
- 'CRITICAL': '\033[1m\033[31m', # Bold Red
183
- }
353
+ """Formatter that adds ANSI colors to multi-line log messages."""
354
+
184
355
  RESET = '\033[0m'
185
356
 
357
+ def __init__(self, fmt=None, datefmt=None, colors=None):
358
+ super().__init__(fmt=fmt, datefmt=datefmt)
359
+ self.COLORS = (
360
+ colors
361
+ if colors is not None
362
+ else {
363
+ 'DEBUG': '\033[32m',
364
+ 'INFO': '\033[34m',
365
+ 'WARNING': '\033[33m',
366
+ 'ERROR': '\033[31m',
367
+ 'CRITICAL': '\033[1m\033[31m',
368
+ }
369
+ )
370
+
186
371
  def format(self, record):
187
372
  lines = super().format(record).splitlines()
188
373
  log_color = self.COLORS.get(record.levelname, self.RESET)
374
+ formatted_lines = [f'{log_color}{line}{self.RESET}' for line in lines]
375
+ return '\n'.join(formatted_lines)
189
376
 
190
- # Create a formatted message for each line separately
191
- formatted_lines = []
192
- for line in lines:
193
- formatted_lines.append(f'{log_color}{line}{self.RESET}')
194
377
 
195
- return '\n'.join(formatted_lines)
378
+ def _create_console_handler(
379
+ use_rich: bool = False,
380
+ console_width: int = 120,
381
+ show_path: bool = False,
382
+ date_format: str = '%Y-%m-%d %H:%M:%S',
383
+ format: str = '%(message)s',
384
+ colors: dict[str, str] | None = None,
385
+ ) -> logging.Handler:
386
+ """Create a console (stdout) logging handler.
196
387
 
388
+ Args:
389
+ use_rich: If True, use RichHandler with color support.
390
+ console_width: Width of the console for Rich handler.
391
+ show_path: Show file paths in log messages (Rich only).
392
+ date_format: Date/time format string.
393
+ format: Log message format string.
394
+ colors: Dictionary of ANSI color codes for each log level.
197
395
 
198
- def _get_logging_handler(log_file: str | None = None, use_rich_handler: bool = False) -> logging.Handler:
199
- """Returns a logging handler for the given log file."""
200
- if use_rich_handler and log_file is None:
201
- # RichHandler for console output
202
- console = Console(width=120)
203
- rich_handler = RichHandler(
396
+ Returns:
397
+ Configured logging handler (RichHandler or StreamHandler).
398
+ """
399
+ if use_rich:
400
+ # Convert ANSI codes to Rich theme
401
+ if colors:
402
+ theme_dict = {}
403
+ for level, ansi_code in colors.items():
404
+ # Rich can parse ANSI codes directly!
405
+ try:
406
+ style = Style.from_ansi(ansi_code)
407
+ theme_dict[f'logging.level.{level.lower()}'] = style
408
+ except Exception:
409
+ # Fallback to default if parsing fails
410
+ pass
411
+
412
+ theme = Theme(theme_dict) if theme_dict else None
413
+ else:
414
+ theme = None
415
+
416
+ console = Console(width=console_width, theme=theme)
417
+ handler = RichHandler(
204
418
  console=console,
205
419
  rich_tracebacks=True,
206
420
  omit_repeated_times=True,
207
- show_path=False,
208
- log_time_format='%Y-%m-%d %H:%M:%S',
209
- )
210
- rich_handler.setFormatter(logging.Formatter('%(message)s')) # Simplified formatting
211
-
212
- return rich_handler
213
- elif log_file is None:
214
- # Regular Logger with custom formating enabled
215
- file_handler = logging.StreamHandler()
216
- file_handler.setFormatter(
217
- ColoredMultilineFormater(
218
- fmt='%(message)s',
219
- datefmt='%Y-%m-%d %H:%M:%S',
220
- )
421
+ show_path=show_path,
422
+ log_time_format=date_format,
221
423
  )
222
- return file_handler
424
+ handler.setFormatter(logging.Formatter(format))
223
425
  else:
224
- # FileHandler for file output
225
- file_handler = logging.FileHandler(log_file)
226
- file_handler.setFormatter(
227
- MultilineFormater(
228
- fmt='%(message)s',
229
- datefmt='%Y-%m-%d %H:%M:%S',
230
- )
231
- )
232
- return file_handler
426
+ handler = logging.StreamHandler()
427
+ handler.setFormatter(ColoredMultilineFormater(fmt=format, datefmt=date_format, colors=colors))
428
+
429
+ return handler
430
+
431
+
432
+ def _create_file_handler(
433
+ log_file: str,
434
+ max_file_size: int = 10_485_760,
435
+ backup_count: int = 5,
436
+ date_format: str = '%Y-%m-%d %H:%M:%S',
437
+ format: str = '%(message)s',
438
+ ) -> RotatingFileHandler:
439
+ """Create a rotating file handler to prevent huge log files.
233
440
 
441
+ Args:
442
+ log_file: Path to the log file.
443
+ max_file_size: Maximum size in bytes before rotation.
444
+ backup_count: Number of backup files to keep.
445
+ date_format: Date/time format string.
446
+ format: Log message format string.
447
+
448
+ Returns:
449
+ Configured RotatingFileHandler (without colors).
450
+ """
451
+ handler = RotatingFileHandler(
452
+ log_file,
453
+ maxBytes=max_file_size,
454
+ backupCount=backup_count,
455
+ encoding='utf-8',
456
+ )
457
+ handler.setFormatter(MultilineFormater(fmt=format, datefmt=date_format))
458
+ return handler
234
459
 
235
- def setup_logging(
460
+
461
+ def _setup_logging(
236
462
  default_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = 'INFO',
237
- log_file: str | None = 'flixopt.log',
463
+ log_file: str | None = None,
238
464
  use_rich_handler: bool = False,
465
+ console: bool = False,
466
+ max_file_size: int = 10_485_760,
467
+ backup_count: int = 5,
468
+ date_format: str = '%Y-%m-%d %H:%M:%S',
469
+ format: str = '%(message)s',
470
+ console_width: int = 120,
471
+ show_path: bool = False,
472
+ colors: dict[str, str] | None = None,
239
473
  ):
240
- """Setup logging configuration"""
241
- logger = logging.getLogger('flixopt') # Use a specific logger name for your package
242
- logger.setLevel(get_logging_level_by_name(default_level))
243
- # Clear existing handlers
244
- if logger.hasHandlers():
245
- logger.handlers.clear()
474
+ """Internal function to setup logging - use CONFIG.apply() instead.
246
475
 
247
- logger.addHandler(_get_logging_handler(use_rich_handler=use_rich_handler))
248
- if log_file is not None:
249
- logger.addHandler(_get_logging_handler(log_file, use_rich_handler=False))
476
+ Configures the flixopt logger with console and/or file handlers.
477
+ If no handlers are configured, adds NullHandler (library best practice).
250
478
 
251
- return logger
479
+ Args:
480
+ default_level: Logging level for the logger.
481
+ log_file: Path to log file (None to disable file logging).
482
+ use_rich_handler: Use Rich for enhanced console output.
483
+ console: Enable console logging.
484
+ max_file_size: Maximum log file size before rotation.
485
+ backup_count: Number of backup log files to keep.
486
+ date_format: Date/time format for log messages.
487
+ format: Log message format string.
488
+ console_width: Console width for Rich handler.
489
+ show_path: Show file paths in log messages (Rich only).
490
+ colors: ANSI color codes for each log level.
491
+ """
492
+ logger = logging.getLogger('flixopt')
493
+ logger.setLevel(getattr(logging, default_level.upper()))
494
+ logger.propagate = False # Prevent duplicate logs
495
+ logger.handlers.clear()
496
+
497
+ if console:
498
+ logger.addHandler(
499
+ _create_console_handler(
500
+ use_rich=use_rich_handler,
501
+ console_width=console_width,
502
+ show_path=show_path,
503
+ date_format=date_format,
504
+ format=format,
505
+ colors=colors,
506
+ )
507
+ )
508
+
509
+ if log_file:
510
+ logger.addHandler(
511
+ _create_file_handler(
512
+ log_file=log_file,
513
+ max_file_size=max_file_size,
514
+ backup_count=backup_count,
515
+ date_format=date_format,
516
+ format=format,
517
+ )
518
+ )
252
519
 
520
+ # Library best practice: NullHandler if no handlers configured
521
+ if not logger.handlers:
522
+ logger.addHandler(logging.NullHandler())
253
523
 
254
- def get_logging_level_by_name(level_name: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']) -> int:
255
- possible_logging_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
256
- if level_name.upper() not in possible_logging_levels:
257
- raise ValueError(f'Invalid logging level {level_name}')
258
- else:
259
- logging_level = getattr(logging, level_name.upper(), logging.WARNING)
260
- return logging_level
524
+ return logger
261
525
 
262
526
 
263
527
  def change_logging_level(level_name: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']):
528
+ """
529
+ Change the logging level for the flixopt logger and all its handlers.
530
+
531
+ .. deprecated:: 2.1.11
532
+ Use ``CONFIG.Logging.level = level_name`` and ``CONFIG.apply()`` instead.
533
+ This function will be removed in version 3.0.0.
534
+
535
+ Parameters
536
+ ----------
537
+ level_name : {'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'}
538
+ The logging level to set.
539
+
540
+ Examples
541
+ --------
542
+ >>> change_logging_level('DEBUG') # deprecated
543
+ >>> # Use this instead:
544
+ >>> CONFIG.Logging.level = 'DEBUG'
545
+ >>> CONFIG.apply()
546
+ """
547
+ warnings.warn(
548
+ 'change_logging_level is deprecated and will be removed in version 3.0.0. '
549
+ 'Use CONFIG.Logging.level = level_name and CONFIG.apply() instead.',
550
+ DeprecationWarning,
551
+ stacklevel=2,
552
+ )
264
553
  logger = logging.getLogger('flixopt')
265
- logging_level = get_logging_level_by_name(level_name)
554
+ logging_level = getattr(logging, level_name.upper())
266
555
  logger.setLevel(logging_level)
267
556
  for handler in logger.handlers:
268
557
  handler.setLevel(logging_level)
558
+
559
+
560
+ # Initialize default config
561
+ CONFIG.apply()
flixopt/elements.py CHANGED
@@ -248,7 +248,7 @@ class Flow(Element):
248
248
  size: Flow capacity or nominal rating. Can be:
249
249
  - Scalar value for fixed capacity
250
250
  - InvestParameters for investment-based sizing decisions
251
- - None to use large default value (CONFIG.modeling.BIG)
251
+ - None to use large default value (CONFIG.Modeling.big)
252
252
  relative_minimum: Minimum flow rate as fraction of size.
253
253
  Example: 0.2 means flow cannot go below 20% of rated capacity.
254
254
  relative_maximum: Maximum flow rate as fraction of size (typically 1.0).
@@ -356,7 +356,7 @@ class Flow(Element):
356
356
  `relative_maximum` for upper bounds on optimization variables.
357
357
 
358
358
  Notes:
359
- - Default size (CONFIG.modeling.BIG) is used when size=None
359
+ - Default size (CONFIG.Modeling.big) is used when size=None
360
360
  - list inputs for previous_flow_rate are converted to NumPy arrays
361
361
  - Flow direction is determined by component input/output designation
362
362
 
@@ -383,7 +383,7 @@ class Flow(Element):
383
383
  meta_data: dict | None = None,
384
384
  ):
385
385
  super().__init__(label, meta_data=meta_data)
386
- self.size = CONFIG.modeling.BIG if size is None else size
386
+ self.size = CONFIG.Modeling.big if size is None else size
387
387
  self.relative_minimum = relative_minimum
388
388
  self.relative_maximum = relative_maximum
389
389
  self.fixed_relative_profile = fixed_relative_profile
@@ -455,11 +455,11 @@ class Flow(Element):
455
455
  raise PlausibilityError(self.label_full + ': Take care, that relative_minimum <= relative_maximum!')
456
456
 
457
457
  if (
458
- self.size == CONFIG.modeling.BIG and self.fixed_relative_profile is not None
458
+ self.size == CONFIG.Modeling.big and self.fixed_relative_profile is not None
459
459
  ): # Default Size --> Most likely by accident
460
460
  logger.warning(
461
461
  f'Flow "{self.label}" has no size assigned, but a "fixed_relative_profile". '
462
- f'The default size is {CONFIG.modeling.BIG}. As "flow_rate = size * fixed_relative_profile", '
462
+ f'The default size is {CONFIG.Modeling.big}. As "flow_rate = size * fixed_relative_profile", '
463
463
  f'the resulting flow_rate will be very high. To fix this, assign a size to the Flow {self}.'
464
464
  )
465
465
 
flixopt/features.py CHANGED
@@ -143,7 +143,7 @@ class InvestmentModel(Model):
143
143
  # eq2: P_invest >= isInvested * max(epsilon, investSize_min)
144
144
  self.add(
145
145
  self._model.add_constraints(
146
- self.size >= self.is_invested * np.maximum(CONFIG.modeling.EPSILON, self.parameters.minimum_size),
146
+ self.size >= self.is_invested * np.maximum(CONFIG.Modeling.epsilon, self.parameters.minimum_size),
147
147
  name=f'{self.label_full}|is_invested_lb',
148
148
  ),
149
149
  'is_invested_lb',
@@ -304,7 +304,7 @@ class StateModel(Model):
304
304
  # Constraint: on * lower_bound <= def_var
305
305
  self.add(
306
306
  self._model.add_constraints(
307
- self.on * np.maximum(CONFIG.modeling.EPSILON, lb) <= def_var, name=f'{self.label_full}|on_con1'
307
+ self.on * np.maximum(CONFIG.Modeling.epsilon, lb) <= def_var, name=f'{self.label_full}|on_con1'
308
308
  ),
309
309
  'on_con1',
310
310
  )
@@ -314,7 +314,7 @@ class StateModel(Model):
314
314
  else:
315
315
  # Case for multiple defining variables
316
316
  ub = sum(bound[1] for bound in self._defining_bounds) / nr_of_def_vars
317
- lb = CONFIG.modeling.EPSILON # TODO: Can this be a bigger value? (maybe the smallest bound?)
317
+ lb = CONFIG.Modeling.epsilon # TODO: Can this be a bigger value? (maybe the smallest bound?)
318
318
 
319
319
  # Constraint: on * epsilon <= sum(all_defining_variables)
320
320
  self.add(
@@ -337,7 +337,7 @@ class StateModel(Model):
337
337
  @property
338
338
  def previous_states(self) -> np.ndarray:
339
339
  """Computes the previous states {0, 1} of defining variables as a binary array from their previous values."""
340
- return StateModel.compute_previous_states(self._previous_values, epsilon=CONFIG.modeling.EPSILON)
340
+ return StateModel.compute_previous_states(self._previous_values, epsilon=CONFIG.Modeling.epsilon)
341
341
 
342
342
  @property
343
343
  def previous_on_states(self) -> np.ndarray:
@@ -603,14 +603,14 @@ class ConsecutiveStateModel(Model):
603
603
  elif np.isscalar(binary_values) and not np.isscalar(hours_per_timestep):
604
604
  return binary_values * hours_per_timestep[-1]
605
605
 
606
- if np.isclose(binary_values[-1], 0, atol=CONFIG.modeling.EPSILON):
606
+ if np.isclose(binary_values[-1], 0, atol=CONFIG.Modeling.epsilon):
607
607
  return 0
608
608
 
609
609
  if np.isscalar(hours_per_timestep):
610
610
  hours_per_timestep = np.ones(len(binary_values)) * hours_per_timestep
611
611
  hours_per_timestep: np.ndarray
612
612
 
613
- indexes_with_zero_values = np.where(np.isclose(binary_values, 0, atol=CONFIG.modeling.EPSILON))[0]
613
+ indexes_with_zero_values = np.where(np.isclose(binary_values, 0, atol=CONFIG.Modeling.epsilon))[0]
614
614
  if len(indexes_with_zero_values) == 0:
615
615
  nr_of_indexes_with_consecutive_ones = len(binary_values)
616
616
  else:
flixopt/interface.py CHANGED
@@ -650,10 +650,10 @@ class InvestParameters(Interface):
650
650
  fixed_size: When specified, creates a binary investment decision at exactly
651
651
  this size. When None, allows continuous sizing between minimum and maximum bounds.
652
652
  minimum_size: Lower bound for continuous sizing decisions. Defaults to a small
653
- positive value (CONFIG.modeling.EPSILON) to avoid numerical issues.
653
+ positive value (CONFIG.Modeling.epsilon) to avoid numerical issues.
654
654
  Ignored when fixed_size is specified.
655
655
  maximum_size: Upper bound for continuous sizing decisions. Defaults to a large
656
- value (CONFIG.modeling.BIG) representing unlimited capacity.
656
+ value (CONFIG.Modeling.big) representing unlimited capacity.
657
657
  Ignored when fixed_size is specified.
658
658
  optional: Controls whether investment is required. When True (default),
659
659
  optimization can choose not to invest. When False, forces investment
@@ -833,8 +833,8 @@ class InvestParameters(Interface):
833
833
  self.optional = optional
834
834
  self.specific_effects: EffectValuesUserScalar = specific_effects or {}
835
835
  self.piecewise_effects = piecewise_effects
836
- self._minimum_size = minimum_size if minimum_size is not None else CONFIG.modeling.EPSILON
837
- self._maximum_size = maximum_size if maximum_size is not None else CONFIG.modeling.BIG # default maximum
836
+ self._minimum_size = minimum_size if minimum_size is not None else CONFIG.Modeling.epsilon
837
+ self._maximum_size = maximum_size if maximum_size is not None else CONFIG.Modeling.big # default maximum
838
838
 
839
839
  def transform_data(self, flow_system: FlowSystem):
840
840
  self.fix_effects = flow_system.effects.create_effect_values_dict(self.fix_effects)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flixopt
3
- Version: 2.1.11
3
+ Version: 2.2.0
4
4
  Summary: Vector based energy and material flow optimization framework in Python.
5
5
  Author-email: "Chair of Building Energy Systems and Heat Supply, TU Dresden" <peter.stange@tu-dresden.de>, Felix Bumann <felixbumann387@gmail.com>, Felix Panitz <baumbude@googlemail.com>, Peter Stange <peter.stange@tu-dresden.de>
6
6
  Maintainer-email: Felix Bumann <felixbumann387@gmail.com>, Peter Stange <peter.stange@tu-dresden.de>
@@ -53,10 +53,10 @@ Provides-Extra: dev
53
53
  Requires-Dist: pytest==8.4.2; extra == "dev"
54
54
  Requires-Dist: pytest-xdist==3.8.0; extra == "dev"
55
55
  Requires-Dist: nbformat==5.10.4; extra == "dev"
56
- Requires-Dist: ruff==0.13.0; extra == "dev"
56
+ Requires-Dist: ruff==0.13.2; extra == "dev"
57
57
  Requires-Dist: pre-commit==4.3.0; extra == "dev"
58
58
  Requires-Dist: pyvis==0.3.2; extra == "dev"
59
- Requires-Dist: tsam==2.3.1; extra == "dev"
59
+ Requires-Dist: tsam==2.3.9; extra == "dev"
60
60
  Requires-Dist: scipy==1.15.1; extra == "dev"
61
61
  Requires-Dist: gurobipy==12.0.3; extra == "dev"
62
62
  Requires-Dist: dash==3.0.0; extra == "dev"
@@ -65,7 +65,7 @@ Requires-Dist: dash-daq==0.6.0; extra == "dev"
65
65
  Requires-Dist: networkx==3.0.0; extra == "dev"
66
66
  Requires-Dist: werkzeug==3.0.0; extra == "dev"
67
67
  Provides-Extra: docs
68
- Requires-Dist: mkdocs-material==9.6.19; extra == "docs"
68
+ Requires-Dist: mkdocs-material==9.6.20; extra == "docs"
69
69
  Requires-Dist: mkdocstrings-python==1.18.2; extra == "docs"
70
70
  Requires-Dist: mkdocs-table-reader-plugin==3.1.0; extra == "docs"
71
71
  Requires-Dist: mkdocs-gen-files==0.5.0; extra == "docs"
@@ -1,16 +1,15 @@
1
- flixopt/__init__.py,sha256=xTxHXC-_lWa65roGUM3J3HXfiYcMCJHoQsuL5mS0Xkg,688
1
+ flixopt/__init__.py,sha256=WodgPpH8UHORIGh9EucATUTK6ct4ebPDBuSxymZgl3E,666
2
2
  flixopt/aggregation.py,sha256=BFy2ngr0a0By5wfBZet9fX8X5ZPmkn1x6HMRaw1ZsTo,16892
3
- flixopt/calculation.py,sha256=VBcvpQr7ZXvWZfryfilwUzK161br4ZP3zTwvYHraGjY,24476
3
+ flixopt/calculation.py,sha256=2Dfgpwjmg_dCz9jWHOUpEwIgzggvIPXhiIg8nGl1YMc,24476
4
4
  flixopt/commons.py,sha256=ZNlUN1z-h9OGHPo-s-n5OLlJaoPZKVGcAdRyGKpMk4M,1256
5
5
  flixopt/components.py,sha256=GhALvEkjGagm6iy2VxU3lVkUPlQOtsnFmu6ZsUDrcwM,54632
6
- flixopt/config.py,sha256=2GJ9NZl35OKMUvofIpztgKh3kdca1m1wa77RjsUaob0,9509
7
- flixopt/config.yaml,sha256=imzAnnhcJhIfKNTTXFB5Td7Pvk5ARn5j720k-oGGRug,392
6
+ flixopt/config.py,sha256=Gy2k1J4A-TS01jILwgUy9xp38AEMawU_pk0_XseufKI,19889
8
7
  flixopt/core.py,sha256=HnXnUXnAL8yqBExdsXGgwu5HnfWRH0omjjMKgp_SBKc,37824
9
8
  flixopt/effects.py,sha256=HcalPMUaAfuzD2p1OeQ9nLY2vmHv_GxwyGLLeqNGNZ8,19435
10
- flixopt/elements.py,sha256=IWtr0zyS1GEttYMiBZxC6sEX7y8pCWEbySxok3nDxwI,33535
11
- flixopt/features.py,sha256=m61ixTFJ_Jh-Emx6QKVxo04Heswm89XsPUqbH8ezNr8,43891
9
+ flixopt/elements.py,sha256=tzQVeh1z98CDv2nm8wiwxY_B2cKMLLZyYwGAoYsNLso,33535
10
+ flixopt/features.py,sha256=hX2485foIxzKQ6DMv73FlIXcSmVhe6n1taeLmfyXE-A,43891
12
11
  flixopt/flow_system.py,sha256=tOOGtEzZFISL87lmxNOsYf9uiSfE9XLUzenDBsSf8ns,20895
13
- flixopt/interface.py,sha256=e5tom-aTEPfl4FQg8abyDv7g1tTvxeOWiSWg_IGti-g,47575
12
+ flixopt/interface.py,sha256=sBJJxVKIPrl3w8NQ-BRnr_1C65A5i1ZWispPdV6YeVM,47575
14
13
  flixopt/io.py,sha256=huFIS1c_1qXR5LJBl1hUmaN_nCxqj0GC1MUqvzObMfA,11477
15
14
  flixopt/linear_converters.py,sha256=rKa0AZlJHgDsPF_LVsvrhxhsmVTRanPE0NuKP7OICtg,22668
16
15
  flixopt/network_app.py,sha256=LnVAlAgzL1BgMYLsJ20a62j6nQUmNccF1zo4ACUXzL4,29433
@@ -19,8 +18,8 @@ flixopt/results.py,sha256=zSOFbDV7gpvoEUvRvmlP5JUHe-QhOJOuGKgZUsFjiGQ,40701
19
18
  flixopt/solvers.py,sha256=m38Smc22MJfHYMiqfNf1MA3OmvbTRm5OWS9nECkDdQk,2355
20
19
  flixopt/structure.py,sha256=vyD1lc80NH3JLexKJuar9btgHhEbcNEmihCQkBWea8k,26254
21
20
  flixopt/utils.py,sha256=a-YKR7C7HtD8dSIcxzlJTgryV5HMS7zELSXNYr_Lz9Q,1775
22
- flixopt-2.1.11.dist-info/licenses/LICENSE,sha256=HKsZnbrM_3Rvnr_u9cWSG90cBsj5_slaqI_z_qcxnGI,1118
23
- flixopt-2.1.11.dist-info/METADATA,sha256=oAsmH85SKM0yBLHacY8G3zzLwCk-drY1OBFR9WGieX8,8386
24
- flixopt-2.1.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
- flixopt-2.1.11.dist-info/top_level.txt,sha256=fanTzb9NylIXfv6Ic7spU97fVmRgGDPKvI_91tw4S3E,8
26
- flixopt-2.1.11.dist-info/RECORD,,
21
+ flixopt-2.2.0.dist-info/licenses/LICENSE,sha256=HKsZnbrM_3Rvnr_u9cWSG90cBsj5_slaqI_z_qcxnGI,1118
22
+ flixopt-2.2.0.dist-info/METADATA,sha256=yvVKc1LFjiGo8zDKAVQcaSsMzlrEDbXsn3nhoi4kg0o,8385
23
+ flixopt-2.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
+ flixopt-2.2.0.dist-info/top_level.txt,sha256=fanTzb9NylIXfv6Ic7spU97fVmRgGDPKvI_91tw4S3E,8
25
+ flixopt-2.2.0.dist-info/RECORD,,
flixopt/config.yaml DELETED
@@ -1,10 +0,0 @@
1
- # Default configuration of flixopt
2
- config_name: flixopt # Name of the config file. This has no effect on the configuration itself.
3
- logging:
4
- level: INFO
5
- file: flixopt.log
6
- rich: false # logging output is formatted using rich. This is only advisable when using a proper terminal
7
- modeling:
8
- BIG: 10000000 # 1e notation not possible in yaml
9
- EPSILON: 0.00001
10
- BIG_BINARY_BOUND: 100000