Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,7 @@ cython_debug/

# linting
.ruff_cache

# SimLogger data directory (test artifacts)
data/

57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,63 @@ print(sampleArrayLoaded)
# [1, 2, 3, 4]
```

## Autosave Command Line Arguments
Never forget to save your argparse CLI arguments again! SimLogger provides **automatic** saving of command-line arguments.

### Fully Automatic (Recommended)
Use `SimLogger.ArgumentParser` instead of `argparse.ArgumentParser` for completely automatic saving:

```python
from SimLogger import SimLogger

# Set your simulation tag once
SimLogger.setSimTag('myExperiment')

# Use SimLogger.ArgumentParser instead of argparse.ArgumentParser
parser = SimLogger.ArgumentParser(description='My Experiment')
parser.add_argument('--learning_rate', type=float, default=0.001)
parser.add_argument('--epochs', type=int, default=100)
parser.add_argument('--model_name', type=str, default='resnet50')

# Arguments are AUTOMATICALLY saved when you call parse_args()!
args = parser.parse_args()

# Outputs
# 2024-06-25 10:44:44,063 [INFO ] Logger Loaded
# 2024-06-25 10:44:44,064 [INFO ] ARGS,myExperiment,saved,data/obj/myExperiment_args_2024-06-25_10-44-44.pkl
# 2024-06-25 10:44:44,064 [INFO ] ARG,myExperiment,learning_rate,0.001
# 2024-06-25 10:44:44,064 [INFO ] ARG,myExperiment,epochs,100
# 2024-06-25 10:44:44,064 [INFO ] ARG,myExperiment,model_name,resnet50
```

You can also pass the `simTag` directly to the parser:
```python
parser = SimLogger.ArgumentParser(simTag='myExperiment')
```

### Manual Saving
If you prefer to manually control when arguments are saved, you can use the `saveArgs()` function:

```python
import argparse
from SimLogger import SimLogger

parser = argparse.ArgumentParser()
parser.add_argument('--learning_rate', type=float, default=0.001)
args = parser.parse_args()

