# pyruns

![logo](docs/assets/pyruns_logo2.png)

**[English](README-en.md) | 简体中文**

<p align="center">
  <img src="https://img.shields.io/pypi/v/pyruns.svg?style=for-the-badge&color=blue" alt="PyPI version">
  <img src="https://img.shields.io/pypi/pyversions/pyruns.svg?style=for-the-badge" alt="Python Versions">
  <img src="https://img.shields.io/badge/License-MIT-green.svg?style=for-the-badge" alt="License">
</p>

<p align="center">
  <b>Python 实验管理 Web UI 工具：参数可视化编辑、网格批量调度、实时日志流式查看与指标聚合导出</b><br>
  <sub>✅ 零侵入兼容 argparse · ✅ 命令行模式兼容任意框架 · ✅ 全流程本地运行</sub>
</p>

Pyruns 为 Python 脚本提供基于本地浏览器的图形界面。其主要功能包括：通过解析 `argparse` 生成可视化设置表单、支持特定语法实现参数网格的批量任务生成、管理并行调度队列，以及在浏览器端提供 ANSI 彩色日志的实时流式输出和跨任务的指标聚合导出。

**无需修改原生业务代码 · 无需注册账号 · 无需联网 · 所有流程均在本地执行**

```bash
pip install pyruns
pyr train.py          
```

> 💡 **仅依赖 3 个包**：`nicegui` + `pyyaml` + `psutil`，安装后即用，不会给你的环境引入"重量级"框架。

---

## 💡 解决的工程痛点

### 痛点 1：超参搜索要靠手写 Bash 循环

**没有 Pyruns 时**——你需要写这种令人头痛的嵌套脚本：
```bash
for lr in 0.001 0.01 0.1; do
  for bs in 32 64 128; do
    for opt in adam sgd; do
      python train.py --lr $lr --batch_size $bs --optimizer $opt \
        > logs/lr${lr}_bs${bs}_${opt}.log 2>&1 &
    done
  done
done
wait
```

**用 Pyruns**——两行声明就等价于上面的 18 个嵌套组合：
```yaml
lr: 0.001 | 0.01 | 0.1
batch_size: 32 | 64 | 128
optimizer: adam | sgd
```
点击 **GENERATE** → 自动创建 18 个隔离任务，每个任务有独立的配置快照和日志目录。

### 痛点 2：实验记录全靠人脑和 Excel

> *"上周跑的那组 lr=0.01 的实验，用的是哪个 batch_size 来着？结果保存在哪了？"*

Pyruns 自动为**每个任务**保存完整的参数快照（`config.yaml`）、运行时间线、PID 记录、以及你随时可以追加的实验笔记。在 Manager 界面，你可以即时搜索、筛选、复用历史任务——再也不用翻文件夹或 grep 日志了。

### 痛点 3：多任务并发时日志混成一团

并发跑 6 个实验，终端输出全部交错在一起？Pyruns 将每个任务的标准输出**完全隔离**到独立日志文件，并在 Monitor 页面提供实时的 ANSI 彩色终端——包括 `tqdm` 进度条都能正确渲染。SSH 断开也不怕，任务在后台持续运行。

---

## ✨ 核心特性

| 特性 | 说明 |
|------|------|
| 🔌 **多框架零侵入解析** | 从源码中提取 `argparse` 定义来渲染 Web UI 表单；亦支持直接基于 `yaml` 文件的配置；**命令行模式可直接运行任意框架的脚本（包括 Hydra、Fire 等）**。 |
| 🧮 **参数网格展开** | 支持使用 `\|` 进行笛卡尔积排列或 `(\|)` 进行配对组合，用于生成批量参数配置集。 |
| ⚡ **独立任务队列** | 内部实现任务队列缓冲，可选 Thread/Process pool 型执行器控制并发规模。 |
| 📋 **状态看板管理** | 对任务进行 Pending/Running/Failed/Completed 状态流转管理，支持对历史配置的搜索与复用。 |
| 🖥️ **流式彩色终端** | 增量返回终端输出并完整支持 ANSI 转义字符渲染（如 `tqdm`）。 |
| 📊 **指标监控聚合** | 提供 `pyruns.record()` 回调，帮助在多组参数实验后导出 CSV/JSON 等合并报告。 |
| 📁 **环境参数快照** | 在 `_pyruns/` 下建立与脚本结构绑定的运行时目录，快照当前环境与参数，防干扰且支持软删除安全机制。 |

> 🎯 **设计理念**：Pyruns 不是一个重量级的实验平台（如 MLflow、W&B），而是一个**开箱即用、轻量化的本地调参助手**。它的目标是让你在**不改变现有工作流**的前提下，用最低的学习成本获得参数管理和实验追踪的便利。

---

## 🚀 启动方式

### 模式 1：基于 Argparse 的脚本接入

无需修改源码。Pyruns 通过 AST 分析器提取源码中 `add_argument` 的参数。

```bash
pyr train.py
```
这会在脚本所在目录下生成 `_pyruns_/train/config_default.yaml`，随后启动本地的 Web 服务（默认监听端口 `8099`）。

### 模式 2：基于 YAML 加载逻辑的接入

若您的源码直接通过读取外部 YAML 配置驱动并在代码内调用 `pyruns.load()`：

```bash
# 首次运行：传入默认参数模板
pyr train.py my_config.yaml  
```
此后，Pyruns 会接管参数调度及其相关环境变量：

```bash
pyr train.py
```

### 模式 3：CLI 命令行交互模式

如果你在无头服务器 (Headless Server) 上运行，或者更偏爱纯命令行操作，Pyruns 提供了一个**与 Web UI 级别完全等同**的 CLI 交互环境。

```bash
pyr cli train.py
```

这会自动构建和 Web UI 完全一样的工作区目录结构和配置，然后进入交互式的 REPL 终端：
```text
  Pyruns CLI  (type 'help' for commands, 'exit' to quit)
pyruns> ls         # 查看当前任务
pyruns> run 1      # 运行指定任务，并自动 stream 输出日志！
pyruns> status -i  # 实时显示系统和 GPU 监控
```
任何通过 CLI 触发的操作和 Web UI 是 100% 数据互通和兼容的。

### 模式 4：命令行模式 / Args 模式（兼容任意框架）

Pyruns 的 **Args 模式** 可以用来管理任意框架的实验。你只需要在生成任务时使用 Args 视图，并在 `run_script` 中填写启动命令：

```yaml
# 示例：管理 Hydra 实验
run_script: "python -m my_project.train"
args: |
  model=vit
  dataset=imagenet
  training.lr=0.001
  training.epochs=300
```

Pyruns 会将 `args` 字段的内容作为命令行参数直接追加到 `run_script` 后面执行。这意味着**无论你的脚本使用 Hydra、Fire、Click 还是任何其他框架**，只要它能通过命令行参数启动，你就可以利用 Pyruns 的批量生成和任务管理能力来管理它。

---

## 📝 实际上手示例

项目的 `examples/` 目录下提供了可直接运行的完整示例脚本，分别对应两种接入模式。

### 示例 1：Argparse 原生支持（零改动接入）

> 对应目录：`examples/1_argparse_script/`

下面是一个标准的 `argparse` 训练脚本，可以直接交给 Pyruns 接管——**无需对原始代码做任何修改**：

```python
# examples/1_argparse_script/main.py
import pyruns
import argparse
import time

def main():
    parser = argparse.ArgumentParser(description="A simple ML training script.")
    parser.add_argument("--lr", type=float, default=0.001, help="Learning rate")
    parser.add_argument("--epochs", type=int, default=10, help="Number of training epochs")
    parser.add_argument("-b", "--batch_size", type=int, default=32, help="Batch size")
    parser.add_argument("--optimizer", type=str, default="adam", choices=["adam", "sgd"])
    args = parser.parse_args()

    print(f"Hyperparameters: LR={args.lr}, Batch Size={args.batch_size}")
    for epoch in range(1, args.epochs + 1):
        time.sleep(0.5)
        loss = 1.0 / (epoch * args.lr * 100)
        print(f"Epoch {epoch}/{args.epochs} - Loss: {loss:.4f}")

    # 可选：记录最终指标，脱离 Pyruns 环境时该调用被静默忽略
    pyruns.record(last_loss=loss)

if __name__ == "__main__":
    main()
```

**使用方式**：

```bash
# Pyruns 自动解析 argparse 参数并启动 Web UI
pyr main.py
```

Pyruns 通过 AST 静态分析自动提取 `add_argument()` 的所有定义（参数名、类型、默认值、help 文本），生成可编辑的 Web UI 表单。用户在界面修改参数后，Pyruns 会以命令行参数的形式将其传递给脚本——脚本本身的 `parse_args()` 逻辑完全不受影响。

### 示例 2：使用 `pyruns.load()` 加载 YAML 配置

> 对应目录：`examples/2_pyruns_config/`

当训练脚本不使用命令行参数、而是直接读取 YAML 配置文件时，可以通过 `pyruns.load()` 完成接入。`load()` 返回的 `ConfigNode` 对象支持点号属性访问，嵌套结构会被自动递归封装：

```python
# examples/2_pyruns_config/main1.py
import pyruns
import time

def main():
    config = pyruns.load()  # 自动绑定当前任务的 config.yaml

    lr = config.lr
    epochs = config.epochs
    optimizer = config.optimizer

    print(f"Hyperparameters: LR={lr}, Optimizer={optimizer}")
    for epoch in range(1, epochs + 1):
        time.sleep(0.5)
        loss = 1.0 / (epoch * lr * 100)
        print(f"Epoch {epoch}/{epochs} - Loss: {loss:.4f}")

if __name__ == "__main__":
    main()
```

配套的默认配置文件 `config1.yaml`：

```yaml
lr: 5e-3
epochs: 20
optimizer: sgd
batch_size: 64
dropout: 0.2
model: resnet50
```

**使用方式**：

```bash
# 首次运行：传入 YAML 模板，Pyruns 将其复制为 config_default.yaml
pyr main1.py config1.yaml

# 后续运行：无需再指定 YAML，Pyruns 自动使用已保存的模板
pyr main1.py
```

此外，`pyruns.load()` 也支持多层嵌套的 YAML 结构。以 `config2.yaml` 为例，项目、模型、训练三级分层的配置可以通过链式点号一路访问到底：

```yaml
# config2.yaml — 三级嵌套结构
project:
  name: "DeepSense_Alpha"
  version: 1.2
  output_dir: "./results"
model:
  type: "Transformer"
  layers: 12
  dropout: 0.1
training:
  hyperparams:
    lr: 0.0005
    epochs: 8
    optimizer: "AdamW"
  resources:
    device: "cuda"
    precision: "fp16"
    gpu_config:
      memory_frac: 0.8
```

在脚本中通过点号链式访问即可读取任意层级的值：

```python
config = pyruns.load()
config.project.name              # "DeepSense_Alpha"
config.training.hyperparams.lr   # 0.0005
config.training.resources.device # "cuda"
```

---

## 🎯 界面模块

### 🔧 Generator — 简洁清晰的参数编辑器
在左侧提供清晰可见的结构化表单控制超参数修改，并支持声明化批量语法；右侧可实时预览将会并行生成的批量实验任务。通过（Pin）功能可以方便地标记核心参数。
![Generator UI](docs/assets/ui_generator_refined.png)

> UI/Performance update: the Generator page now uses a flatter visual system with tighter spacing, smaller corner radii, true lazy tab rendering, and more selective Manager/Monitor refresh behavior.

