Skip to content

Commit 2fed1fd

Browse files
authored
Devs/rich logging (#105)
* Using the package rich for formated logging instead of own formater * Adding logging formater for logging to file * Re-adding the old custom formater (simplified), to enable colored logs in the python console * Adding rich.print() to examples * Changing rich.print() to rich.pprint() * Adjusted colors of logging to match RichHandler * Default to not use the rich Handler
1 parent 352fce0 commit 2fed1fd

File tree

7 files changed

+90
-46
lines changed

7 files changed

+90
-46
lines changed

examples/Ex00_minmal/minimal_example.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"""
22
This script shows how to use the flixOpt framework to model a super minimalistic energy system.
33
"""
4+
import flixOpt as fx
45

56
import numpy as np
6-
import flixOpt as fx
7+
from rich.pretty import pprint
78

89
if __name__ == '__main__':
910

@@ -54,5 +55,6 @@
5455
results.plot_operation('District Heating', 'area')
5556

5657
# Print results to the console. Check Results in file or perform more plotting
57-
print(calculation.results())
58-
print(f'Look into .yaml and .json file for results')
58+
pprint(calculation.results())
59+
pprint(f'Look into .yaml and .json file for results')
60+
pprint(calculation.system_model.main_results)

examples/Ex01_simple/simple_example.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""
22
THis script shows how to use the flixOpt framework to model a simple energy system.
33
"""
4-
54
import numpy as np
5+
from rich.pretty import pprint # Used for pretty printing
66
import flixOpt as fx
77

88
if __name__ == '__main__':
@@ -96,4 +96,4 @@
9696

9797
# Convert the results for the storage component to a dataframe and display
9898
results.to_dataframe('Storage')
99-
print(results.all_results)
99+
pprint(results.all_results)

examples/Ex02_complex/complex_example.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""
22
This script shows how to use the flixOpt framework to model a more complex energy system.
33
"""
4-
54
import numpy as np
65
import pandas as pd
6+
from rich.pretty import pprint # Used for pretty printing
77
import flixOpt as fx
88

99
if __name__ == '__main__':
@@ -125,15 +125,15 @@
125125
flow_system.add_elements(Costs, CO2, PE, Gaskessel, Waermelast, Gasbezug, Stromverkauf, aSpeicher)
126126
flow_system.add_elements(aKWK2) if use_chp_with_segments else flow_system.add_components(aKWK)
127127

128-
print(flow_system) # Get a string representation of the FlowSystem
128+
pprint(flow_system) # Get a string representation of the FlowSystem
129129

130130
# --- Solve FlowSystem ---
131131
calculation = fx.FullCalculation('Sim1', flow_system, 'pyomo', time_indices)
132132
calculation.do_modeling()
133133

134134
# Show variables as str (else, you can find them in the results.yaml file
135-
print(calculation.system_model.description_of_constraints())
136-
print(calculation.system_model.description_of_variables())
135+
pprint(calculation.system_model.description_of_constraints())
136+
pprint(calculation.system_model.description_of_variables())
137137

138138
calculation.solve(fx.solvers.HighsSolver(mip_gap=0.005, time_limit_seconds=30), # Specify which solver you want to use and specify parameters
139139
save_results='results') # If and where to save results

examples/Ex03_calculation_types/example_calculation_types.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import numpy as np
1010
import pandas as pd
11+
from rich.pretty import pprint # Used for pretty printing
1112

1213
import flixOpt as fx
1314

@@ -148,7 +149,7 @@
148149
calculation.solve(fx.solvers.HighsSolver())
149150
calculations['Aggregated'] = calculation
150151
results['Aggregated'] = calculations['Aggregated'].results()
151-
152+
pprint(results)
152153

153154
def extract_result(results_data: dict[str, dict], keys: List[str]) -> Dict[str,Union[int, float, np.ndarray]]:
154155
"""

flixOpt/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
"""
44

55
from .commons import *
6-
setup_logging('INFO')
6+
setup_logging('INFO', use_rich_handler=False)

flixOpt/core.py

Lines changed: 75 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import inspect
99

1010
import numpy as np
11+
from rich.logging import RichHandler
12+
from rich.console import Console
1113

1214
from . import utils
1315

@@ -286,57 +288,95 @@ def as_effect_dict_with_ts(name_of_param: str,
286288
return effect_ts_dict
287289

288290

289-
# TODO: Move logging to utils.py
290-
class CustomFormatter(logging.Formatter):
291+
class MultilineFormater(logging.Formatter):
292+
293+
def format(self, record):
294+
message_lines = record.getMessage().split('\n')
295+
296+
# Prepare the log prefix (timestamp + log level)
297+
timestamp = self.formatTime(record, self.datefmt)
298+
log_level = record.levelname.ljust(8) # Align log levels for consistency
299+
log_prefix = f"{timestamp} | {log_level} |"
300+
301+
# Format all lines
302+
first_line = [f'{log_prefix} {message_lines[0]}']
303+
if len(message_lines) > 1:
304+
lines = first_line + [f"{log_prefix} {line}" for line in message_lines[1:]]
305+
else:
306+
lines = first_line
307+
308+
return '\n'.join(lines)
309+
310+
311+
class ColoredMultilineFormater(MultilineFormater):
291312
# ANSI escape codes for colors
292313
COLORS = {
293-
'DEBUG': '\033[96m', # Cyan
294-
'INFO': '\033[92m', # Green
295-
'WARNING': '\033[93m', # Yellow
296-
'ERROR': '\033[91m', # Red
297-
'CRITICAL': '\033[91m\033[1m', # Bold Red
314+
'DEBUG': '\033[32m', # Green
315+
'INFO': '\033[34m', # Blue
316+
'WARNING': '\033[33m', # Yellow
317+
'ERROR': '\033[31m', # Red
318+
'CRITICAL': '\033[1m\033[31m', # Bold Red
298319
}
299320
RESET = '\033[0m'
300321

301322
def format(self, record):
323+
lines = super().format(record).splitlines()
302324
log_color = self.COLORS.get(record.levelname, self.RESET)
303-
original_message = record.getMessage()
304-
message_lines = original_message.split('\n')
305325

306326
# Create a formatted message for each line separately
307327
formatted_lines = []
308-
for line in message_lines:
309-
temp_record = logging.LogRecord(
310-
record.name, record.levelno, record.pathname, record.lineno,
311-
line, record.args, record.exc_info, record.funcName, record.stack_info
312-
)
313-
formatted_line = super().format(temp_record)
314-
formatted_lines.append(f"{log_color}{formatted_line}{self.RESET}")
315-
316-
formatted_message = '\n'.join(formatted_lines)
317-
return formatted_message
318-
319-
320-
def setup_logging(level_name: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']):
328+
for line in lines:
329+
formatted_lines.append(f"{log_color}{line}{self.RESET}")
330+
331+
return '\n'.join(formatted_lines)
332+
333+
334+
def _get_logging_handler(log_file: Optional[str] = None,
335+
use_rich_handler: bool = False) -> logging.Handler:
336+
"""Returns a logging handler for the given log file."""
337+
if use_rich_handler and log_file is None:
338+
# RichHandler for console output
339+
console = Console(width=120)
340+
rich_handler = RichHandler(
341+
console=console,
342+
rich_tracebacks=True,
343+
omit_repeated_times=True,
344+
show_path=False,
345+
log_time_format="%Y-%m-%d %H:%M:%S",
346+
)
347+
rich_handler.setFormatter(logging.Formatter("%(message)s")) # Simplified formatting
348+
349+
return rich_handler
350+
elif log_file is None:
351+
# Regular Logger with custom formating enabled
352+
file_handler = logging.StreamHandler()
353+
file_handler.setFormatter(ColoredMultilineFormater(
354+
fmt="%(message)s",
355+
datefmt="%Y-%m-%d %H:%M:%S",
356+
))
357+
return file_handler
358+
else:
359+
# FileHandler for file output
360+
file_handler = logging.FileHandler(log_file)
361+
file_handler.setFormatter(MultilineFormater(
362+
fmt="%(message)s",
363+
datefmt="%Y-%m-%d %H:%M:%S",
364+
))
365+
return file_handler
366+
367+
def setup_logging(default_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = 'INFO',
368+
log_file: Optional[str] = 'flixOpt.log',
369+
use_rich_handler: bool = False):
321370
"""Setup logging configuration"""
322371
logger = logging.getLogger('flixOpt') # Use a specific logger name for your package
323-
logging_level = get_logging_level_by_name(level_name)
324-
logger.setLevel(logging_level)
325-
372+
logger.setLevel(get_logging_level_by_name(default_level))
326373
# Clear existing handlers
327374
if logger.hasHandlers():
328375
logger.handlers.clear()
329376

330-
# Create console handler
331-
c_handler = logging.StreamHandler()
332-
c_handler.setLevel(logging_level)
333-
334-
# Create a clean and aligned formatter
335-
log_format = '%(asctime)s - %(levelname)-8s : %(message)s'
336-
date_format = '%Y-%m-%d %H:%M:%S'
337-
c_format = CustomFormatter(log_format, datefmt=date_format)
338-
c_handler.setFormatter(c_format)
339-
logger.addHandler(c_handler)
377+
logger.addHandler(_get_logging_handler(use_rich_handler=use_rich_handler))
378+
if log_file is not None:
379+
logger.addHandler(_get_logging_handler(log_file, use_rich_handler=False))
340380

341381
return logger
342382

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dependencies = [
3333
"numpy >= 1.21.5, < 2",
3434
"PyYAML >= 6.0",
3535
"Pyomo >= 6.4.2",
36+
"rich >= 13.0.1",
3637
"tsam >= 2.3.1", # Used for time series aggregation
3738
"highspy >= 1.5.3", # Default solver
3839
"pandas >= 2, < 3", # Used in post-processing

0 commit comments

Comments
 (0)