# Manually save all CLI arguments
SimLogger.saveArgs('myExperiment', args)
```

### Loading Saved Arguments
Later, you can load the saved arguments:
```python
from SimLogger import SimLogger
loaded_args = SimLogger.getObj('myExperiment', 'args')
print(loaded_args['learning_rate']) # 0.001
```

## Installation
Install the python package
```pip install SimLogger```
Expand Down
127 changes: 127 additions & 0 deletions src/SimLogger/SimLogger.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import pandas as pd
import os
from git import Repo
import argparse

isLoaded = False
_simTag = None # Global variable to store the simulation tag for auto-saving


def setupLogger(fileName="example.log", githubLink=None):
Expand Down Expand Up @@ -64,6 +66,45 @@ def logNotes(notes):
logging.info(notes)


def saveArgs(simTag, args, objFolder=os.path.join("data", "obj"), makeNote=True):
"""Save argparse command line arguments to a pickle file and optionally log them.
This is a convenience function to automatically save CLI arguments from argparse.

Args:
simTag (str): Unique tag for the simulation
args (argparse.Namespace): Parsed arguments from argparse
ArgumentParser.parse_args()
objFolder (str): Folder where the pickled file will be saved
makeNote (bool): Sets whether an additional note is made about the
arguments

Returns:
filePath (str): the unique file path where the pickled arguments are saved

Example:
>>> import argparse
>>> from SimLogger import SimLogger
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('--param1', type=int, default=10)
>>> parser.add_argument('--param2', type=str, default='test')
>>> args = parser.parse_args()
>>> SimLogger.saveArgs('mySimulation', args)
"""
# Convert argparse.Namespace to dictionary for better readability
args_dict = vars(args)

# Save the arguments as a pickled object
filePath = saveObj(simTag, "args", args_dict, objFolder=objFolder, makeNote=False)

# Log each argument individually for easy reference
if makeNote:
logNotes("ARGS," + simTag + ",saved," + filePath)
for key, value in args_dict.items():
logNotes("ARG," + simTag + "," + key + "," + str(value))

return filePath


def saveObj(simTag, objTag, obj, objFolder=os.path.join("data", "obj"), makeNote=False):
"""Save an object (obj) to pickle file in the object folder.
The file name will be the {simTag}_{objTag}_{dateTimeString}.pkl
Expand Down Expand Up @@ -228,3 +269,89 @@ def isSimTagUsed(simTag, objFolder=os.path.join("data", "obj")):
if len(fileList) > 0:
return False
return True


class ArgumentParser(argparse.ArgumentParser):
"""Custom ArgumentParser that automatically saves arguments when
parse_args() is called.

This class extends argparse.ArgumentParser to provide automatic saving of
command-line arguments. When parse_args() is called, the arguments are
automatically saved using saveArgs() if a simulation tag has been set.

To use this feature:
1. Set the simulation tag using setSimTag() before parsing arguments
2. Use SimLogger.ArgumentParser instead of argparse.ArgumentParser
3. Call parse_args() as usual - arguments will be saved automatically

Example:
>>> from SimLogger import SimLogger
>>> SimLogger.setSimTag('myExperiment')
>>> parser = SimLogger.ArgumentParser()
>>> parser.add_argument('--learning_rate', type=float, default=0.001)
>>> args = parser.parse_args() # Arguments are automatically saved!
"""

def __init__(self, *args, simTag=None, autoSave=True, **kwargs):
"""Initialize the ArgumentParser.

Args:
simTag (str): Optional simulation tag for auto-saving arguments.
If not provided, uses the global simTag set by setSimTag().
autoSave (bool): Whether to automatically save arguments when
parse_args() is called. Default is True.
*args, **kwargs: All other arguments are passed to
argparse.ArgumentParser
"""
super().__init__(*args, **kwargs)
self._simTag = simTag
self._autoSave = autoSave

def parse_args(self, args=None, namespace=None):
"""Parse arguments and automatically save them if autoSave is enabled.

Args:
args: List of strings to parse. If None, uses sys.argv.
namespace: Object to populate with parsed arguments.

Returns:
Namespace object with parsed arguments
"""
parsed_args = super().parse_args(args, namespace)

# Auto-save if enabled and we have a simTag
if self._autoSave:
simTag = self._simTag if self._simTag is not None else _simTag
if simTag is not None:
saveArgs(simTag, parsed_args)
else:
# Log a warning if auto-save is enabled but no simTag is set
global isLoaded
if not isLoaded:
setupLogger()
logging.warning(
"ArgumentParser auto-save is enabled but no simTag is set. "
"Use setSimTag() or pass simTag to ArgumentParser constructor."
)

return parsed_args


def setSimTag(simTag):
"""Set the global simulation tag for automatic argument saving.

This sets the simulation tag that will be used by SimLogger.ArgumentParser
for automatic saving of parsed arguments.

Args:
simTag (str): Unique tag for the simulation

Example:
>>> from SimLogger import SimLogger
>>> SimLogger.setSimTag('myExperiment')
>>> parser = SimLogger.ArgumentParser()
>>> parser.add_argument('--param1', type=int)
>>> args = parser.parse_args() # Automatically saved to 'myExperiment'
"""
global _simTag
_simTag = simTag
105 changes: 105 additions & 0 deletions tests/test_SimLogger.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from SimLogger import SimLogger
import argparse


def test_save_and_get():
Expand All @@ -10,3 +11,107 @@ def test_save_and_get():
aLoadedObjTag = SimLogger.getObj(simTag, objTag)
assert a == aLoadedUniqueId
assert a == aLoadedObjTag


def test_save_args():
# Create a simple argparse parser and parse some test arguments
parser = argparse.ArgumentParser()
parser.add_argument("--param1", type=int, default=42)
parser.add_argument("--param2", type=str, default="test_value")
parser.add_argument("--param3", type=float, default=3.14)

# Parse with default values
args = parser.parse_args([])

simTag = "testing-args-simTag"

# Save the args
filePath = SimLogger.saveArgs(simTag, args)

# Verify the file was created
assert filePath is not None
assert "args" in filePath
assert simTag in filePath

# Load the args back
loaded_args = SimLogger.getObj(simTag, "args")

# Verify the loaded args match the original
assert loaded_args["param1"] == 42
assert loaded_args["param2"] == "test_value"
assert loaded_args["param3"] == 3.14


def test_save_args_with_custom_values():
# Create a parser and parse with custom values
parser = argparse.ArgumentParser()
parser.add_argument("--learning_rate", type=float, default=0.001)
parser.add_argument("--epochs", type=int, default=100)
parser.add_argument("--model_name", type=str, default="default")

# Parse with custom values
args = parser.parse_args(
["--learning_rate", "0.01", "--epochs", "50", "--model_name", "custom_model"]
)

simTag = "testing-custom-args"

# Save the args
SimLogger.saveArgs(simTag, args, makeNote=True)

# Load the args back
loaded_args = SimLogger.getObj(simTag, "args")

# Verify the loaded args match the custom values
assert loaded_args["learning_rate"] == 0.01
assert loaded_args["epochs"] == 50
assert loaded_args["model_name"] == "custom_model"


def test_argument_parser_auto_save_with_simTag_param():
# Test automatic saving using ArgumentParser with simTag parameter
parser = SimLogger.ArgumentParser(simTag="auto-save-test-1")
parser.add_argument("--param1", type=int, default=100)
parser.add_argument("--param2", type=str, default="auto_test")

# Parse args - should automatically save
parser.parse_args([])

# Verify the args were automatically saved
loaded_args = SimLogger.getObj("auto-save-test-1", "args")
assert loaded_args["param1"] == 100
assert loaded_args["param2"] == "auto_test"


def test_argument_parser_auto_save_with_setSimTag():
# Test automatic saving using ArgumentParser with global setSimTag
SimLogger.setSimTag("auto-save-test-2")
parser = SimLogger.ArgumentParser()
parser.add_argument("--learning_rate", type=float, default=0.002)
parser.add_argument("--batch_size", type=int, default=64)

# Parse args - should automatically save using global simTag
parser.parse_args(["--learning_rate", "0.005", "--batch_size", "128"])

# Verify the args were automatically saved
loaded_args = SimLogger.getObj("auto-save-test-2", "args")
assert loaded_args["learning_rate"] == 0.005
assert loaded_args["batch_size"] == 128


def test_argument_parser_auto_save_disabled():
# Test that auto-save can be disabled
parser = SimLogger.ArgumentParser(simTag="should-not-save", autoSave=False)
parser.add_argument("--param1", type=int, default=999)

# Parse args - should NOT automatically save
parser.parse_args([])

# Verify the args were NOT saved
import os

objFolder = os.path.join("data", "obj")
fileList = os.listdir(objFolder)
matching_files = [f for f in fileList if "should-not-save" in f]
# Should have no files since auto-save was disabled
assert len(matching_files) == 0