### 📦 Manager — 便捷的历史任务记录与管理
核心的任务管理面板。能够极其方便地监控、搜索并管理所有生成的任务队列。支持勾选进行并发执行限制；点击进入任务卡片，可查阅其精准的参数快照 (`config.yaml`) 历史记录。
![Manager UI](docs/assets/tab_manager.png)

<details>
<summary><b>🔥 点击查看卡片内部详情弹窗特性</b></summary>

| 特性 | 视图预览 |
| :---: | :---: |
| **生命周期总览**<br>重跑历史与 PIDs 记录 | ![Task Details Info](docs/assets/taskinfo.png) |
| **绝对隔离快照**<br>独享的 `config.yaml` 映射 | ![Task Details Config](docs/assets/config.png) |
| **实验笔记记录**<br>支持随时修改的实验副文本 | ![Task Details Notes](docs/assets/notes.png) |
| **环境变量溯源**<br>完整还原任务启动时的全部系统变量 | ![Task Details Env](docs/assets/env.png) |

</details>

### 📈 Monitor — 实时查看与一键导出报告
将处于活跃状态的任务的标准输流出实时定向到浏览器终端。同时，监视到的任务运行指标（如 Loss、Accuracy 等），在此支持跨任务勾选，一键导出聚合比对报表。
![Monitor UI](docs/assets/tab_monitor.png)

---

## 🧪 批量生成语法

在 Generator 的代码域内支持特定的管道解析语法，用于描述性构建执行计划：

**笛卡尔积组合 `|`** — 全排列组装（如 3 × 2 = 6 个独立任务）：
```yaml
learning_rate: 0.001 | 0.01 | 0.1
batch_size: 32 | 64
```

**一一配对映射 `(|)`** — 等长对应（共 3 个独立任务）：
```yaml
seed: (1 | 2 | 3)
experiment_name: (exp_a | exp_b | exp_c)
```
支持数字序列区间（如：`lr: 1:10:2`）。详细结构规则见 [批量构建语法说明](docs/batch-syntax.md)。

---

## 📂 运行区结构原理

Pyruns 采用隔离持久化策略。针对单一入口脚本所生成的执行缓存符合以下文件树形规约：

```text
your_project/
├── train.py
└── _pyruns_/
    ├── _pyruns_settings.yaml         # 全局端口、并发数相关配制 
    └── train/                        # 对应该脚本的独立命名空间
        ├── script_info.json          # 记录脚本路径与环境依赖
        ├── config_default.yaml       # UI 表单所需的参数初始骨架 
        └── tasks/
            ├── fast_tuning_[1-of-6]/
            │   ├── task_info.json    # 元数据状态机（存放执行PID、启停时间、监控数据等）
            │   ├── config.yaml       # 该任务启动时的确切参数配置切片
            │   └── run_logs/
            │       ├── run1.log      # 主流终端输出
            │       └── error.log     # 非零状态码退出时的标准错误分离堆栈
            └── .trash/               # Manager界面执行的非实质性软删除回收站
```

底层调用逻辑在执行队列发出 Run 信号时生效，执行器将 `config.yaml` 对应路径经由 `__PYRUNS_CONFIG__` 环境变量置入被唤醒的子进程。

---

## 📖 补充文档

| 文档部分 | 说明 |
|------|------|
| [📗 安装部署与初次接入](docs/getting-started.md) | 环境说明与第一个示例运作 |
| [📘 批量语法细则](docs/batch-syntax.md) | 复杂网格规则与类型推断行为 |
| [💻 命令行 CLI 交互控制](docs/cli-guide.md) | 在无头服务器/纯终端使用 REPL 环境管理全部流程的操作详解 |
| [📕 界面高级操作与控制](docs/ui-guide.md) | Manager执行限制细节及报表数据的导出等 |
| [📙 配置流转与沙盒说明](docs/configuration.md) | Node树层级、优先读取顺位判定 |
| [📓 API 接口文档](docs/api-reference.md) | 深入代码端的 `read()` / `load()` 及 `record()` 功能介绍 |

---

## License

MIT
