Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SimulatedTrace() class or trace functionality #24

Open
TomMonks opened this issue Jun 15, 2024 · 5 comments
Open

Add SimulatedTrace() class or trace functionality #24

TomMonks opened this issue Jun 15, 2024 · 5 comments
Assignees
Labels
enhancement New feature or request

Comments

@TomMonks
Copy link
Owner

Add a trace class

General idea:

  • decorate the print function.
  • Have an experiment settable log_level e.g. -1
  • print method that prints message if the print log_level is > self.log_level

This will need a new submodule e.g.io or display or output

Example implementation:

class SimulatedTrace:
    def __init__(self, log_level=-1):
        self.log_level= log_level

   def print(self, msg, log_level=0):
       if (log_level > self.log_level):
          print(msg)

Example one. Default log_level

trace = SimulatedTrace()

# this will be printed in the experiment 
trace.print("hello world")

# this will also be printed in the experiment
trace.print("hello world", log_level=1)

Example 2: log_level=1:

trace2 = SimulatedTrace(log_level=1)

# this will not be printed as log_level is -1 by default
trace2.print("hello world")

# this will be printed in this experiment
trace2.print("level 1 message", log_level=1)

# this will also be printed 
trace2.print("level 2 message", log_level=2)

Example 2: log_level=2:

trace3 = SimulatedTrace(log_level=2)

# this will not be printed as log_level is -1 by default
trace3.print("hello world")

# this will NOT be printed in this experiment
trace3.print("level 1 message", log_level=1)

# this will be printed 
trace3.print("level 2 message", log_level=2)
@TomMonks TomMonks added the enhancement New feature or request label Jun 15, 2024
@TomMonks
Copy link
Owner Author

experimenting with the following class in treat_sim docs. This can be modified to print to file.

class SimulatedTrace:
    '''
    Utility class for printing a trace as the
    simulation model executes.
    '''
    def __init__(self, trace_level=0):
        '''Simulated Trace
        Log events as they happen.

        Params:
        -------
        log_level: int, optional (default=0)
            Minimum log level of a print statement
            in order for it to be logged.
        '''
        self.trace_level = trace_level
        
    def __call__(self, msg, trace_level=0):
        '''Override callable.
        This makes objects behave like functions.
        decorates the print function. conditional
        logic to print output or not.
        
        Params:
        ------
        msg: str
            string to print to screen.
        
        trace_level: int, optional (default=0)
            minimum trace level in order for the message
            to display
        
        '''
        if (trace_level >= self.trace_level):
            print(msg)

@TomMonks
Copy link
Owner Author

suggested modification is to include the rich package to support formatted text and columns in console and Jupyter.

Recommend importing rprint rather than overriding print to allow flexibility.

https://rich.readthedocs.io/en/stable/introduction.html

@TomMonks
Copy link
Owner Author

other options

  1. a decorator for a process. The downside is that if we want a time then we need to a.) pass the time to the function as an additional parameter or b.) assume there is a self.env parameter
def traceable(debug=True):
    def decorator(cls):   
        class TracedProcess(cls):
            def __init__(self, *args, **kwargs):
                super().__init__(*args, **kwargs)
                self.debug = debug
        
            def trace(self, env, msg):
                if self.debug:
                    console.print(f'[bold blue]{env.now:.3f}: [/bold blue]: {msg}')
                    
        return TracedProcess
    return decorator
  1. subclass simpy.Environment. So this will behave the same as the normal environemnt, but include enhanced tracing functionality.
class TraceableEnvironment(simpy.Environment):
    def __init__(self, debug=False):
        super().__init__()
        self.debug = debug
        
    def trace(self, msg):
        if self.debug:
            console.print(f'[bold blue]{self.now:.3f}: [/bold blue]: {msg}')

@TomMonks
Copy link
Owner Author

of all options the SimulatedTrace class is probably the most transparent. This is because a user has to explicitly create an instance and monkey patch a process class. The downside is that it needs access to env and at the moment it is created in a place where it doesn't have access. Therefore it has to be passed as an additional parameter.

the decorator option is neat, but imo they are ideally used when they keep the original function unchanged.

@TomMonks TomMonks self-assigned this Jun 20, 2024
@TomMonks
Copy link
Owner Author

An inheritance based option seems the best way forward. more technical to implement at a user end, but intent is clear. A draft for experimentation:

from abc import ABC, abstractmethod

class Traceable(ABC):
    '''Provide basic trace functionality to subclass
    
    Abstract base class Traceable
    
    Subclasses must call 
    
    super().__init__(debug=True) in their __init__() method to 
    initialise trace.
    
    This adds 
    '''
    def __init__(self, debug=False):
        self.debug = debug
        self.config = self._default_config()
        self.console = Console()
    
    def _default_config(self):
        config = {
            "name":None, 
            "name_colour":"bold blue", 
            "time_colour":'bold blue', 
            "time_dp":2,
            "message_colour":'black',
            "tracked":None
        }
        return config
        
    
    def _trace_config(self):
        config = {
            "name":None, 
            "name_colour":"bold blue", 
            "time_colour":'bold blue', 
            "time_dp":2,
            "message_colour":'black',
            "tracked":None
        }
        return config
    
    
    def trace(self, time, msg=None, process_id=None):
        '''
        Display a trace of an event
        '''
        
        if not hasattr(self, 'config'):
            raise AttributeError("Your trace has not been initialised. Call super__init__(debug=True) in class initialiser"
                                 "or omit debug for default of no trace.")
        
        # if in debug mode
        if self.debug:
            
            # check for override to default configs
            process_config = self._trace_config()
            self.config.update(process_config)
            
            # conditional logic to limit tracking to specific processes/entities
            if self.config['tracked'] is None or process_id in self.config['tracked']:

                # display and format time stamp
                out = f"[{self.config['time_colour']}][{time:.{self.config['time_dp']}f}]:[/{self.config['time_colour']}]"
                
                # if provided display and format a process ID 
                if self.config['name'] is not None and process_id is not None:
                    out += f"[{self.config['name_colour']}]<{self.config['name']} {process_id}>: [/{self.config['name_colour']}]"

                # format traced event message
                out += f"[{self.config['message_colour']}]{msg}[/{self.config['message_colour']}]"

                # print to rich console
                self.console.print(out)
        

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant