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.
- flixopt/__init__.py +57 -49
- flixopt/carrier.py +159 -0
- flixopt/clustering/__init__.py +51 -0
- flixopt/clustering/base.py +1746 -0
- flixopt/clustering/intercluster_helpers.py +201 -0
- flixopt/color_processing.py +372 -0
- flixopt/comparison.py +819 -0
- flixopt/components.py +848 -270
- flixopt/config.py +853 -496
- flixopt/core.py +111 -98
- flixopt/effects.py +294 -284
- flixopt/elements.py +484 -223
- flixopt/features.py +220 -118
- flixopt/flow_system.py +2026 -389
- flixopt/interface.py +504 -286
- flixopt/io.py +1718 -55
- flixopt/linear_converters.py +291 -230
- flixopt/modeling.py +304 -181
- flixopt/network_app.py +2 -1
- flixopt/optimization.py +788 -0
- flixopt/optimize_accessor.py +373 -0
- flixopt/plot_result.py +143 -0
- flixopt/plotting.py +1177 -1034
- flixopt/results.py +1331 -372
- flixopt/solvers.py +12 -4
- flixopt/statistics_accessor.py +2412 -0
- flixopt/stats_accessor.py +75 -0
- flixopt/structure.py +954 -120
- flixopt/topology_accessor.py +676 -0
- flixopt/transform_accessor.py +2277 -0
- flixopt/types.py +120 -0
- flixopt-6.0.0rc7.dist-info/METADATA +290 -0
- flixopt-6.0.0rc7.dist-info/RECORD +36 -0
- {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/WHEEL +1 -1
- flixopt/aggregation.py +0 -382
- flixopt/calculation.py +0 -672
- flixopt/commons.py +0 -51
- flixopt/utils.py +0 -86
- flixopt-3.0.1.dist-info/METADATA +0 -209
- flixopt-3.0.1.dist-info/RECORD +0 -26
- {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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
|
-
|
|
12
|
-
from
|
|
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
|
-
|
|
14
|
+
try:
|
|
15
|
+
import colorlog
|
|
16
|
+
from colorlog.escape_codes import escape_codes
|
|
18
17
|
|
|
19
|
-
|
|
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
|
-
|
|
74
|
-
CONFIG.Logging.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
212
|
+
flixopt is silent by default (WARNING level, no handlers).
|
|
92
213
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
150
|
-
CONFIG.Logging.
|
|
151
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
523
|
+
class Solving:
|
|
524
|
+
"""Solver configuration and default parameters.
|
|
189
525
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
251
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
|
268
|
-
"""
|
|
613
|
+
def reset(cls) -> None:
|
|
614
|
+
"""Reset all configuration values to defaults.
|
|
269
615
|
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
326
|
-
|
|
687
|
+
Disables all logging, solver output, and result logging
|
|
688
|
+
for clean production runs. Does not show plots.
|
|
327
689
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
703
|
+
@classmethod
|
|
704
|
+
def debug(cls) -> type[CONFIG]:
|
|
705
|
+
"""Configure for debug mode with verbose output.
|
|
337
706
|
|
|
338
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
721
|
+
@classmethod
|
|
722
|
+
def exploring(cls) -> type[CONFIG]:
|
|
723
|
+
"""Configure for exploring flixopt.
|
|
352
724
|
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
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
|
-
|
|
361
|
-
|
|
746
|
+
Enables file logging only (no console output), disables plots,
|
|
747
|
+
and disables solver console output for clean production runs.
|
|
362
748
|
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
592
|
-
|
|
789
|
+
# Activate flixopt theme
|
|
790
|
+
cls.use_theme()
|
|
593
791
|
|
|
594
|
-
|
|
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
|
-
|
|
599
|
-
|
|
794
|
+
@classmethod
|
|
795
|
+
def use_theme(cls) -> type[CONFIG]:
|
|
796
|
+
"""Activate the flixopt plotly theme as the default template.
|
|
600
797
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
#
|
|
621
|
-
|
|
977
|
+
# Register flixopt template on import (no side effects - just makes it available)
|
|
978
|
+
_register_flixopt_template()
|