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

feat/allow-duplicate-exp-name #713

Merged
merged 14 commits into from
Sep 20, 2024
2 changes: 0 additions & 2 deletions .github/workflows/test-when-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ on:
paths:
- swanlab/**
- test/**
branches:
- main

jobs:
test:
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
swankit==0.1.1b1
swanboard==0.1.3b5
swankit==0.1.1b3
swanboard==0.1.4b1
cos-python-sdk-v5
urllib3>=1.26.0
requests>=2.25.0
Expand Down
30 changes: 28 additions & 2 deletions swanlab/data/formater.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ def _auto_cut(name: str, value: str, max_len: int, cut: bool) -> str:

def check_proj_name_format(name: str, auto_cut: bool = True) -> str:
"""
检查项目名格式,必须是0-9a-zA-Z以及连字符(_-.+)
最大长度为100个字符
检查实验名称格式,最大长度为95个字符,一个中文字符算一个字符
其他不做限制,实验名称可以包含任何字符

Parameters
----------
Expand Down Expand Up @@ -121,6 +121,32 @@ def check_proj_name_format(name: str, auto_cut: bool = True) -> str:
return _auto_cut("project", name, max_len, auto_cut)


def check_exp_name_format(name: str, auto_cut: bool = True) -> str:
"""
检查实验名称格式,最大长度为95个字符,一个中文字符算一个字符
其他不做限制,实验名称可以包含任何字符
:param name: 实验名称
:param auto_cut: 是否自动截断,默认为True
:return: str 检查后的字符串
"""
max_len = 95
if not check_string(name):
raise ValueError("Experiment name is an empty string")
name = name.strip()
return _auto_cut("experiment", name, max_len, auto_cut)


def check_desc_format(desc: str, auto_cut: bool = True) -> str:
"""
检查描述格式,最大长度为255个字符,一个中文字符算一个字符
:param desc: 描述信息
:param auto_cut: 是否自动截断,默认为True
:return: str 检查后的字符串
"""
max_len = 255
return _auto_cut("description", desc, max_len, auto_cut)


def check_key_format(key: str, auto_cut=True) -> str:
"""检查key字符串格式
不能超过255个字符,可以包含任何字符,不允许.和/以及空格开头
Expand Down
9 changes: 4 additions & 5 deletions swanlab/data/run/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
@Description:
回调函数操作员,批量处理回调函数的调用
"""
from typing import List, Union, Dict, Any, Callable
from typing import List, Union, Dict, Any, Tuple
from swankit.callback import SwanKitCallback, MetricInfo, ColumnInfo, OperateErrorInfo, RuntimeInfo
from swankit.core import SwanLabSharedSettings
import swanlab.error as E
Expand Down Expand Up @@ -56,7 +56,7 @@ def __run_all(self, method: str, *args, **kwargs):
@classmethod
def parse_return(cls, ret: OperatorReturnType, key: str = None):
"""
解析返回值,选择不为None的返回值
解析返回值,选择不为None的返回值,如果都为None,则返回None
如果key不为None,则返回对应key的返回值
:param ret: 返回值
:param key: 返回值的key
Expand All @@ -78,10 +78,9 @@ def before_init_experiment(
exp_name: str,
description: str,
num: int,
suffix: str,
setter: Callable[[str, str, str, str], None],
colors: Tuple[str, str],
):
return self.__run_all("before_init_experiment", run_id, exp_name, description, num, suffix, setter)
return self.__run_all("before_init_experiment", run_id, exp_name, description, num, colors)

def on_run(self):
try:
Expand Down
52 changes: 25 additions & 27 deletions swanlab/data/run/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
from datetime import datetime
from typing import Callable, Optional, Dict
from .helper import SwanLabRunOperator, RuntimeInfo
from ..formater import check_key_format
from ..formater import check_key_format, check_exp_name_format, check_desc_format
from swanlab.env import get_mode, get_swanlog_dir
from . import namer as N
import random


Expand Down Expand Up @@ -50,7 +51,6 @@ def __init__(
description: str = None,
run_config=None,
log_level: str = None,
suffix: str = None,
exp_num: int = None,
operator: SwanLabRunOperator = SwanLabRunOperator(),
):
Expand All @@ -74,9 +74,6 @@ def __init__(
当前实验的日志等级,默认为 'info',可以从 'debug' 、'info'、'warning'、'error'、'critical' 中选择
不区分大小写,如果不提供此参数(为None),则默认为 'info'
如果提供的日志等级不在上述范围内,默认改为info
suffix : str, optional
实验名称后缀,用于区分同名实验,格式为yyyy-mm-dd_HH-MM-SS
如果不提供此参数(为None),不会添加后缀
exp_num : int, optional
历史实验总数,用于云端颜色与本地颜色的对应
operator : SwanLabRunOperator, optional
Expand Down Expand Up @@ -106,14 +103,15 @@ def __init__(
# 如果config是以下几个类别之一,则抛出异常
if isinstance(run_config, (int, float, str, bool, list, tuple, set)):
raise TypeError(
f"config: {run_config} (type: {type(run_config)}) is not a json serialized dict (Surpport type is dict, MutableMapping, omegaconf.DictConfig, Argparse.Namespace), please check it"
f"config: {run_config} (type: {type(run_config)}) is not a json serialized dict "
f"(Support type is dict, MutableMapping, omegaconf.DictConfig, Argparse.Namespace), please check it"
)
global config
config.update(run_config)
setattr(config, "_SwanLabConfig__on_setter", self.__operator.on_runtime_info_update)
self.__config = config
# ---------------------------------- 注册实验 ----------------------------------
self.__exp: SwanLabExp = self.__register_exp(experiment_name, description, suffix, num=exp_num)
self.__exp: SwanLabExp = self.__register_exp(experiment_name, description, num=exp_num)
# 实验状态标记,如果status不为0,则无法再次调用log方法
self.__state = SwanLabRunState.RUNNING

Expand All @@ -132,7 +130,8 @@ def _(state: SwanLabRunState):
# 系统信息采集
self.__operator.on_runtime_info_update(
RuntimeInfo(
requirements=get_requirements(), metadata=get_system_info(get_package_version(), self.settings.log_dir)
requirements=get_requirements(),
metadata=get_system_info(get_package_version(), self.settings.log_dir),
)
)

Expand Down Expand Up @@ -335,31 +334,30 @@ def __str__(self) -> str:

def __register_exp(
self,
experiment_name: str,
experiment_name: str = None,
description: str = None,
suffix: str = None,
num: int = None,
) -> SwanLabExp:
"""
注册实验,将实验配置写入数据库中,完成实验配置的初始化
"""

def setter(exp_name: str, light_color: str, dark_color: str, desc: str):
"""
设置实验相关信息的函数
:param exp_name: 实验名称
:param light_color: 亮色
:param dark_color: 暗色
:param desc: 实验描述
:return:
"""
# 实验创建成功,设置实验相关信息
self.settings.exp_name = exp_name
self.settings.exp_colors = (light_color, dark_color)
self.settings.description = desc

self.__operator.before_init_experiment(self.__run_id, experiment_name, description, num, suffix, setter)

if experiment_name:
e = check_exp_name_format(experiment_name)
if experiment_name != e:
swanlog.warning("The experiment name has been truncated automatically.")
experiment_name = e
if description:
d = check_desc_format(description)
if description != d:
swanlog.warning("The description has been truncated automatically.")
description = d
experiment_name = N.generate_name(num) if experiment_name is None else experiment_name
description = "" if description is None else description
colors = N.generate_colors(num)
self.__operator.before_init_experiment(self.__run_id, experiment_name, description, num, colors)
self.settings.exp_name = experiment_name
self.settings.exp_colors = colors
self.settings.description = description
return SwanLabExp(self.settings, operator=self.__operator)

@staticmethod
Expand Down
106 changes: 106 additions & 0 deletions swanlab/data/run/namer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
r"""
@DATE: 2024/9/20 14:35
@File: namer.py
@IDE: pycharm
@Description:
命名器、取色器
"""
from typing import Tuple, Optional
import random

prefix_list = [
"swan-", # 天鹅
"rat-", # 鼠
"ox-", # 牛
"tiger-", # 虎
"rabbit-", # 兔
"dragon-", # 龙
"snake-", # 蛇
"horse-", # 马
"goat-", # 羊
"monkey-", # 猴
"rooster-", # 鸡
"dog-", # 狗
"pig-" # 猪
"cat-", # 猫
"elephant-", # 象
"penguin-", # 企鹅
"kangaroo-", # 袋鼠
"monkey-", # 猴
"panda-", # 熊猫
"tiger-", # 虎
"lion-", # 狮
"zebra-", # 斑马
]


def generate_name(index: Optional[int] = None) -> str:
"""
生成名称
:param index: 生成名称的索引,约定为历史实验数,可以为None
:return: 生成的名称
"""
if index is None:
prefix = random.choice(prefix_list)
return prefix
else:
prefix = prefix_list[index % len(prefix_list)]
return prefix + str(index + 1)


light_colors = [
"#528d59", # 绿色
"#587ad2", # 蓝色
"#c24d46", # 红色
"#9cbe5d", # 青绿色
"#6ebad3", # 天蓝色
"#dfb142", # 橙色
"#6d4ba4", # 紫色
"#8cc5b7", # 淡青绿色
"#892d58", # 紫红色
"#40877c", # 深青绿色
"#d0703c", # 深橙色
"#d47694", # 粉红色
"#e3b292", # 淡橙色
"#b15fbb", # 浅紫红色
"#905f4a", # 棕色
"#989fa3", # 灰色
]

# 黑夜模式的颜色暂时和light_colors保持一致
dark_colors = [
"#528d59", # 绿色
"#587ad2", # 蓝色
"#c24d46", # 红色
"#9cbe5d", # 青绿色
"#6ebad3", # 天蓝色
"#dfb142", # 橙色
"#6d4ba4", # 紫色
"#8cc5b7", # 淡青绿色
"#892d58", # 紫红色
"#40877c", # 深青绿色
"#d0703c", # 深橙色
"#d47694", # 粉红色
"#e3b292", # 淡橙色
"#b15fbb", # 浅紫红色
"#905f4a", # 棕色
"#989fa3", # 灰色
]


def generate_colors(index: Optional[int] = None) -> Tuple[str, str]:
"""
生成颜色
:param index: 生成颜色的索引,约定为历史实验数,可以为None
:return: 生成的颜色,(白天颜色,夜晚颜色)
"""

if index is None:
choice_color = random.choice(light_colors)
return choice_color, choice_color # 随机返回一个颜色
else:
choice_color_light = light_colors[index % len(light_colors)]
choice_color_dark = dark_colors[index % len(dark_colors)]
return choice_color_light, choice_color_dark # 返回对应索引的颜色,如果超出范围则取模
14 changes: 2 additions & 12 deletions swanlab/data/sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ def init(
description: str = None,
config: Union[dict, str] = None,
logdir: str = None,
suffix: Union[str, None, bool] = "default",
mode: Literal["disabled", "cloud", "local"] = None,
load: str = None,
public: bool = None,
Expand Down Expand Up @@ -119,14 +118,6 @@ def init(
anything other than data generated by Swanlab.
In this case, if you want to view the logs,
you must use something like `swanlab watch -l ./your_specified_folder` to specify the folder path.
suffix : str, optional
The suffix of the experiment name, the default is 'default'.
If this parameter is 'default', suffix will be '%b%d-%h-%m-%s'(example:'Feb03_14-45-37'),
which represents the current time.
example: experiment_name = 'example', suffix = 'default' -> 'example_Feb03_14-45-37';
If this parameter is None or False, no suffix will be added.
If this parameter is a string, the suffix will be the string you provided.
Attention: experiment_name + suffix must be unique, otherwise the experiment will not be created.
mode : str, optional
Allowed values are 'cloud', 'cloud-only', 'local', 'disabled'.
If the value is 'cloud', the data will be uploaded to the cloud and the local log will be saved.
Expand Down Expand Up @@ -155,15 +146,15 @@ def init(
description = _load_data(load_data, "description", description)
config = _load_data(load_data, "config", config)
logdir = _load_data(load_data, "logdir", logdir)
suffix = _load_data(load_data, "suffix", suffix)
mode = _load_data(load_data, "mode", mode)
project = _load_data(load_data, "project", project)
workspace = _load_data(load_data, "workspace", workspace)
public = _load_data(load_data, "private", public)
operator, c = _create_operator(mode, public)
project = _check_proj_name(project if project else os.path.basename(os.getcwd())) # 默认实验名称为当前目录名
exp_num = SwanLabRunOperator.parse_return(
operator.on_init(project, workspace, logdir=logdir), key=c.__str__() if c else None
operator.on_init(project, workspace, logdir=logdir),
key=c.__str__() if c else None,
)
# 初始化confi参数
config = _init_config(config)
Expand All @@ -175,7 +166,6 @@ def init(
description=description,
run_config=config,
log_level=kwargs.get("log_level", "info"),
suffix=suffix,
exp_num=exp_num,
operator=operator,
)
Expand Down
39 changes: 39 additions & 0 deletions test/unit/data/run/test_namer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
r"""
@DATE: 2024/9/20 14:42
@File: test_namer.py
@IDE: pycharm
@Description:
测试命名器、取色器
"""
from swanlab.data.run import namer


def test_name_no_index():
name = namer.generate_name()
assert isinstance(name, str)


def test_name_with_index():
name = namer.generate_name(4)
assert isinstance(name, str)
# 极大数
name = namer.generate_name(999999999)
assert isinstance(name, str)


def test_color_no_index():
colors = namer.generate_colors()
assert len(colors) == 2
assert isinstance(colors, tuple)


def test_color_with_index():
colors = namer.generate_colors(4)
assert len(colors) == 2
assert isinstance(colors, tuple)
# 极大数
colors = namer.generate_colors(999999999)
assert len(colors) == 2
assert isinstance(colors, tuple)
Loading