flixopt 3.0.1__py3-none-any.whl → 6.0.0rc7__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.
Files changed (42) hide show
  1. flixopt/__init__.py +57 -49
  2. flixopt/carrier.py +159 -0
  3. flixopt/clustering/__init__.py +51 -0
  4. flixopt/clustering/base.py +1746 -0
  5. flixopt/clustering/intercluster_helpers.py +201 -0
  6. flixopt/color_processing.py +372 -0
  7. flixopt/comparison.py +819 -0
  8. flixopt/components.py +848 -270
  9. flixopt/config.py +853 -496
  10. flixopt/core.py +111 -98
  11. flixopt/effects.py +294 -284
  12. flixopt/elements.py +484 -223
  13. flixopt/features.py +220 -118
  14. flixopt/flow_system.py +2026 -389
  15. flixopt/interface.py +504 -286
  16. flixopt/io.py +1718 -55
  17. flixopt/linear_converters.py +291 -230
  18. flixopt/modeling.py +304 -181
  19. flixopt/network_app.py +2 -1
  20. flixopt/optimization.py +788 -0
  21. flixopt/optimize_accessor.py +373 -0
  22. flixopt/plot_result.py +143 -0
  23. flixopt/plotting.py +1177 -1034
  24. flixopt/results.py +1331 -372
  25. flixopt/solvers.py +12 -4
  26. flixopt/statistics_accessor.py +2412 -0
  27. flixopt/stats_accessor.py +75 -0
  28. flixopt/structure.py +954 -120
  29. flixopt/topology_accessor.py +676 -0
  30. flixopt/transform_accessor.py +2277 -0
  31. flixopt/types.py +120 -0
  32. flixopt-6.0.0rc7.dist-info/METADATA +290 -0
  33. flixopt-6.0.0rc7.dist-info/RECORD +36 -0
  34. {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/WHEEL +1 -1
  35. flixopt/aggregation.py +0 -382
  36. flixopt/calculation.py +0 -672
  37. flixopt/commons.py +0 -51
  38. flixopt/utils.py +0 -86
  39. flixopt-3.0.1.dist-info/METADATA +0 -209
  40. flixopt-3.0.1.dist-info/RECORD +0 -26
  41. {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/licenses/LICENSE +0 -0
  42. {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/top_level.txt +0 -0
flixopt/config.py CHANGED
@@ -1,52 +1,153 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- import sys
4
+ import os
5
5
  import warnings
6
6
  from logging.handlers import RotatingFileHandler
7
7
  from pathlib import Path
8
8
  from types import MappingProxyType
9
- from typing import Literal
9
+ from typing import TYPE_CHECKING, Literal
10
10
 
11
- import yaml
12
- from rich.console import Console
13
- from rich.logging import RichHandler
14
- from rich.style import Style
15
- from rich.theme import Theme
11
+ if TYPE_CHECKING:
12
+ from typing import TextIO
16
13
 
17
- __all__ = ['CONFIG', 'change_logging_level']
14
+ try:
15
+ import colorlog
16
+ from colorlog.escape_codes import escape_codes
18
17
 
19
- logger = logging.getLogger('flixopt')
18
+ COLORLOG_AVAILABLE = True
19
+ except ImportError:
20
+ COLORLOG_AVAILABLE = False
21
+ escape_codes = None
22
+
23
+ __all__ = ['CONFIG', 'MultilineFormatter', 'SUCCESS_LEVEL', 'DEPRECATION_REMOVAL_VERSION']
24
+
25
+ if COLORLOG_AVAILABLE:
26
+ __all__.append('ColoredMultilineFormatter')
27
+
28
+ # Add custom SUCCESS level (between INFO and WARNING)
29
+ SUCCESS_LEVEL = 25
30
+ logging.addLevelName(SUCCESS_LEVEL, 'SUCCESS')
31
+
32
+ # Deprecation removal version - update this when planning the next major version
33
+ DEPRECATION_REMOVAL_VERSION = '7.0.0'
34
+
35
+
36
+ class MultilineFormatter(logging.Formatter):
37
+ """Custom formatter that handles multi-line messages with box-style borders.
38
+
39
+ Uses Unicode box-drawing characters for prettier output, with a fallback
40
+ to simple formatting if any encoding issues occur.
41
+ """
42
+
43
+ def __init__(self, *args, **kwargs):
44
+ super().__init__(*args, **kwargs)
45
+ # Set default format with time
46
+ if not self._fmt:
47
+ self._fmt = '%(asctime)s %(levelname)-8s │ %(message)s'
48
+ self._style = logging.PercentStyle(self._fmt)
49
+
50
+ def format(self, record):
51
+ """Format multi-line messages with box-style borders for better readability."""
52
+ try:
53
+ # Split into lines
54
+ lines = record.getMessage().split('\n')
55
+
56
+ # Add exception info if present (critical for logger.exception())
57
+ if record.exc_info:
58
+ lines.extend(self.formatException(record.exc_info).split('\n'))
59
+ if record.stack_info:
60
+ lines.extend(record.stack_info.rstrip().split('\n'))
61
+
62
+ # Format time with date and milliseconds (YYYY-MM-DD HH:MM:SS.mmm)
63
+ # formatTime doesn't support %f, so use datetime directly
64
+ import datetime
65
+
66
+ dt = datetime.datetime.fromtimestamp(record.created)
67
+ time_str = dt.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
68
+
69
+ # Single line - return standard format
70
+ if len(lines) == 1:
71
+ level_str = f'{record.levelname: <8}'
72
+ return f'{time_str} {level_str} │ {lines[0]}'
73
+
74
+ # Multi-line - use box format
75
+ level_str = f'{record.levelname: <8}'
76
+ result = f'{time_str} {level_str} │ ┌─ {lines[0]}'
77
+ indent = ' ' * 23 # 23 spaces for time with date (YYYY-MM-DD HH:MM:SS.mmm)
78
+ for line in lines[1:-1]:
79
+ result += f'\n{indent} {" " * 8} │ │ {line}'
80
+ result += f'\n{indent} {" " * 8} │ └─ {lines[-1]}'
81
+
82
+ return result
83
+
84
+ except Exception as e:
85
+ # Fallback to simple formatting if anything goes wrong (e.g., encoding issues)
86
+ return f'{record.created} {record.levelname} - {record.getMessage()} [Formatting Error: {e}]'
87
+
88
+
89
+ if COLORLOG_AVAILABLE:
90
+
91
+ class ColoredMultilineFormatter(colorlog.ColoredFormatter):
92
+ """Colored formatter with multi-line message support.
93
+
94
+ Uses Unicode box-drawing characters for prettier output, with a fallback
95
+ to simple formatting if any encoding issues occur.
96
+ """
97
+
98
+ def format(self, record):
99
+ """Format multi-line messages with colors and box-style borders."""
100
+ try:
101
+ # Split into lines
102
+ lines = record.getMessage().split('\n')
103
+
104
+ # Add exception info if present (critical for logger.exception())
105
+ if record.exc_info:
106
+ lines.extend(self.formatException(record.exc_info).split('\n'))
107
+ if record.stack_info:
108
+ lines.extend(record.stack_info.rstrip().split('\n'))
109
+
110
+ # Format time with date and milliseconds (YYYY-MM-DD HH:MM:SS.mmm)
111
+ import datetime
112
+
113
+ # Use thin attribute for timestamp
114
+ dim = escape_codes['thin']
115
+ reset = escape_codes['reset']
116
+ # formatTime doesn't support %f, so use datetime directly
117
+ dt = datetime.datetime.fromtimestamp(record.created)
118
+ time_str = dt.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
119
+ time_formatted = f'{dim}{time_str}{reset}'
120
+
121
+ # Get the color for this level
122
+ log_colors = self.log_colors
123
+ level_name = record.levelname
124
+ color_name = log_colors.get(level_name, '')
125
+ color = escape_codes.get(color_name, '')
126
+
127
+ level_str = f'{level_name: <8}'
128
+
129
+ # Single line - return standard colored format
130
+ if len(lines) == 1:
131
+ return f'{time_formatted} {color}{level_str}{reset} │ {lines[0]}'
132
+
133
+ # Multi-line - use box format with colors
134
+ result = f'{time_formatted} {color}{level_str}{reset} │ {color}┌─{reset} {lines[0]}'
135
+ indent = ' ' * 23 # 23 spaces for time with date (YYYY-MM-DD HH:MM:SS.mmm)
136
+ for line in lines[1:-1]:
137
+ result += f'\n{dim}{indent}{reset} {" " * 8} │ {color}│{reset} {line}'
138
+ result += f'\n{dim}{indent}{reset} {" " * 8} │ {color}└─{reset} {lines[-1]}'
139
+
140
+ return result
141
+
142
+ except Exception as e:
143
+ # Fallback to simple formatting if anything goes wrong (e.g., encoding issues)
144
+ return f'{record.created} {record.levelname} - {record.getMessage()} [Formatting Error: {e}]'
20
145
 
21
146
 
22
147
  # SINGLE SOURCE OF TRUTH - immutable to prevent accidental modification
23
148
  _DEFAULTS = MappingProxyType(
24
149
  {
25
150
  'config_name': 'flixopt',
26
- 'logging': MappingProxyType(
27
- {
28
- 'level': 'INFO',
29
- 'file': None,
30
- 'rich': False,
31
- 'console': False,
32
- 'max_file_size': 10_485_760, # 10MB
33
- 'backup_count': 5,
34
- 'date_format': '%Y-%m-%d %H:%M:%S',
35
- 'format': '%(message)s',
36
- 'console_width': 120,
37
- 'show_path': False,
38
- 'show_logger_name': False,
39
- 'colors': MappingProxyType(
40
- {
41
- 'DEBUG': '\033[90m', # Bright Black/Gray
42
- 'INFO': '\033[0m', # Default/White
43
- 'WARNING': '\033[33m', # Yellow
44
- 'ERROR': '\033[31m', # Red
45
- 'CRITICAL': '\033[1m\033[31m', # Bold Red
46
- }
47
- ),
48
- }
49
- ),
50
151
  'modeling': MappingProxyType(
51
152
  {
52
153
  'big': 10_000_000,
@@ -54,6 +155,26 @@ _DEFAULTS = MappingProxyType(
54
155
  'big_binary_bound': 100_000,
55
156
  }
56
157
  ),
158
+ 'plotting': MappingProxyType(
159
+ {
160
+ 'default_show': True,
161
+ 'default_engine': 'plotly',
162
+ 'default_dpi': 300,
163
+ 'default_facet_cols': 3,
164
+ 'default_sequential_colorscale': 'turbo',
165
+ 'default_qualitative_colorscale': 'plotly',
166
+ 'default_line_shape': 'hv',
167
+ }
168
+ ),
169
+ 'solving': MappingProxyType(
170
+ {
171
+ 'mip_gap': 0.01,
172
+ 'time_limit_seconds': 300,
173
+ 'log_to_console': True,
174
+ 'log_main_results': True,
175
+ 'compute_infeasibilities': True,
176
+ }
177
+ ),
57
178
  }
58
179
  )
59
180
 
@@ -61,116 +182,330 @@ _DEFAULTS = MappingProxyType(
61
182
  class CONFIG:
62
183
  """Configuration for flixopt library.
63
184
 
64
- Always call ``CONFIG.apply()`` after changes.
65
-
66
185
  Attributes:
67
- Logging: Logging configuration.
186
+ Logging: Logging configuration (see CONFIG.Logging for details).
68
187
  Modeling: Optimization modeling parameters.
188
+ Solving: Solver configuration and default parameters.
189
+ Plotting: Plotting configuration.
69
190
  config_name: Configuration name.
70
191
 
71
192
  Examples:
72
193
  ```python
73
- CONFIG.Logging.console = True
74
- CONFIG.Logging.level = 'DEBUG'
75
- CONFIG.apply()
76
- ```
77
-
78
- Load from YAML file:
79
-
80
- ```yaml
81
- logging:
82
- level: DEBUG
83
- console: true
84
- file: app.log
194
+ # Quick logging setup
195
+ CONFIG.Logging.enable_console('INFO')
196
+
197
+ # Or use presets (affects logging, plotting, solver output)
198
+ CONFIG.exploring() # Interactive exploration
199
+ CONFIG.debug() # Troubleshooting
200
+ CONFIG.production() # Production deployment
201
+ CONFIG.silent() # No output
202
+
203
+ # Adjust other settings
204
+ CONFIG.Solving.mip_gap = 0.001
205
+ CONFIG.Plotting.default_dpi = 600
85
206
  ```
86
207
  """
87
208
 
88
209
  class Logging:
89
- """Logging configuration.
210
+ """Logging configuration helpers.
90
211
 
91
- Silent by default. Enable via ``console=True`` or ``file='path'``.
212
+ flixopt is silent by default (WARNING level, no handlers).
92
213
 
93
- Attributes:
94
- level: Logging level.
95
- file: Log file path for file logging.
96
- console: Enable console output.
97
- rich: Use Rich library for enhanced output.
98
- max_file_size: Max file size before rotation.
99
- backup_count: Number of backup files to keep.
100
- date_format: Date/time format string.
101
- format: Log message format string.
102
- console_width: Console width for Rich handler.
103
- show_path: Show file paths in messages.
104
- show_logger_name: Show logger name in messages.
105
- Colors: ANSI color codes for log levels.
214
+ Quick Start - Use Presets:
215
+ These presets configure logging along with plotting and solver output:
216
+
217
+ | Preset | Console Logs | File Logs | Plots | Solver Output | Use Case |
218
+ |--------|-------------|-----------|-------|---------------|----------|
219
+ | ``CONFIG.exploring()`` | INFO (colored) | No | Browser | Yes | Interactive exploration |
220
+ | ``CONFIG.debug()`` | DEBUG (colored) | No | Default | Yes | Troubleshooting |
221
+ | ``CONFIG.production('app.log')`` | No | INFO | No | No | Production deployments |
222
+ | ``CONFIG.silent()`` | No | No | No | No | Silent operation |
223
+
224
+ Examples:
225
+ ```python
226
+ CONFIG.exploring() # Start exploring interactively
227
+ CONFIG.debug() # See everything for troubleshooting
228
+ CONFIG.production('logs/prod.log') # Production mode
229
+ ```
230
+
231
+ Direct Control - Logging Only:
232
+ For fine-grained control of logging without affecting other settings:
233
+
234
+ Methods:
235
+ - ``enable_console(level='INFO', colored=True, stream=None)``
236
+ - ``enable_file(level='INFO', path='flixopt.log', max_bytes=10MB, backup_count=5)``
237
+ - ``disable()`` - Remove all handlers
238
+ - ``set_colors(log_colors)`` - Customize level colors
239
+
240
+ Log Levels:
241
+ Standard levels plus custom SUCCESS level (between INFO and WARNING):
242
+ - DEBUG (10): Detailed debugging information
243
+ - INFO (20): General informational messages
244
+ - SUCCESS (25): Success messages (custom level)
245
+ - WARNING (30): Warning messages
246
+ - ERROR (40): Error messages
247
+ - CRITICAL (50): Critical error messages
248
+
249
+ Examples:
250
+ ```python
251
+ import logging
252
+ from flixopt.config import CONFIG, SUCCESS_LEVEL
253
+
254
+ # Console and file logging
255
+ CONFIG.Logging.enable_console('INFO')
256
+ CONFIG.Logging.enable_file('DEBUG', 'debug.log')
257
+
258
+ # Use SUCCESS level with logger.log()
259
+ logger = logging.getLogger('flixopt')
260
+ CONFIG.Logging.enable_console('SUCCESS') # Shows SUCCESS, WARNING, ERROR, CRITICAL
261
+ logger.log(SUCCESS_LEVEL, 'Operation completed successfully!')
262
+
263
+ # Or use numeric level directly
264
+ logger.log(25, 'Also works with numeric level')
265
+
266
+ # Customize colors
267
+ CONFIG.Logging.set_colors(
268
+ {
269
+ 'INFO': 'bold_white',
270
+ 'SUCCESS': 'bold_green,bg_black',
271
+ 'CRITICAL': 'bold_white,bg_red',
272
+ }
273
+ )
274
+
275
+ # Non-colored output
276
+ CONFIG.Logging.enable_console('INFO', colored=False)
277
+ ```
278
+
279
+ Advanced Customization:
280
+ For full control, use Python's standard logging or create custom formatters:
106
281
 
107
- Examples:
108
282
  ```python
109
- # File logging with rotation
110
- CONFIG.Logging.file = 'app.log'
111
- CONFIG.Logging.max_file_size = 5_242_880 # 5MB
112
- CONFIG.apply()
113
-
114
- # Rich handler with stdout
115
- CONFIG.Logging.console = True # or 'stdout'
116
- CONFIG.Logging.rich = True
117
- CONFIG.apply()
118
-
119
- # Console output to stderr
120
- CONFIG.Logging.console = 'stderr'
121
- CONFIG.apply()
283
+ # Custom formatter
284
+ from flixopt.config import ColoredMultilineFormatter
285
+ import colorlog, logging
286
+
287
+ handler = colorlog.StreamHandler()
288
+ handler.setFormatter(ColoredMultilineFormatter(...))
289
+ logging.getLogger('flixopt').addHandler(handler)
290
+
291
+ # Or standard Python logging
292
+ import logging
293
+
294
+ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
122
295
  ```
296
+
297
+ Note:
298
+ Default formatters (MultilineFormatter and ColoredMultilineFormatter)
299
+ provide pretty output with box borders for multi-line messages.
123
300
  """
124
301
 
125
- level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = _DEFAULTS['logging']['level']
126
- file: str | None = _DEFAULTS['logging']['file']
127
- rich: bool = _DEFAULTS['logging']['rich']
128
- console: bool | Literal['stdout', 'stderr'] = _DEFAULTS['logging']['console']
129
- max_file_size: int = _DEFAULTS['logging']['max_file_size']
130
- backup_count: int = _DEFAULTS['logging']['backup_count']
131
- date_format: str = _DEFAULTS['logging']['date_format']
132
- format: str = _DEFAULTS['logging']['format']
133
- console_width: int = _DEFAULTS['logging']['console_width']
134
- show_path: bool = _DEFAULTS['logging']['show_path']
135
- show_logger_name: bool = _DEFAULTS['logging']['show_logger_name']
136
-
137
- class Colors:
138
- """ANSI color codes for log levels.
139
-
140
- Attributes:
141
- DEBUG: ANSI color for DEBUG level.
142
- INFO: ANSI color for INFO level.
143
- WARNING: ANSI color for WARNING level.
144
- ERROR: ANSI color for ERROR level.
145
- CRITICAL: ANSI color for CRITICAL level.
302
+ @classmethod
303
+ def enable_console(cls, level: str | int = 'INFO', colored: bool = True, stream: TextIO | None = None) -> None:
304
+ """Enable colored console logging.
305
+
306
+ Args:
307
+ level: Log level (DEBUG, INFO, SUCCESS, WARNING, ERROR, CRITICAL or numeric level)
308
+ colored: Use colored output if colorlog is available (default: True)
309
+ stream: Output stream (default: sys.stdout). Can be sys.stdout or sys.stderr.
310
+
311
+ Note:
312
+ For full control over formatting, use logging.basicConfig() instead.
146
313
 
147
314
  Examples:
148
315
  ```python
149
- CONFIG.Logging.Colors.INFO = '\\033[32m' # Green
150
- CONFIG.Logging.Colors.ERROR = '\\033[1m\\033[31m' # Bold red
151
- CONFIG.apply()
316
+ # Colored output to stdout (default)
317
+ CONFIG.Logging.enable_console('INFO')
318
+
319
+ # Plain text output
320
+ CONFIG.Logging.enable_console('INFO', colored=False)
321
+
322
+ # Log to stderr instead
323
+ import sys
324
+
325
+ CONFIG.Logging.enable_console('INFO', stream=sys.stderr)
326
+
327
+ # Using logging constants
328
+ import logging
329
+
330
+ CONFIG.Logging.enable_console(logging.DEBUG)
152
331
  ```
332
+ """
333
+ import sys
334
+
335
+ logger = logging.getLogger('flixopt')
336
+
337
+ # Convert string level to logging constant
338
+ if isinstance(level, str):
339
+ if level.upper().strip() == 'SUCCESS':
340
+ level = SUCCESS_LEVEL
341
+ else:
342
+ level = getattr(logging, level.upper())
343
+
344
+ logger.setLevel(level)
345
+
346
+ # Default to stdout
347
+ if stream is None:
348
+ stream = sys.stdout
349
+
350
+ # Remove existing console handlers to avoid duplicates
351
+ logger.handlers = [
352
+ h
353
+ for h in logger.handlers
354
+ if not isinstance(h, logging.StreamHandler) or isinstance(h, RotatingFileHandler)
355
+ ]
356
+
357
+ if colored and COLORLOG_AVAILABLE:
358
+ handler = colorlog.StreamHandler(stream)
359
+ handler.setFormatter(
360
+ ColoredMultilineFormatter(
361
+ '%(log_color)s%(levelname)-8s%(reset)s %(message)s',
362
+ log_colors={
363
+ 'DEBUG': 'cyan',
364
+ 'INFO': '', # No color - use default terminal color
365
+ 'SUCCESS': 'green',
366
+ 'WARNING': 'yellow',
367
+ 'ERROR': 'red',
368
+ 'CRITICAL': 'bold_red',
369
+ },
370
+ )
371
+ )
372
+ else:
373
+ handler = logging.StreamHandler(stream)
374
+ handler.setFormatter(MultilineFormatter('%(levelname)-8s %(message)s'))
375
+
376
+ logger.addHandler(handler)
377
+ logger.propagate = False # Don't propagate to root
378
+
379
+ @classmethod
380
+ def enable_file(
381
+ cls,
382
+ level: str | int = 'INFO',
383
+ path: str | Path = 'flixopt.log',
384
+ max_bytes: int = 10 * 1024 * 1024,
385
+ backup_count: int = 5,
386
+ encoding: str = 'utf-8',
387
+ ) -> None:
388
+ """Enable file logging with rotation. Removes all existing file handlers!
389
+
390
+ Args:
391
+ level: Log level (DEBUG, INFO, SUCCESS, WARNING, ERROR, CRITICAL or numeric level)
392
+ path: Path to log file (default: 'flixopt.log')
393
+ max_bytes: Maximum file size before rotation in bytes (default: 10MB)
394
+ backup_count: Number of backup files to keep (default: 5)
395
+ encoding: File encoding (default: 'utf-8'). Use 'utf-8' for maximum compatibility.
396
+
397
+ Note:
398
+ For full control over formatting and handlers, use logging module directly.
153
399
 
154
- Common ANSI codes:
155
- - '\\033[30m' - Black
156
- - '\\033[31m' - Red
157
- - '\\033[32m' - Green
158
- - '\\033[33m' - Yellow
159
- - '\\033[34m' - Blue
160
- - '\\033[35m' - Magenta
161
- - '\\033[36m' - Cyan
162
- - '\\033[37m' - White
163
- - '\\033[90m' - Bright Black/Gray
164
- - '\\033[0m' - Reset to default
165
- - '\\033[1m\\033[3Xm' - Bold (replace X with color code 0-7)
166
- - '\\033[2m\\033[3Xm' - Dim (replace X with color code 0-7)
400
+ Examples:
401
+ ```python
402
+ # Basic file logging
403
+ CONFIG.Logging.enable_file('INFO', 'app.log')
404
+
405
+ # With custom rotation
406
+ CONFIG.Logging.enable_file('DEBUG', 'debug.log', max_bytes=50 * 1024 * 1024, backup_count=10)
407
+
408
+ # With explicit encoding
409
+ CONFIG.Logging.enable_file('INFO', 'app.log', encoding='utf-8')
410
+ ```
167
411
  """
412
+ logger = logging.getLogger('flixopt')
413
+
414
+ # Convert string level to logging constant
415
+ if isinstance(level, str):
416
+ if level.upper().strip() == 'SUCCESS':
417
+ level = SUCCESS_LEVEL
418
+ else:
419
+ level = getattr(logging, level.upper())
420
+
421
+ logger.setLevel(level)
422
+
423
+ # Remove existing file handlers to avoid duplicates, keep all non-file handlers (including custom handlers)
424
+ logger.handlers = [
425
+ h for h in logger.handlers if not isinstance(h, (logging.FileHandler, RotatingFileHandler))
426
+ ]
427
+
428
+ # Create log directory if needed
429
+ log_path = Path(path)
430
+ log_path.parent.mkdir(parents=True, exist_ok=True)
168
431
 
169
- DEBUG: str = _DEFAULTS['logging']['colors']['DEBUG']
170
- INFO: str = _DEFAULTS['logging']['colors']['INFO']
171
- WARNING: str = _DEFAULTS['logging']['colors']['WARNING']
172
- ERROR: str = _DEFAULTS['logging']['colors']['ERROR']
173
- CRITICAL: str = _DEFAULTS['logging']['colors']['CRITICAL']
432
+ handler = RotatingFileHandler(path, maxBytes=max_bytes, backupCount=backup_count, encoding=encoding)
433
+ handler.setFormatter(MultilineFormatter())
434
+
435
+ logger.addHandler(handler)
436
+ logger.propagate = False # Don't propagate to root
437
+
438
+ @classmethod
439
+ def disable(cls) -> None:
440
+ """Disable all flixopt logging.
441
+
442
+ Examples:
443
+ ```python
444
+ CONFIG.Logging.disable()
445
+ ```
446
+ """
447
+ logger = logging.getLogger('flixopt')
448
+ logger.handlers.clear()
449
+ logger.setLevel(logging.CRITICAL)
450
+
451
+ @classmethod
452
+ def set_colors(cls, log_colors: dict[str, str]) -> None:
453
+ """Customize log level colors for console output.
454
+
455
+ This updates the colors for the current console handler.
456
+ If no console handler exists, this does nothing.
457
+
458
+ Args:
459
+ log_colors: Dictionary mapping log levels to color names.
460
+ Colors can be comma-separated for multiple attributes
461
+ (e.g., 'bold_red,bg_white').
462
+
463
+ Available colors:
464
+ - Basic: black, red, green, yellow, blue, purple, cyan, white
465
+ - Bold: bold_red, bold_green, bold_yellow, bold_blue, etc.
466
+ - Light: light_red, light_green, light_yellow, light_blue, etc.
467
+ - Backgrounds: bg_red, bg_green, bg_light_red, etc.
468
+ - Combined: 'bold_white,bg_red' for white text on red background
469
+
470
+ Examples:
471
+ ```python
472
+ # Enable console first
473
+ CONFIG.Logging.enable_console('INFO')
474
+
475
+ # Then customize colors
476
+ CONFIG.Logging.set_colors(
477
+ {
478
+ 'DEBUG': 'cyan',
479
+ 'INFO': 'bold_white',
480
+ 'SUCCESS': 'bold_green',
481
+ 'WARNING': 'bold_yellow,bg_black', # Yellow on black
482
+ 'ERROR': 'bold_red',
483
+ 'CRITICAL': 'bold_white,bg_red', # White on red
484
+ }
485
+ )
486
+ ```
487
+
488
+ Note:
489
+ Requires colorlog to be installed. Has no effect on file handlers.
490
+ """
491
+ if not COLORLOG_AVAILABLE:
492
+ warnings.warn('colorlog is not installed. Colors cannot be customized.', stacklevel=2)
493
+ return
494
+
495
+ logger = logging.getLogger('flixopt')
496
+
497
+ # Find and update ColoredMultilineFormatter
498
+ for handler in logger.handlers:
499
+ if isinstance(handler, logging.StreamHandler):
500
+ formatter = handler.formatter
501
+ if isinstance(formatter, ColoredMultilineFormatter):
502
+ formatter.log_colors = log_colors
503
+ return
504
+
505
+ warnings.warn(
506
+ 'No ColoredMultilineFormatter found. Call CONFIG.Logging.enable_console() with colored=True first.',
507
+ stacklevel=2,
508
+ )
174
509
 
175
510
  class Modeling:
176
511
  """Optimization modeling parameters.
@@ -185,105 +520,133 @@ class CONFIG:
185
520
  epsilon: float = _DEFAULTS['modeling']['epsilon']
186
521
  big_binary_bound: int = _DEFAULTS['modeling']['big_binary_bound']
187
522
 
188
- config_name: str = _DEFAULTS['config_name']
523
+ class Solving:
524
+ """Solver configuration and default parameters.
189
525
 
190
- @classmethod
191
- def reset(cls):
192
- """Reset all configuration values to defaults."""
193
- for key, value in _DEFAULTS['logging'].items():
194
- if key == 'colors':
195
- # Reset nested Colors class
196
- for color_key, color_value in value.items():
197
- setattr(cls.Logging.Colors, color_key, color_value)
198
- else:
199
- setattr(cls.Logging, key, value)
526
+ Attributes:
527
+ mip_gap: Default MIP gap tolerance for solver convergence.
528
+ time_limit_seconds: Default time limit in seconds for solver runs.
529
+ log_to_console: Whether solver should output to console.
530
+ log_main_results: Whether to log main results after solving.
531
+ compute_infeasibilities: Whether to compute infeasibility analysis when the model is infeasible.
200
532
 
201
- for key, value in _DEFAULTS['modeling'].items():
202
- setattr(cls.Modeling, key, value)
533
+ Examples:
534
+ ```python
535
+ # Set tighter convergence and longer timeout
536
+ CONFIG.Solving.mip_gap = 0.001
537
+ CONFIG.Solving.time_limit_seconds = 600
538
+ CONFIG.Solving.log_to_console = False
539
+ ```
540
+ """
203
541
 
204
- cls.config_name = _DEFAULTS['config_name']
205
- cls.apply()
542
+ mip_gap: float = _DEFAULTS['solving']['mip_gap']
543
+ time_limit_seconds: int = _DEFAULTS['solving']['time_limit_seconds']
544
+ log_to_console: bool = _DEFAULTS['solving']['log_to_console']
545
+ log_main_results: bool = _DEFAULTS['solving']['log_main_results']
546
+ compute_infeasibilities: bool = _DEFAULTS['solving']['compute_infeasibilities']
206
547
 
207
- @classmethod
208
- def apply(cls):
209
- """Apply current configuration to logging system."""
210
- # Convert Colors class attributes to dict
211
- colors_dict = {
212
- 'DEBUG': cls.Logging.Colors.DEBUG,
213
- 'INFO': cls.Logging.Colors.INFO,
214
- 'WARNING': cls.Logging.Colors.WARNING,
215
- 'ERROR': cls.Logging.Colors.ERROR,
216
- 'CRITICAL': cls.Logging.Colors.CRITICAL,
217
- }
218
- valid_levels = list(colors_dict)
219
- if cls.Logging.level.upper() not in valid_levels:
220
- raise ValueError(f"Invalid log level '{cls.Logging.level}'. Must be one of: {', '.join(valid_levels)}")
221
-
222
- if cls.Logging.max_file_size <= 0:
223
- raise ValueError('max_file_size must be positive')
224
-
225
- if cls.Logging.backup_count < 0:
226
- raise ValueError('backup_count must be non-negative')
227
-
228
- if cls.Logging.console not in (False, True, 'stdout', 'stderr'):
229
- raise ValueError(f"console must be False, True, 'stdout', or 'stderr', got {cls.Logging.console}")
230
-
231
- _setup_logging(
232
- default_level=cls.Logging.level,
233
- log_file=cls.Logging.file,
234
- use_rich_handler=cls.Logging.rich,
235
- console=cls.Logging.console,
236
- max_file_size=cls.Logging.max_file_size,
237
- backup_count=cls.Logging.backup_count,
238
- date_format=cls.Logging.date_format,
239
- format=cls.Logging.format,
240
- console_width=cls.Logging.console_width,
241
- show_path=cls.Logging.show_path,
242
- show_logger_name=cls.Logging.show_logger_name,
243
- colors=colors_dict,
244
- )
548
+ class Plotting:
549
+ """Plotting configuration.
245
550
 
246
- @classmethod
247
- def load_from_file(cls, config_file: str | Path):
248
- """Load configuration from YAML file and apply it.
551
+ Configure backends via environment variables:
552
+ - Matplotlib: Set `MPLBACKEND` environment variable (e.g., 'Agg', 'TkAgg')
553
+ - Plotly: Set `PLOTLY_RENDERER` or use `plotly.io.renderers.default`
249
554
 
250
- Args:
251
- config_file: Path to the YAML configuration file.
555
+ Attributes:
556
+ default_show: Default value for the `show` parameter in plot methods.
557
+ default_engine: Default plotting engine.
558
+ default_dpi: Default DPI for saved plots.
559
+ default_facet_cols: Default number of columns for faceted plots.
560
+ default_sequential_colorscale: Default colorscale for heatmaps and continuous data.
561
+ default_qualitative_colorscale: Default colormap for categorical plots (bar/line/area charts).
252
562
 
253
- Raises:
254
- FileNotFoundError: If the config file does not exist.
563
+ Examples:
564
+ ```python
565
+ # Configure default export and color settings
566
+ CONFIG.Plotting.default_dpi = 600
567
+ CONFIG.Plotting.default_sequential_colorscale = 'plasma'
568
+ CONFIG.Plotting.default_qualitative_colorscale = 'Dark24'
569
+ ```
255
570
  """
256
- config_path = Path(config_file)
257
- if not config_path.exists():
258
- raise FileNotFoundError(f'Config file not found: {config_file}')
259
571
 
260
- with config_path.open() as file:
261
- config_dict = yaml.safe_load(file) or {}
262
- cls._apply_config_dict(config_dict)
572
+ default_show: bool = _DEFAULTS['plotting']['default_show']
573
+ default_engine: Literal['plotly', 'matplotlib'] = _DEFAULTS['plotting']['default_engine']
574
+ default_dpi: int = _DEFAULTS['plotting']['default_dpi']
575
+ default_facet_cols: int = _DEFAULTS['plotting']['default_facet_cols']
576
+ default_sequential_colorscale: str = _DEFAULTS['plotting']['default_sequential_colorscale']
577
+ default_qualitative_colorscale: str = _DEFAULTS['plotting']['default_qualitative_colorscale']
578
+ default_line_shape: str = _DEFAULTS['plotting']['default_line_shape']
579
+
580
+ class Carriers:
581
+ """Default carrier definitions for common energy types.
582
+
583
+ Provides convenient defaults for carriers. Colors are from D3/Plotly palettes.
584
+
585
+ Predefined: electricity, heat, gas, hydrogen, fuel, biomass
586
+
587
+ Examples:
588
+ ```python
589
+ import flixopt as fx
263
590
 
264
- cls.apply()
591
+ # Access predefined carriers
592
+ fx.CONFIG.Carriers.electricity # Carrier with color '#FECB52'
593
+ fx.CONFIG.Carriers.heat.color # '#D62728'
594
+
595
+ # Use with buses
596
+ bus = fx.Bus('Grid', carrier='electricity')
597
+ ```
598
+ """
599
+
600
+ from .carrier import Carrier
601
+
602
+ # Default carriers - colors from D3/Plotly palettes
603
+ electricity: Carrier = Carrier('electricity', '#FECB52') # Yellow
604
+ heat: Carrier = Carrier('heat', '#D62728') # Red
605
+ gas: Carrier = Carrier('gas', '#1F77B4') # Blue
606
+ hydrogen: Carrier = Carrier('hydrogen', '#9467BD') # Purple
607
+ fuel: Carrier = Carrier('fuel', '#8C564B') # Brown
608
+ biomass: Carrier = Carrier('biomass', '#2CA02C') # Green
609
+
610
+ config_name: str = _DEFAULTS['config_name']
265
611
 
266
612
  @classmethod
267
- def _apply_config_dict(cls, config_dict: dict):
268
- """Apply configuration dictionary to class attributes.
613
+ def reset(cls) -> None:
614
+ """Reset all configuration values to defaults.
269
615
 
270
- Args:
271
- config_dict: Dictionary containing configuration values.
616
+ This resets modeling, solving, and plotting settings to their default values,
617
+ and disables all logging handlers (back to silent mode).
618
+
619
+ Examples:
620
+ ```python
621
+ CONFIG.debug() # Enable debug mode
622
+ # ... do some work ...
623
+ CONFIG.reset() # Back to defaults (silent)
624
+ ```
272
625
  """
273
- for key, value in config_dict.items():
274
- if key == 'logging' and isinstance(value, dict):
275
- for nested_key, nested_value in value.items():
276
- if nested_key == 'colors' and isinstance(nested_value, dict):
277
- # Handle nested colors under logging
278
- for color_key, color_value in nested_value.items():
279
- setattr(cls.Logging.Colors, color_key, color_value)
280
- else:
281
- setattr(cls.Logging, nested_key, nested_value)
282
- elif key == 'modeling' and isinstance(value, dict):
283
- for nested_key, nested_value in value.items():
284
- setattr(cls.Modeling, nested_key, nested_value)
285
- elif hasattr(cls, key):
286
- setattr(cls, key, value)
626
+ # Reset settings
627
+ for key, value in _DEFAULTS['modeling'].items():
628
+ setattr(cls.Modeling, key, value)
629
+
630
+ for key, value in _DEFAULTS['solving'].items():
631
+ setattr(cls.Solving, key, value)
632
+
633
+ for key, value in _DEFAULTS['plotting'].items():
634
+ setattr(cls.Plotting, key, value)
635
+
636
+ # Reset Carriers to defaults
637
+ from .carrier import Carrier
638
+
639
+ cls.Carriers.electricity = Carrier('electricity', '#FECB52')
640
+ cls.Carriers.heat = Carrier('heat', '#D62728')
641
+ cls.Carriers.gas = Carrier('gas', '#1F77B4')
642
+ cls.Carriers.hydrogen = Carrier('hydrogen', '#9467BD')
643
+ cls.Carriers.fuel = Carrier('fuel', '#8C564B')
644
+ cls.Carriers.biomass = Carrier('biomass', '#2CA02C')
645
+
646
+ cls.config_name = _DEFAULTS['config_name']
647
+
648
+ # Reset logging to default (silent)
649
+ cls.Logging.disable()
287
650
 
288
651
  @classmethod
289
652
  def to_dict(cls) -> dict:
@@ -294,328 +657,322 @@ class CONFIG:
294
657
  """
295
658
  return {
296
659
  'config_name': cls.config_name,
297
- 'logging': {
298
- 'level': cls.Logging.level,
299
- 'file': cls.Logging.file,
300
- 'rich': cls.Logging.rich,
301
- 'console': cls.Logging.console,
302
- 'max_file_size': cls.Logging.max_file_size,
303
- 'backup_count': cls.Logging.backup_count,
304
- 'date_format': cls.Logging.date_format,
305
- 'format': cls.Logging.format,
306
- 'console_width': cls.Logging.console_width,
307
- 'show_path': cls.Logging.show_path,
308
- 'show_logger_name': cls.Logging.show_logger_name,
309
- 'colors': {
310
- 'DEBUG': cls.Logging.Colors.DEBUG,
311
- 'INFO': cls.Logging.Colors.INFO,
312
- 'WARNING': cls.Logging.Colors.WARNING,
313
- 'ERROR': cls.Logging.Colors.ERROR,
314
- 'CRITICAL': cls.Logging.Colors.CRITICAL,
315
- },
316
- },
317
660
  'modeling': {
318
661
  'big': cls.Modeling.big,
319
662
  'epsilon': cls.Modeling.epsilon,
320
663
  'big_binary_bound': cls.Modeling.big_binary_bound,
321
664
  },
665
+ 'solving': {
666
+ 'mip_gap': cls.Solving.mip_gap,
667
+ 'time_limit_seconds': cls.Solving.time_limit_seconds,
668
+ 'log_to_console': cls.Solving.log_to_console,
669
+ 'log_main_results': cls.Solving.log_main_results,
670
+ 'compute_infeasibilities': cls.Solving.compute_infeasibilities,
671
+ },
672
+ 'plotting': {
673
+ 'default_show': cls.Plotting.default_show,
674
+ 'default_engine': cls.Plotting.default_engine,
675
+ 'default_dpi': cls.Plotting.default_dpi,
676
+ 'default_facet_cols': cls.Plotting.default_facet_cols,
677
+ 'default_sequential_colorscale': cls.Plotting.default_sequential_colorscale,
678
+ 'default_qualitative_colorscale': cls.Plotting.default_qualitative_colorscale,
679
+ 'default_line_shape': cls.Plotting.default_line_shape,
680
+ },
322
681
  }
323
682
 
683
+ @classmethod
684
+ def silent(cls) -> type[CONFIG]:
685
+ """Configure for silent operation.
324
686
 
325
- class MultilineFormatter(logging.Formatter):
326
- """Formatter that handles multi-line messages with consistent prefixes.
687
+ Disables all logging, solver output, and result logging
688
+ for clean production runs. Does not show plots.
327
689
 
328
- Args:
329
- fmt: Log message format string.
330
- datefmt: Date/time format string.
331
- show_logger_name: Show logger name in log messages.
332
- """
690
+ Examples:
691
+ ```python
692
+ CONFIG.silent()
693
+ # Now run optimizations with no output
694
+ result = optimization.solve()
695
+ ```
696
+ """
697
+ cls.Logging.disable()
698
+ cls.Plotting.default_show = False
699
+ cls.Solving.log_to_console = False
700
+ cls.Solving.log_main_results = False
701
+ return cls
333
702
 
334
- def __init__(self, fmt: str = '%(message)s', datefmt: str | None = None, show_logger_name: bool = False):
335
- super().__init__(fmt=fmt, datefmt=datefmt)
336
- self.show_logger_name = show_logger_name
703
+ @classmethod
704
+ def debug(cls) -> type[CONFIG]:
705
+ """Configure for debug mode with verbose output.
337
706
 
338
- def format(self, record) -> str:
339
- record.message = record.getMessage()
340
- message_lines = self._style.format(record).split('\n')
341
- timestamp = self.formatTime(record, self.datefmt)
342
- log_level = record.levelname.ljust(8)
707
+ Enables console logging at DEBUG level and all solver output for troubleshooting.
343
708
 
344
- if self.show_logger_name:
345
- # Truncate long logger names for readability
346
- logger_name = record.name if len(record.name) <= 20 else f'...{record.name[-17:]}'
347
- log_prefix = f'{timestamp} | {log_level} | {logger_name.ljust(20)} |'
348
- else:
349
- log_prefix = f'{timestamp} | {log_level} |'
709
+ Examples:
710
+ ```python
711
+ CONFIG.debug()
712
+ # See detailed DEBUG logs and full solver output
713
+ optimization.solve()
714
+ ```
715
+ """
716
+ cls.Logging.enable_console('DEBUG')
717
+ cls.Solving.log_to_console = True
718
+ cls.Solving.log_main_results = True
719
+ return cls
350
720
 
351
- indent = ' ' * (len(log_prefix) + 1) # +1 for the space after prefix
721
+ @classmethod
722
+ def exploring(cls) -> type[CONFIG]:
723
+ """Configure for exploring flixopt.
352
724
 
353
- lines = [f'{log_prefix} {message_lines[0]}']
354
- if len(message_lines) > 1:
355
- lines.extend([f'{indent}{line}' for line in message_lines[1:]])
725
+ Enables console logging at INFO level and all solver output.
726
+ Also enables browser plotting for plotly with showing plots per default.
356
727
 
357
- return '\n'.join(lines)
728
+ Examples:
729
+ ```python
730
+ CONFIG.exploring()
731
+ # Perfect for interactive sessions
732
+ optimization.solve() # Shows INFO logs and solver output
733
+ result.plot() # Opens plots in browser
734
+ ```
735
+ """
736
+ cls.Logging.enable_console('INFO')
737
+ cls.Solving.log_to_console = True
738
+ cls.Solving.log_main_results = True
739
+ cls.browser_plotting()
740
+ return cls
358
741
 
742
+ @classmethod
743
+ def production(cls, log_file: str | Path = 'flixopt.log') -> type[CONFIG]:
744
+ """Configure for production use.
359
745
 
360
- class ColoredMultilineFormatter(MultilineFormatter):
361
- """Formatter that adds ANSI colors to multi-line log messages.
746
+ Enables file logging only (no console output), disables plots,
747
+ and disables solver console output for clean production runs.
362
748
 
363
- Args:
364
- fmt: Log message format string.
365
- datefmt: Date/time format string.
366
- colors: Dictionary of ANSI color codes for each log level.
367
- show_logger_name: Show logger name in log messages.
368
- """
749
+ Args:
750
+ log_file: Path to log file (default: 'flixopt.log')
369
751
 
370
- RESET = '\033[0m'
371
-
372
- def __init__(
373
- self,
374
- fmt: str | None = None,
375
- datefmt: str | None = None,
376
- colors: dict[str, str] | None = None,
377
- show_logger_name: bool = False,
378
- ):
379
- super().__init__(fmt=fmt, datefmt=datefmt, show_logger_name=show_logger_name)
380
- self.COLORS = (
381
- colors
382
- if colors is not None
383
- else {
384
- 'DEBUG': '\033[90m',
385
- 'INFO': '\033[0m',
386
- 'WARNING': '\033[33m',
387
- 'ERROR': '\033[31m',
388
- 'CRITICAL': '\033[1m\033[31m',
389
- }
390
- )
752
+ Examples:
753
+ ```python
754
+ CONFIG.production('production.log')
755
+ # Logs to file, no console output
756
+ optimization.solve()
757
+ ```
758
+ """
759
+ cls.Logging.disable() # Clear any console handlers
760
+ cls.Logging.enable_file('INFO', log_file)
761
+ cls.Plotting.default_show = False
762
+ cls.Solving.log_to_console = False
763
+ cls.Solving.log_main_results = False
764
+ return cls
391
765
 
392
- def format(self, record):
393
- lines = super().format(record).splitlines()
394
- log_color = self.COLORS.get(record.levelname, self.RESET)
395
- formatted_lines = [f'{log_color}{line}{self.RESET}' for line in lines]
396
- return '\n'.join(formatted_lines)
397
-
398
-
399
- def _create_console_handler(
400
- use_rich: bool = False,
401
- stream: Literal['stdout', 'stderr'] = 'stdout',
402
- console_width: int = 120,
403
- show_path: bool = False,
404
- show_logger_name: bool = False,
405
- date_format: str = '%Y-%m-%d %H:%M:%S',
406
- format: str = '%(message)s',
407
- colors: dict[str, str] | None = None,
408
- ) -> logging.Handler:
409
- """Create a console logging handler.
410
-
411
- Args:
412
- use_rich: If True, use RichHandler with color support.
413
- stream: Output stream
414
- console_width: Width of the console for Rich handler.
415
- show_path: Show file paths in log messages (Rich only).
416
- show_logger_name: Show logger name in log messages.
417
- date_format: Date/time format string.
418
- format: Log message format string.
419
- colors: Dictionary of ANSI color codes for each log level.
420
-
421
- Returns:
422
- Configured logging handler (RichHandler or StreamHandler).
423
- """
424
- # Determine the stream object
425
- stream_obj = sys.stdout if stream == 'stdout' else sys.stderr
426
-
427
- if use_rich:
428
- # Convert ANSI codes to Rich theme
429
- if colors:
430
- theme_dict = {}
431
- for level, ansi_code in colors.items():
432
- # Rich can parse ANSI codes directly!
433
- try:
434
- style = Style.from_ansi(ansi_code)
435
- theme_dict[f'logging.level.{level.lower()}'] = style
436
- except Exception:
437
- # Fallback to default if parsing fails
438
- pass
439
-
440
- theme = Theme(theme_dict) if theme_dict else None
441
- else:
442
- theme = None
443
-
444
- console = Console(width=console_width, theme=theme, file=stream_obj)
445
- handler = RichHandler(
446
- console=console,
447
- rich_tracebacks=True,
448
- omit_repeated_times=True,
449
- show_path=show_path,
450
- log_time_format=date_format,
451
- )
452
- handler.setFormatter(logging.Formatter(format))
453
- else:
454
- handler = logging.StreamHandler(stream=stream_obj)
455
- handler.setFormatter(
456
- ColoredMultilineFormatter(
457
- fmt=format,
458
- datefmt=date_format,
459
- colors=colors,
460
- show_logger_name=show_logger_name,
461
- )
462
- )
766
+ @classmethod
767
+ def browser_plotting(cls) -> type[CONFIG]:
768
+ """Configure for interactive usage with plotly to open plots in browser.
463
769
 
464
- return handler
465
-
466
-
467
- def _create_file_handler(
468
- log_file: str,
469
- max_file_size: int = 10_485_760,
470
- backup_count: int = 5,
471
- show_logger_name: bool = False,
472
- date_format: str = '%Y-%m-%d %H:%M:%S',
473
- format: str = '%(message)s',
474
- ) -> RotatingFileHandler:
475
- """Create a rotating file handler to prevent huge log files.
476
-
477
- Args:
478
- log_file: Path to the log file.
479
- max_file_size: Maximum size in bytes before rotation.
480
- backup_count: Number of backup files to keep.
481
- show_logger_name: Show logger name in log messages.
482
- date_format: Date/time format string.
483
- format: Log message format string.
484
-
485
- Returns:
486
- Configured RotatingFileHandler (without colors).
487
- """
770
+ Sets plotly.io.renderers.default = 'browser'. Useful for running examples
771
+ and viewing interactive plots. Does NOT modify CONFIG.Plotting settings.
488
772
 
489
- # Ensure parent directory exists
490
- log_path = Path(log_file)
491
- try:
492
- log_path.parent.mkdir(parents=True, exist_ok=True)
493
- except PermissionError as e:
494
- raise PermissionError(f"Cannot create log directory '{log_path.parent}': Permission denied") from e
495
-
496
- try:
497
- handler = RotatingFileHandler(
498
- log_file,
499
- maxBytes=max_file_size,
500
- backupCount=backup_count,
501
- encoding='utf-8',
502
- )
503
- except PermissionError as e:
504
- raise PermissionError(
505
- f"Cannot write to log file '{log_file}': Permission denied. "
506
- f'Choose a different location or check file permissions.'
507
- ) from e
508
-
509
- handler.setFormatter(
510
- MultilineFormatter(
511
- fmt=format,
512
- datefmt=date_format,
513
- show_logger_name=show_logger_name,
514
- )
515
- )
516
- return handler
517
-
518
-
519
- def _setup_logging(
520
- default_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = 'INFO',
521
- log_file: str | None = None,
522
- use_rich_handler: bool = False,
523
- console: bool | Literal['stdout', 'stderr'] = False,
524
- max_file_size: int = 10_485_760,
525
- backup_count: int = 5,
526
- date_format: str = '%Y-%m-%d %H:%M:%S',
527
- format: str = '%(message)s',
528
- console_width: int = 120,
529
- show_path: bool = False,
530
- show_logger_name: bool = False,
531
- colors: dict[str, str] | None = None,
532
- ) -> None:
533
- """Internal function to setup logging - use CONFIG.apply() instead.
534
-
535
- Configures the flixopt logger with console and/or file handlers.
536
- If no handlers are configured, adds NullHandler (library best practice).
537
-
538
- Args:
539
- default_level: Logging level for the logger.
540
- log_file: Path to log file (None to disable file logging).
541
- use_rich_handler: Use Rich for enhanced console output.
542
- console: Enable console logging.
543
- max_file_size: Maximum log file size before rotation.
544
- backup_count: Number of backup log files to keep.
545
- date_format: Date/time format for log messages.
546
- format: Log message format string.
547
- console_width: Console width for Rich handler.
548
- show_path: Show file paths in log messages (Rich only).
549
- show_logger_name: Show logger name in log messages.
550
- colors: ANSI color codes for each log level.
551
- """
552
- logger = logging.getLogger('flixopt')
553
- logger.setLevel(getattr(logging, default_level.upper()))
554
- logger.propagate = False # Prevent duplicate logs
555
- logger.handlers.clear()
556
-
557
- # Handle console parameter: False = disabled, True = stdout, 'stdout' = stdout, 'stderr' = stderr
558
- if console:
559
- # Convert True to 'stdout', keep 'stdout'/'stderr' as-is
560
- stream = 'stdout' if console is True else console
561
- logger.addHandler(
562
- _create_console_handler(
563
- use_rich=use_rich_handler,
564
- stream=stream,
565
- console_width=console_width,
566
- show_path=show_path,
567
- show_logger_name=show_logger_name,
568
- date_format=date_format,
569
- format=format,
570
- colors=colors,
571
- )
572
- )
773
+ Respects FLIXOPT_CI environment variable if set.
573
774
 
574
- if log_file:
575
- logger.addHandler(
576
- _create_file_handler(
577
- log_file=log_file,
578
- max_file_size=max_file_size,
579
- backup_count=backup_count,
580
- show_logger_name=show_logger_name,
581
- date_format=date_format,
582
- format=format,
583
- )
584
- )
775
+ Examples:
776
+ ```python
777
+ CONFIG.browser_plotting()
778
+ result.plot() # Opens in browser instead of inline
779
+ ```
780
+ """
781
+ cls.Plotting.default_show = True
585
782
 
586
- # Library best practice: NullHandler if no handlers configured
587
- if not logger.handlers:
588
- logger.addHandler(logging.NullHandler())
783
+ # Only set to True if environment variable hasn't overridden it
784
+ if 'FLIXOPT_CI' not in os.environ:
785
+ import plotly.io as pio
589
786
 
787
+ pio.renderers.default = 'browser'
590
788
 
591
- def change_logging_level(level_name: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']):
592
- """Change the logging level for the flixopt logger and all its handlers.
789
+ # Activate flixopt theme
790
+ cls.use_theme()
593
791
 
594
- .. deprecated:: 2.1.11
595
- Use ``CONFIG.Logging.level = level_name`` and ``CONFIG.apply()`` instead.
596
- This function will be removed in version 3.0.0.
792
+ return cls
597
793
 
598
- Args:
599
- level_name: The logging level to set.
794
+ @classmethod
795
+ def use_theme(cls) -> type[CONFIG]:
796
+ """Activate the flixopt plotly theme as the default template.
600
797
 
601
- Examples:
602
- >>> change_logging_level('DEBUG') # deprecated
603
- >>> # Use this instead:
604
- >>> CONFIG.Logging.level = 'DEBUG'
605
- >>> CONFIG.apply()
798
+ Sets ``plotly.io.templates.default = 'plotly_white+flixopt'``.
799
+
800
+ The 'flixopt' template is registered automatically on import with colorscales
801
+ from CONFIG.Plotting. Call this method to make it the default for all plots.
802
+
803
+ Returns:
804
+ The CONFIG class for method chaining.
805
+
806
+ Examples:
807
+ ```python
808
+ # Activate flixopt theme globally
809
+ CONFIG.use_theme()
810
+
811
+ # Or combine with other setup
812
+ CONFIG.notebook() # Already calls use_theme() internally
813
+
814
+ # Per-figure usage (without setting global default)
815
+ fig.update_layout(template='plotly_white+flixopt')
816
+ ```
817
+ """
818
+ import plotly.io as pio
819
+
820
+ # Re-register template to pick up any config changes made after import
821
+ _register_flixopt_template()
822
+ pio.templates.default = 'plotly_white+flixopt'
823
+ return cls
824
+
825
+ @classmethod
826
+ def notebook(cls) -> type[CONFIG]:
827
+ """Configure for Jupyter notebook environments.
828
+
829
+ Optimizes settings for notebook usage:
830
+ - Sets plotly renderer to 'notebook' for inline display (unless PLOTLY_RENDERER env var is set)
831
+ - Disables automatic plot.show() calls (notebooks display via _repr_html_)
832
+ - Enables SUCCESS-level console logging
833
+ - Disables solver console output for cleaner notebook cells
834
+
835
+ Note:
836
+ The plotly renderer can be overridden by setting the PLOTLY_RENDERER
837
+ environment variable (e.g., 'notebook_connected' for CDN-based rendering).
838
+
839
+ Examples:
840
+ ```python
841
+ # At the start of your notebook
842
+ import flixopt as fx
843
+
844
+ fx.CONFIG.notebook()
845
+
846
+ # Now plots display inline automatically
847
+ flow_system.statistics.plot.balance('Heat') # Displays inline
848
+ ```
849
+ """
850
+ import plotly.io as pio
851
+
852
+ # Set plotly to render inline in notebooks (respect PLOTLY_RENDERER env var)
853
+ if 'PLOTLY_RENDERER' not in os.environ:
854
+ pio.renderers.default = 'notebook'
855
+
856
+ # Activate flixopt theme
857
+ cls.use_theme()
858
+
859
+ # Disable default show since notebooks render via _repr_html_
860
+ cls.Plotting.default_show = False
861
+
862
+ # Light logging - SUCCESS level without too much noise
863
+ cls.Logging.enable_console('SUCCESS')
864
+
865
+ # Disable verbose solver output for cleaner notebook cells
866
+ cls.Solving.log_to_console = False
867
+ cls.Solving.log_main_results = False
868
+
869
+ return cls
870
+
871
+ @classmethod
872
+ def load_from_file(cls, config_file: str | Path) -> type[CONFIG]:
873
+ """Load configuration from YAML file and apply it.
874
+
875
+ Args:
876
+ config_file: Path to the YAML configuration file.
877
+
878
+ Returns:
879
+ The CONFIG class for method chaining.
880
+
881
+ Raises:
882
+ FileNotFoundError: If the config file does not exist.
883
+
884
+ Examples:
885
+ ```python
886
+ CONFIG.load_from_file('my_config.yaml')
887
+ ```
888
+
889
+ Example YAML file:
890
+ ```yaml
891
+ config_name: my_project
892
+ modeling:
893
+ big: 10000000
894
+ epsilon: 0.00001
895
+ solving:
896
+ mip_gap: 0.001
897
+ time_limit_seconds: 600
898
+ plotting:
899
+ default_engine: matplotlib
900
+ default_dpi: 600
901
+ ```
902
+ """
903
+ # Import here to avoid circular import
904
+ from . import io as fx_io
905
+
906
+ config_path = Path(config_file)
907
+ if not config_path.exists():
908
+ raise FileNotFoundError(f'Config file not found: {config_file}')
909
+
910
+ config_dict = fx_io.load_yaml(config_path)
911
+ cls._apply_config_dict(config_dict)
912
+
913
+ return cls
914
+
915
+ @classmethod
916
+ def _apply_config_dict(cls, config_dict: dict) -> None:
917
+ """Apply configuration dictionary to class attributes.
918
+
919
+ Args:
920
+ config_dict: Dictionary containing configuration values.
921
+ """
922
+ for key, value in config_dict.items():
923
+ if key == 'modeling' and isinstance(value, dict):
924
+ for nested_key, nested_value in value.items():
925
+ if hasattr(cls.Modeling, nested_key):
926
+ setattr(cls.Modeling, nested_key, nested_value)
927
+ elif key == 'solving' and isinstance(value, dict):
928
+ for nested_key, nested_value in value.items():
929
+ if hasattr(cls.Solving, nested_key):
930
+ setattr(cls.Solving, nested_key, nested_value)
931
+ elif key == 'plotting' and isinstance(value, dict):
932
+ for nested_key, nested_value in value.items():
933
+ if hasattr(cls.Plotting, nested_key):
934
+ setattr(cls.Plotting, nested_key, nested_value)
935
+ elif hasattr(cls, key) and key != 'logging':
936
+ # Skip 'logging' as it requires special handling via CONFIG.Logging methods
937
+ setattr(cls, key, value)
938
+
939
+
940
+ def _register_flixopt_template() -> None:
941
+ """Register the 'flixopt' plotly template (called on module import).
942
+
943
+ This makes the template available as 'flixopt' or 'plotly_white+flixopt',
944
+ but does NOT set it as the default. Users must call CONFIG.use_theme()
945
+ to activate it globally, or use it per-figure via template='flixopt'.
606
946
  """
607
- warnings.warn(
608
- 'change_logging_level is deprecated and will be removed in version 3.0.0. '
609
- 'Use CONFIG.Logging.level = level_name and CONFIG.apply() instead.',
610
- DeprecationWarning,
611
- stacklevel=2,
947
+ import logging
948
+
949
+ import plotly.graph_objects as go
950
+ import plotly.io as pio
951
+ from plotly.express import colors
952
+
953
+ # Get colorway from qualitative colorscale name
954
+ # Use .title() for multi-word names like 'dark24' -> 'Dark24'
955
+ colorscale_name = CONFIG.Plotting.default_qualitative_colorscale.title()
956
+ colorway = getattr(colors.qualitative, colorscale_name, None)
957
+
958
+ # Fall back to Plotly default if colorscale not found
959
+ if colorway is None:
960
+ logging.getLogger(__name__).warning(
961
+ f"Colorscale '{CONFIG.Plotting.default_qualitative_colorscale}' not found in "
962
+ f"plotly.express.colors.qualitative, falling back to 'Plotly'. "
963
+ f'Available: {[n for n in dir(colors.qualitative) if not n.startswith("_")]}'
964
+ )
965
+ colorway = colors.qualitative.Plotly
966
+
967
+ pio.templates['flixopt'] = go.layout.Template(
968
+ layout=go.Layout(
969
+ colorway=colorway,
970
+ colorscale=dict(
971
+ sequential=CONFIG.Plotting.default_sequential_colorscale,
972
+ ),
973
+ )
612
974
  )
613
- logger = logging.getLogger('flixopt')
614
- logging_level = getattr(logging, level_name.upper())
615
- logger.setLevel(logging_level)
616
- for handler in logger.handlers:
617
- handler.setLevel(logging_level)
618
975
 
619
976
 
620
- # Initialize default config
621
- CONFIG.apply()
977
+ # Register flixopt template on import (no side effects - just makes it available)
978
+ _register_flixopt_template()