From e6220883aa878a4573c6cd9d9b726480c82bf9e7 Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Thu, 10 Jul 2025 11:20:32 +0800 Subject: [PATCH 01/25] Prob_DeepONnet --- .../DeepONet_Grid_UQ_Probabilistic.ipynb | 717 ++++++++++++++++++ .../application/deeponet-grid/README.md | 214 ++++++ .../deeponet-grid/configs/config.yaml | 61 ++ .../application/deeponet-grid/deeonet-grid.md | 40 + .../application/deeponet-grid/images/arch.png | Bin 0 -> 298230 bytes .../deeponet-grid/images/loss_curves.png | Bin 0 -> 207410 bytes .../application/deeponet-grid/images/uml.png | Bin 0 -> 199642 bytes .../application/deeponet-grid/issue_cn_api.md | 178 +++++ .../deeponet-grid/issue_cn_application.md | 161 ++++ .../deeponet-grid/requirements.txt | 22 + .../application/deeponet-grid/src/__init__.py | 19 + .../application/deeponet-grid/src/data.py | 485 ++++++++++++ .../application/deeponet-grid/src/metrics.py | 286 +++++++ .../application/deeponet-grid/src/model.py | 225 ++++++ .../application/deeponet-grid/src/trainer.py | 628 +++++++++++++++ .../application/deeponet-grid/src/utils.py | 374 +++++++++ .../application/deeponet-grid/test_system.py | 550 ++++++++++++++ MindEnergy/application/deeponet-grid/train.py | 325 ++++++++ 18 files changed, 4285 insertions(+) create mode 100644 MindEnergy/application/deeponet-grid/DeepONet_Grid_UQ_Probabilistic.ipynb create mode 100644 MindEnergy/application/deeponet-grid/README.md create mode 100644 MindEnergy/application/deeponet-grid/configs/config.yaml create mode 100644 MindEnergy/application/deeponet-grid/deeonet-grid.md create mode 100644 MindEnergy/application/deeponet-grid/images/arch.png create mode 100644 MindEnergy/application/deeponet-grid/images/loss_curves.png create mode 100644 MindEnergy/application/deeponet-grid/images/uml.png create mode 100644 MindEnergy/application/deeponet-grid/issue_cn_api.md create mode 100644 MindEnergy/application/deeponet-grid/issue_cn_application.md create mode 100644 MindEnergy/application/deeponet-grid/requirements.txt create mode 100644 MindEnergy/application/deeponet-grid/src/__init__.py create mode 100644 MindEnergy/application/deeponet-grid/src/data.py create mode 100644 MindEnergy/application/deeponet-grid/src/metrics.py create mode 100644 MindEnergy/application/deeponet-grid/src/model.py create mode 100644 MindEnergy/application/deeponet-grid/src/trainer.py create mode 100644 MindEnergy/application/deeponet-grid/src/utils.py create mode 100644 MindEnergy/application/deeponet-grid/test_system.py create mode 100644 MindEnergy/application/deeponet-grid/train.py diff --git a/MindEnergy/application/deeponet-grid/DeepONet_Grid_UQ_Probabilistic.ipynb b/MindEnergy/application/deeponet-grid/DeepONet_Grid_UQ_Probabilistic.ipynb new file mode 100644 index 000000000..8d4c7ee40 --- /dev/null +++ b/MindEnergy/application/deeponet-grid/DeepONet_Grid_UQ_Probabilistic.ipynb @@ -0,0 +1,717 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import sys\n", + "import numpy as np\n", + "import mindspore as ms\n", + "from mindspore import context\n", + "import yaml" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import sys\n", + "import numpy as np\n", + "import mindspore as ms\n", + "from mindspore import context\n", + "import yaml" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "sys.path.append(os.path.join(os.getcwd(), 'deeponet-grid'))\n", + "sys.path.append(os.path.join(os.getcwd(), 'deeponet-grid/src'))\n", + "\n", + "from src.model import Prob_DeepONet\n", + "# from mindenergy.models import Prob_DeepONet\n", + "from src.data import load_real_data, prepare_deeponet_data, normalize_data_mindspore, split_data, create_mindspore_datasets, trajectory_prediction, batch_trajectory_prediction\n", + "from src.trainer import create_trainer\n", + "from src.metrics import compute_metrics, update_metrics_history, test, test_one, MetricsCalculator" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "==================================================\n", + "DeepONet-Grid-UQ MindSpore Configuration\n", + "==================================================\n", + "Configuration:\n", + " Sensors (m): 33\n", + " Time points (q): 100\n", + " Basis functions: 100\n", + " Network width: 200\n", + " Network depth: 3\n", + " Learning rate: 5e-05\n", + " Batch size: 1024\n", + " Epochs: 1\n" + ] + } + ], + "source": [ + "# ===================================\n", + "# Step 1: Configuration Parameters\n", + "# ===================================\n", + "print(\"=\" * 50)\n", + "print(\"DeepONet-Grid-UQ MindSpore Configuration\")\n", + "print(\"=\" * 50)\n", + "\n", + "# General Parameters\n", + "verbose = True\n", + "seed = 1234\n", + "\n", + "# DeepONet parameters (based on your data)\n", + "m = 33 # Number of sensors (voltage data)\n", + "q = 100 # Number of time points\n", + "n_basis = 100 # Number of basis functions\n", + "branch_type = \"modified\" # Branch network type\n", + "trunk_type = \"modified\" # Trunk network type\n", + "width = 200 # Network width\n", + "depth = 3 # Network depth\n", + "activation = \"sin\" # Activation function\n", + "\n", + "# Training parameters\n", + "learning_rate = 5e-5 # Learning rate\n", + "batch_size = 1024 # Batch size\n", + "n_epochs = 1 # Number of epochs (for demo)\n", + "\n", + "# Data parameters\n", + "version = \"v1\"\n", + "state = \"voltage\"\n", + "cont = \"mix\"\n", + "\n", + "print(f\"Configuration:\")\n", + "print(f\" Sensors (m): {m}\")\n", + "print(f\" Time points (q): {q}\")\n", + "print(f\" Basis functions: {n_basis}\")\n", + "print(f\" Network width: {width}\")\n", + "print(f\" Network depth: {depth}\")\n", + "print(f\" Learning rate: {learning_rate}\")\n", + "print(f\" Batch size: {batch_size}\")\n", + "print(f\" Epochs: {n_epochs}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[WARNING] ME(60943:8377666560,MainProcess):2025-07-10-11:08:40.849.842 [mindspore/context.py:1335] For 'context.set_context', the parameter 'device_target' will be deprecated and removed in a future version. Please use the api mindspore.set_device() instead.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "==================================================\n", + "Setting up MindSpore device and context\n", + "==================================================\n" + ] + } + ], + "source": [ + "# ===================================\n", + "# Step 2: Set up device and context\n", + "# ===================================\n", + "print(\"\\n\" + \"=\" * 50)\n", + "print(\"Setting up MindSpore device and context\")\n", + "print(\"=\" * 50)\n", + "\n", + "# Set random seed\n", + "np.random.seed(seed)\n", + "ms.set_seed(seed)\n", + "\n", + "# Set up device context\n", + "context.set_context(mode=context.PYNATIVE_MODE, device_target=\"CPU\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "==================================================\n", + "Loading training and test data\n", + "==================================================\n", + "Loading training data from: /Users/congwang/Documents/codes/M_deeponetgrid/deeponet_ms/data/grid_real/data/trustworthydatasetv1/train-data-voltage-m-33-Q-100-mix.npz\n", + "Training data shapes: u=(11574, 33), y=(11574, 100), s=(11574, 100)\n", + "Loading test data from: /Users/congwang/Documents/codes/M_deeponetgrid/deeponet_ms/data/grid_real/data/trustworthydatasetv1/test-data-voltage-m-33-mix.npz\n", + "Test data shapes: u=(1286, 33), y=(1286, 100), s=(1286, 100)\n" + ] + } + ], + "source": [ + "# ===================================\n", + "# Step 3: Load training and test data\n", + "# ===================================\n", + "print(\"\\n\" + \"=\" * 50)\n", + "print(\"Loading training and test data\")\n", + "print(\"=\" * 50)\n", + "\n", + "# Define data paths\n", + "train_path = f\"/Users/congwang/Documents/codes/M_deeponetgrid/deeponet_ms/data/grid_real/data/trustworthydataset{version}/train-data-{state}-m-{m}-Q-{q}-{cont}.npz\"\n", + "test_path = f\"/Users/congwang/Documents/codes/M_deeponetgrid/deeponet_ms/data/grid_real/data/trustworthydataset{version}/test-data-{state}-m-{m}-{cont}.npz\"\n", + "\n", + "# Check if files exist\n", + "if not os.path.exists(train_path):\n", + " print(f\"Training data not found: {train_path}\")\n", + " print(\"Please check the data path or use synthetic data\")\n", + "\n", + "if not os.path.exists(test_path):\n", + " print(f\"Test data not found: {test_path}\")\n", + " print(\"Please check the data path\")\n", + "\n", + "# Load training data\n", + "print(f\"Loading training data from: {train_path}\")\n", + "u_train, y_train, s_train = load_real_data(train_path)\n", + "print(f\"Training data shapes: u={u_train.shape}, y={y_train.shape}, s={s_train.shape}\")\n", + "\n", + "# Load test data\n", + "print(f\"Loading test data from: {test_path}\")\n", + "u_test, y_test, s_test = load_real_data(test_path)\n", + "print(f\"Test data shapes: u={u_test.shape}, y={y_test.shape}, s={s_test.shape}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "==================================================\n", + "Preparing data for DeepONet training\n", + "==================================================\n", + "Preparing training data...\n", + "Expanded training data: u=(1157400, 33), y=(1157400, 1), s=(1157400, 1)\n", + "Preparing test data...\n", + "Expanded test data: u=(128600, 33), y=(128600, 1), s=(128600, 1)\n", + "Splitting data...\n", + "Creating MindSpore datasets...\n", + "Created datasets: ['train', 'val', 'test']\n" + ] + } + ], + "source": [ + "# ===================================\n", + "# Step 4: Prepare data for DeepONet\n", + "# ===================================\n", + "print(\"\\n\" + \"=\" * 50)\n", + "print(\"Preparing data for DeepONet training\")\n", + "print(\"=\" * 50)\n", + "\n", + "# Prepare training data (expand multi-time-point to single query points)\n", + "print(\"Preparing training data...\")\n", + "u_train_expanded, y_train_expanded, s_train_expanded, train_metadata = prepare_deeponet_data(u_train, y_train, s_train)\n", + "print(f\"Expanded training data: u={u_train_expanded.shape}, y={y_train_expanded.shape}, s={s_train_expanded.shape}\")\n", + "\n", + "# Prepare test data\n", + "print(\"Preparing test data...\")\n", + "u_test_expanded, y_test_expanded, s_test_expanded, test_metadata = prepare_deeponet_data(u_test, y_test, s_test)\n", + "print(f\"Expanded test data: u={u_test_expanded.shape}, y={y_test_expanded.shape}, s={s_test_expanded.shape}\")\n", + "\n", + "# Normalize data (optional - you can comment this out if not needed)\n", + "# print(\"Normalizing data...\")\n", + "# u_train_norm, y_train_norm, s_train_norm, scalers = normalize_data_mindspore(\n", + "# u_train_expanded, y_train_expanded, s_train_expanded, method='standard'\n", + "# )\n", + "\n", + "# Split data, only use 10% of data for training for demo\n", + "print(\"Splitting data...\")\n", + "data_splits = split_data(\n", + " u_train_expanded, y_train_expanded, s_train_expanded,\n", + " train_split=0.1, val_split=0.5, test_split=0.4\n", + ")\n", + "\n", + "# Create MindSpore datasets\n", + "print(\"Creating MindSpore datasets...\")\n", + "datasets = create_mindspore_datasets(data_splits, batch_size=batch_size)\n", + "print(f\"Created datasets: {list(datasets.keys())}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "==================================================\n", + "Building Probabilistic DeepONet model\n", + "==================================================\n", + "Model created successfully\n", + "Branch network: [33, 200, 200, 200, 100]\n", + "Trunk network: [1, 200, 200, 200, 100]\n" + ] + } + ], + "source": [ + "# ===================================\n", + "# Step 5: Build Probabilistic DeepONet\n", + "# ===================================\n", + "print(\"\\n\" + \"=\" * 50)\n", + "print(\"Building Probabilistic DeepONet model\")\n", + "print(\"=\" * 50)\n", + "\n", + "# Model configuration\n", + "dim = 1 # Trunk input dimension (always 1 for DeepONet)\n", + "\n", + "# Branch configuration\n", + "branch_config = {\n", + " \"type\": branch_type,\n", + " \"layer_size\": [m] + [width] * depth + [n_basis], # [input] + [width] * depth + [output]\n", + " \"activation\": activation\n", + "}\n", + "\n", + "# Trunk configuration\n", + "trunk_config = {\n", + " \"type\": trunk_type,\n", + " \"layer_size\": [dim] + [width] * depth + [n_basis], # [input] + [width] * depth + [output]\n", + " \"activation\": activation\n", + "}\n", + "\n", + "# Create model\n", + "model = Prob_DeepONet(branch_config, trunk_config, use_bias=True)\n", + "\n", + "print(f\"Model created successfully\")\n", + "print(f\"Branch network: {branch_config['layer_size']}\")\n", + "print(f\"Trunk network: {trunk_config['layer_size']}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[WARNING] ME(60943:8377666560,MainProcess):2025-07-10-11:08:44.528.767 [mindspore/context.py:1335] For 'context.set_context', the parameter 'device_target' will be deprecated and removed in a future version. Please use the api mindspore.set_device() instead.\n", + "INFO:src.trainer:Device set to: CPU, Mode: PYNATIVE_MODE\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "==================================================\n", + "Creating trainer and training configuration\n", + "==================================================\n", + "Trainer created successfully\n" + ] + } + ], + "source": [ + "# ===================================\n", + "# Step 6: Create trainer and training configuration\n", + "# ===================================\n", + "print(\"\\n\" + \"=\" * 50)\n", + "print(\"Creating trainer and training configuration\")\n", + "print(\"=\" * 50)\n", + "\n", + "# Training configuration\n", + "config = {\n", + " 'model': {\n", + " 'm': m,\n", + " 'dim': dim,\n", + " 'width': width,\n", + " 'depth': depth,\n", + " 'n_basis': n_basis,\n", + " 'branch_type': branch_type,\n", + " 'trunk_type': trunk_type,\n", + " 'activation': activation,\n", + " 'use_bias': True\n", + " },\n", + " 'training': {\n", + " 'learning_rate': learning_rate,\n", + " 'batch_size': batch_size,\n", + " 'epochs': n_epochs,\n", + " 'print_every': 10,\n", + " 'eval_every': 100,\n", + " 'patience': 100,\n", + " 'factor': 0.8,\n", + " 'verbose': verbose,\n", + " 'loss_type': 'nll',\n", + " 'optimizer': 'adam',\n", + " 'weight_decay': 0.0\n", + " },\n", + " 'device': {\n", + " 'target': 'CPU',\n", + " 'mode': 'PYNATIVE_MODE'\n", + " },\n", + " 'output': {\n", + " 'save_dir': 'outputs',\n", + " 'save_best': True\n", + " }\n", + "}\n", + "\n", + "# Create trainer\n", + "trainer = create_trainer(model, config, save_dir='outputs')\n", + "print(f\"Trainer created successfully\")" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "==================================================\n", + "Initial test (before training)\n", + "==================================================\n", + "Testing on 10 samples...\n", + "Initial test - Mean prediction shape: (1024, 1)\n", + "Initial test - Log std prediction shape: (1024, 1)\n" + ] + } + ], + "source": [ + "# ===================================\n", + "# Step 7: Initial test (before training)\n", + "# ===================================\n", + "print(\"\\n\" + \"=\" * 50)\n", + "print(\"Initial test (before training)\")\n", + "print(\"=\" * 50)\n", + "\n", + "# Test on a small subset\n", + "n_test_samples = 10\n", + "print(f\"Testing on {n_test_samples} samples...\")\n", + "\n", + "# Get a small batch for testing\n", + "test_batch = next(datasets['test'].create_dict_iterator())\n", + "u_test_batch, y_test_batch, s_test_batch = test_batch['u'], test_batch['y'], test_batch['s']\n", + "\n", + "# Forward pass\n", + "mean_pred, log_std_pred = model(u_test_batch, y_test_batch)\n", + "print(f\"Initial test - Mean prediction shape: {mean_pred.shape}\")\n", + "print(f\"Initial test - Log std prediction shape: {log_std_pred.shape}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:root:Total model parameters: 343202\n", + "INFO:root:Trainable parameters: 343202\n", + "INFO:root:Training data size (number of samples): 116736\n", + "INFO:root:Training batch size : 1024\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "==================================================\n", + "Starting training\n", + "==================================================\n", + "Model parameter details:\n", + "bias_mu: shape=(1,), size=1\n", + "bias_std: shape=(1,), size=1\n", + "branch.net.0.weight: shape=(200, 33), size=6600\n", + "branch.net.0.bias: shape=(200,), size=200\n", + "branch.net.1.weight: shape=(200, 200), size=40000\n", + "branch.net.1.bias: shape=(200,), size=200\n", + "branch.U.0.weight: shape=(200, 33), size=6600\n", + "branch.U.0.bias: shape=(200,), size=200\n", + "branch.V.0.weight: shape=(200, 33), size=6600\n", + "branch.V.0.bias: shape=(200,), size=200\n", + "trunk.net.0.weight: shape=(200, 1), size=200\n", + "trunk.net.0.bias: shape=(200,), size=200\n", + "trunk.net.1.weight: shape=(200, 200), size=40000\n", + "trunk.net.1.bias: shape=(200,), size=200\n", + "trunk.U.0.weight: shape=(200, 1), size=200\n", + "trunk.U.0.bias: shape=(200,), size=200\n", + "trunk.V.0.weight: shape=(200, 1), size=200\n", + "trunk.V.0.bias: shape=(200,), size=200\n", + "branch_mu.1.weight: shape=(200, 200), size=40000\n", + "branch_mu.1.bias: shape=(200,), size=200\n", + "branch_mu.3.weight: shape=(100, 200), size=20000\n", + "branch_mu.3.bias: shape=(100,), size=100\n", + "branch_std.1.weight: shape=(200, 200), size=40000\n", + "branch_std.1.bias: shape=(200,), size=200\n", + "branch_std.3.weight: shape=(100, 200), size=20000\n", + "branch_std.3.bias: shape=(100,), size=100\n", + "trunk_mu.1.weight: shape=(200, 200), size=40000\n", + "trunk_mu.1.bias: shape=(200,), size=200\n", + "trunk_mu.3.weight: shape=(100, 200), size=20000\n", + "trunk_mu.3.bias: shape=(100,), size=100\n", + "trunk_std.1.weight: shape=(200, 200), size=40000\n", + "trunk_std.1.bias: shape=(200,), size=200\n", + "trunk_std.3.weight: shape=(100, 200), size=20000\n", + "trunk_std.3.bias: shape=(100,), size=100\n", + "\n", + "***** Probabilistic Training for 1 epochs *****\n", + "\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:root:Epoch 1, Step 10, Batch 10, Loss: 0.621717, Step time: 0.077s\n", + "INFO:root:Epoch 1, Step 20, Batch 20, Loss: 0.412504, Step time: 0.082s\n", + "INFO:root:Epoch 1, Step 30, Batch 30, Loss: 0.241822, Step time: 0.093s\n", + "INFO:root:Epoch 1, Step 40, Batch 40, Loss: 0.108371, Step time: 0.089s\n", + "INFO:root:Epoch 1, Step 50, Batch 50, Loss: -0.016050, Step time: 0.088s\n", + "INFO:root:Epoch 1, Step 60, Batch 60, Loss: -0.252723, Step time: 0.077s\n", + "INFO:root:Epoch 1, Step 70, Batch 70, Loss: -0.371130, Step time: 0.084s\n", + "INFO:root:Epoch 1, Step 80, Batch 80, Loss: -0.305221, Step time: 0.098s\n", + "INFO:root:Epoch 1, Step 90, Batch 90, Loss: -0.583641, Step time: 0.090s\n", + "INFO:root:Epoch 1, Step 100, Batch 100, Loss: -0.720639, Step time: 0.086s\n", + "INFO:root:[Eval] Epoch 1, Step 100, Val-Loss: -0.721453 \n", + "INFO:src.trainer:Model saved to: outputs/best_model.ckpt\n", + "INFO:root:Epoch 1, Step 110, Batch 110, Loss: -0.874301, Step time: 0.048s\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1/1:\n", + " Train-Loss: -0.121288 \n", + " Best-Loss: -0.721453 \n", + "Training completed!\n" + ] + } + ], + "source": [ + "# ===================================\n", + "# Step 8: Training\n", + "# ===================================\n", + "print(\"\\n\" + \"=\" * 50)\n", + "print(\"Starting training\")\n", + "print(\"=\" * 50)\n", + "\n", + "# Train the model\n", + "history = trainer.train(\n", + " train_dataset=datasets['train'],\n", + " val_dataset=datasets['val']\n", + ")\n", + "\n", + "print(\"Training completed!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "==================================================\n", + "Evaluation and testing\n", + "==================================================\n", + "Evaluating on test set...\n", + "Test Results:\n", + " MSE: 0.007808\n", + " MAE: 0.055851\n", + " R2: -0.711629\n", + " L1_RELATIVE_ERROR: 0.055933\n", + " L2_RELATIVE_ERROR: 0.088294\n", + " CALIBRATION_ERROR: 0.000000\n", + " FRACTION_IN_CI: 0.970874\n", + " TRAJECTORY_L1_ERROR: 0.055933\n", + " TRAJECTORY_L2_ERROR: 0.088294\n" + ] + } + ], + "source": [ + "# ===================================\n", + "# Step 9: Evaluation and testing\n", + "# ===================================\n", + "print(\"\\n\" + \"=\" * 50)\n", + "print(\"Evaluation and testing\")\n", + "print(\"=\" * 50)\n", + "\n", + "# Evaluate on test set\n", + "print(\"Evaluating on test set...\")\n", + "test_metrics = trainer.evaluate(datasets['test'])\n", + "\n", + "print(\"Test Results:\")\n", + "for metric, value in test_metrics.items():\n", + " print(f\" {metric.upper()}: {value:.6f}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "==================================================\n", + "Trajectory prediction and uncertainty quantification\n", + "==================================================\n", + "Testing trajectory prediction...\n", + "Predicting trajectories for 5 samples at 100 time points...\n", + "Trajectory predictions shape: (5, 100, 1)\n" + ] + } + ], + "source": [ + "# ===================================\n", + "# Step 10: Trajectory prediction and UQ\n", + "# ===================================\n", + "print(\"\\n\" + \"=\" * 50)\n", + "print(\"Trajectory prediction and uncertainty quantification\")\n", + "print(\"=\" * 50)\n", + "\n", + "# Test trajectory prediction\n", + "print(\"Testing trajectory prediction...\")\n", + "\n", + "# Select a few test samples for trajectory prediction\n", + "n_traj_test = 5\n", + "u_traj_test = u_test[:n_traj_test] # First 5 samples\n", + "time_points = np.linspace(0, 2.0, num=100) # 100 time points from 0 to 2\n", + "\n", + "print(f\"Predicting trajectories for {n_traj_test} samples at {len(time_points)} time points...\")\n", + "\n", + "# Predict trajectories\n", + "predictions = batch_trajectory_prediction(u_traj_test, time_points, model)\n", + "print(f\"Trajectory predictions shape: {predictions.shape}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:src.trainer:Model saved to: outputs/final_model.ckpt\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "==================================================\n", + "Saving results\n", + "==================================================\n", + "Training history saved to: outputs/training_history.json\n", + "Test results saved to: outputs/test_results.json\n", + "Final model saved to: outputs/final_model.ckpt\n", + "\n", + "==================================================\n", + "DeepONet-Grid-UQ MindSpore example completed successfully!\n", + "==================================================\n" + ] + } + ], + "source": [ + "# ===================================\n", + "# Step 11: Save results\n", + "# ===================================\n", + "print(\"\\n\" + \"=\" * 50)\n", + "print(\"Saving results\")\n", + "print(\"=\" * 50)\n", + "\n", + "# Save training history\n", + "import json\n", + "history_path = os.path.join('outputs', 'training_history.json')\n", + "with open(history_path, 'w') as f:\n", + " json.dump(history, f, indent=2)\n", + "print(f\"Training history saved to: {history_path}\")\n", + "\n", + "# Save test results\n", + "test_results_path = os.path.join('outputs', 'test_results.json')\n", + "with open(test_results_path, 'w') as f:\n", + " json.dump(test_metrics, f, indent=2)\n", + "print(f\"Test results saved to: {test_results_path}\")\n", + "\n", + "# Save model\n", + "trainer.save_model('final_model.ckpt')\n", + "print(\"Final model saved to: outputs/final_model.ckpt\")\n", + "\n", + "print(\"\\n\" + \"=\" * 50)\n", + "print(\"DeepONet-Grid-UQ MindSpore example completed successfully!\")\n", + "print(\"=\" * 50)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/MindEnergy/application/deeponet-grid/README.md b/MindEnergy/application/deeponet-grid/README.md new file mode 100644 index 000000000..64d6685ad --- /dev/null +++ b/MindEnergy/application/deeponet-grid/README.md @@ -0,0 +1,214 @@ +# DeepONet-Grid-UQ with MindSpore + +基于MindSpore框架的DeepONet-Grid不确定性量化实现。 + +## 案例特性概述 + +### 需求来源及价值概述 + +本工作构建了一个高效的网络DeepONet-Grid,用于对故障后的电力系统进行动态安全分析,该网络 + + (i) 接收故障前和故障期间收集的轨迹作为输入,并且 + (ii) 输出预测的故障后轨迹。 + +此外,本网络还通过不确定性量化(Uncertainty Quantification)为其方法赋予了在效率与可靠/可信预测之间取得平衡的能力。 + +原始论文:[DeepONet-grid-UQ: A trustworthy deep operator framework for predicting the power grid's post-fault trajectories](!https://www.sciencedirect.com/science/article/abs/pii/S0925231223002503) + +原始代码仓:[Github Link](!https://github.com/cmoyacal/DeepONet-Grid-UQ) + + + +### 研究背景与动机 + +电力系统作为关键基础设施,其稳定性和可靠性对现代社会至关重要。然而,电网经常面临罕见但严重的故障和扰动,这些事件可能导致系统不稳定,甚至引发大规模停电。 + +传统的动态安全分析需要求解复杂的非线性微分代数方程组,计算成本极高,难以实现实时分析。随着电网的转型,电力公司迫切需要能够进行近实时的动态安全评估。 + +现有的机器学习方法主要关注二分类问题(稳定/不稳定),缺乏对故障后轨迹的定量预测能力。系统运营商和规划者需要了解故障后各种状态变量的轨迹,以评估电压或频率是否会违反预定义限制并触发负荷切除等保护措施。 + +## 项目结构 + +``` +deeponet-grid/ +├── configs/ +│ └── config.yaml # 配置文件 +├── src/ +│ ├── model.py # DeepONet模型定义 +│ ├── data.py # 数据加载和预处理 +│ ├── utils.py # 功能函数,比如打印loss曲线 +│ ├── trainer.py # 训练器实现 +│ └── metrics.py # 评估指标 +├── train.py # 主训练脚本 +├── requirements.txt # 依赖包列表 +├── README.md # 本文件 +├── Prob_DeepONet_Grid.ipynb # 使用Prob_DeepONet的样例notebook +└── outputs/ # 输出目录(自动创建) +``` + +## 安装 + +1. 安装MindSpore框架: + ```bash + pip install mindspore + ``` + +## 配置 + +编辑 `configs/config.yaml` 文件来配置模型参数: + +### 模型配置 +- `branch`: 分支网络配置(处理输入函数) +- `trunk`: 主干网络配置(处理评估点) +- `use_bias`: 是否使用偏置项 + +### 训练配置 +- `learning_rate`: 学习率 +- `batch_size`: 批次大小 +- `epochs`: 训练轮数 +- `optimizer`: 优化器类型(adam, sgd, adamw) +- `loss_type`: 损失函数类型(nll, mse) + +### 数据配置 +- `use_synthetic`: 是否使用合成数据 +- `data_path`: 数据文件路径 +- `normalize`: 是否标准化数据 + +## 使用方法 + +### 1. 使用真实数据训练 + +```bash +python train.py --data_path /path/to/your/data.npz --epochs 500 +``` + +### 2. 自定义配置 + +```bash +python train.py \ + --config configs/config.yaml \ + --data_path data/real_data.npz \ + --output_dir outputs/my_experiment \ + --epochs 1000 \ + --batch_size 64 \ + --learning_rate 1e-4 +``` + +### 4. 从检查点恢复训练 + +```bash +python train.py --resume outputs/best_model.ckpt +``` + +### 5. 仅运行评估 + +```bash +python train.py --test_only --resume outputs/best_model.ckpt +``` + +## 数据格式 + +数据文件应为 `.npz` 格式,包含以下字段: + +- `u`: 输入函数值,形状为 `(n_samples, n_sensors)` +- `y`: 评估点,形状为 `(n_samples, n_points, 1)` , 在数据处理阶段会自动转为n_samples * n_points, 1) +- `s`: 真实解值,形状为 `(n_samples, n_points, 1)`, 在数据处理阶段会自动转为n_samples * n_points, 1) + +示例: +```python +import numpy as np + +# 生成示例数据 +n_samples = 1000 +n_sensors = 200 +n_points = 1 + +u = np.random.randn(n_samples, n_sensors) +y = np.random.rand(n_samples, n_points, 1) * 2.0 +s = np.sin(u.mean(axis=1, keepdims=True) * y.squeeze(-1)) +s = s.reshape(n_samples, n_points, 1) + +# 保存数据 +np.savez('data.npz', u=u, y=y, s=s) +``` + +## 模型架构 + +DeepONet由两个主要组件组成: + +1. **分支网络 (Branch Network)**: 处理输入函数 `u(x)` +2. **主干网络 (Trunk Network)**: 处理评估点 `y` + +输出通过点积计算: +``` +G(u)(y) = Σᵢ bᵢ(u) tᵢ(y) +``` + +其中 `bᵢ(u)` 是分支网络的输出,`tᵢ(y)` 是主干网络的输出。 + +## 不确定性量化 + +模型提供不确定性量化功能: + +- **均值预测**: 模型预测的期望值 +- **标准差预测**: 预测的不确定性度量 + +损失函数支持: +- **负对数似然 (NLL)**: 用于不确定性量化 +- **均方误差 (MSE)**: 标准回归损失 + +## 输出文件 + +训练完成后,在输出目录中会生成: + +- `best_model.ckpt`: 最佳模型检查点 +- `final_model.ckpt`: 最终模型检查点 +- `training_history.json`: 训练历史记录 +- `test_results.json`: 测试集评估结果 +- `training.log`: 训练日志 + +## 评估指标 + +- **MSE**: 均方误差 +- **MAE**: 平均绝对误差 +- **R²**: 决定系数 +- **Calibration Error**: 校准误差 + + +### 调试模式 + +设置环境变量启用详细日志: +```bash +export MINDSPORE_LOG_LEVEL=DEBUG +python train.py +``` + +## 扩展功能 + +### 自定义损失函数 + +在 `src/trainer.py` 中添加新的损失函数类: + +```python +class CustomLoss(nn.Cell): + def construct(self, mean_pred, log_std_pred, target): + # 自定义损失计算 + return loss +``` + +### 自定义数据加载器 + +在 `src/data.py` 中添加新的数据加载函数: + +```python +def load_custom_data(data_path): + # 自定义数据加载逻辑 + return u, y, s +``` + +# + +## 训练损失函数图 + + +![loss](images/loss_curves.png) \ No newline at end of file diff --git a/MindEnergy/application/deeponet-grid/configs/config.yaml b/MindEnergy/application/deeponet-grid/configs/config.yaml new file mode 100644 index 000000000..bea0b4313 --- /dev/null +++ b/MindEnergy/application/deeponet-grid/configs/config.yaml @@ -0,0 +1,61 @@ +# DeepONet-Grid-UQ Configuration +# MindSpore-based DeepONet-Grid-UQ configuration file + +# Model Configuration +model: + # Network parameters + m: 33 # Number of sensors (based on data u.shape[1]) + dim: 1 # Trunk network input dimension (always 1 for DeepONet) + width: 200 # Network width + depth: 3 # Network depth (number of hidden layers) + n_basis: 100 # Number of basis functions + + # Network types + branch_type: "modified" # Branch network type: "modified" or "MLP" + trunk_type: "modified" # Trunk network type: "modified" or "MLP" + activation: "sin" # Activation function + + # Other parameters + use_bias: true # Whether to use bias + +# Training Configuration +training: + learning_rate: 0.00005 # Learning rate (5e-5) + batch_size: 1024 # Batch size + epochs: 100 # Number of training epochs + print_every: 100 # Print progress every N steps + eval_every: 1000 # Evaluate every N steps + + # Learning rate scheduler + patience: 100 # Early stopping patience + factor: 0.8 # Learning rate reduction factor + + # Other settings + verbose: true # Whether to output detailed information + loss_type: "nll" # Loss function type: "nll" (negative log likelihood) + optimizer: "adam" # Optimizer + weight_decay: 0.0 # Weight decay + +# Data Configuration +data: + data_path: null # Real data path (if null, use synthetic data) + use_synthetic: true # Whether to use synthetic data + train_split: 0.1 # Training set ratio + val_split: 0.5 # Validation set ratio + test_split: 0.4 # Test set ratio + + # Synthetic data parameters (only used when use_synthetic=true) + n_samples: 1000 # Number of samples + n_sensors: 33 # Number of sensors + n_points: 100 # Number of time points + seed: 42 # Random seed + +# Device Configuration +device: + target: "CPU" # Device target: "CPU", "GPU", "Ascend" + mode: "PYNATIVE_MODE" # Running mode: "PYNATIVE_MODE", "GRAPH_MODE" + +# Output Configuration +output: + save_dir: "outputs" # Output directory + save_best: true # Whether to save best model diff --git a/MindEnergy/application/deeponet-grid/deeonet-grid.md b/MindEnergy/application/deeponet-grid/deeonet-grid.md new file mode 100644 index 000000000..1fb8bfbc3 --- /dev/null +++ b/MindEnergy/application/deeponet-grid/deeonet-grid.md @@ -0,0 +1,40 @@ +# DeepOnet-Grid-UQ + +## Description + +This work build an efficient DeepONet that + + (i) takes as inputs the trajectories collected before and during the fault and + (ii) outputs the predicted post-fault trajectories. + +In addition, they also endow their method with the much-needed ability to balance efficiency with reliable/trustworthy predictions via Ucertainty Quantification. + +Original Paper : [DeepONet-grid-UQ: A trustworthy deep operator framework for predicting the power grid’s post-fault trajectories](!https://www.sciencedirect.com/science/article/abs/pii/S0925231223002503) +Original Code on torch: [Github](!https://github.com/cmoyacal/DeepONet-Grid-UQ) + + + +## Implementation + +### Enviroment + +MindSpore 2.6.0 + + +### Network +From the original work, they have built two networks: + + (1) Bayesian DeepONet (B-DeepONet) + uses stochastic gradient Hamiltonian Monte-Carlo to sample from the posterior distribution of the DeepONet trainable parameters + + (2) Probabilistic DeepONet (Prob-DeepONet) that uses a probabilistic training strategy to enable quantifying uncertainty at virtually no extra computational cost. + +Currently, we only support Porb-DeepONet in `src/model.py`. + +### Data Loader & Training Codes & Evaluation + +under development + + + + diff --git a/MindEnergy/application/deeponet-grid/images/arch.png b/MindEnergy/application/deeponet-grid/images/arch.png new file mode 100644 index 0000000000000000000000000000000000000000..36e27d415a78e028990c56ca27ef8c9a0b7209bf GIT binary patch literal 298230 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%0De-msNlQHScAoT0YHXGlU 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

5u#=AkNO1Bh{Khp(r;J0c9qFKNn8;S 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⩔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

F~tIh0c=-={;1z_h+5xwpMBB4ay^3Y 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%0AxV81L(?C79ayxI^+B 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 literal 0 HcmV?d00001 diff --git a/MindEnergy/application/deeponet-grid/images/loss_curves.png b/MindEnergy/application/deeponet-grid/images/loss_curves.png new file mode 100644 index 0000000000000000000000000000000000000000..11933f3ef7864a9cfadb469742c520191d7ce4eb GIT binary patch literal 207410 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 literal 0 HcmV?d00001 diff --git a/MindEnergy/application/deeponet-grid/images/uml.png b/MindEnergy/application/deeponet-grid/images/uml.png new file mode 100644 index 0000000000000000000000000000000000000000..35b033f54793447f778a5f729f0c58cc8ab74a78 GIT binary patch literal 199642 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

JWQLxUz@;>(xwCd`TN4A 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∾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Ν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 literal 0 HcmV?d00001 diff --git a/MindEnergy/application/deeponet-grid/issue_cn_api.md b/MindEnergy/application/deeponet-grid/issue_cn_api.md new file mode 100644 index 000000000..ec7e51803 --- /dev/null +++ b/MindEnergy/application/deeponet-grid/issue_cn_api.md @@ -0,0 +1,178 @@ +# DeepOnet-Grid + +表1-1: 设计的特性列表 + +| ISSUE编号 | ISSUE标题 | 特性等级 | 支持后端 | CANN版本 | 支持模式 | 支持平台 | MindEnergy规划版本 | MindSpore支持版本 | +|------|------|------|------|------|------|----| ------|------| +| | 【MindEnergy】DeepOnet-Grid | | Atlas 800T A2 | 8.1.RC1.beta1 | Pynative/Graph | Linux | 0.1.0 | >=2.5.0 | + +## 案例特性概述 + +### 需求来源及价值概述 + +本工作构建了一个高效的网络DeepONet-Grid,用于对故障后的电力系统进行动态安全分析,该网络 + + (i) 接收故障前和故障期间收集的轨迹作为输入,并且 + (ii) 输出预测的故障后轨迹。 + +此外,本网络还通过不确定性量化(Uncertainty Quantification)为其方法赋予了在效率与可靠/可信预测之间取得平衡的能力。 + +原始论文:[DeepONet-grid-UQ: A trustworthy deep operator framework for predicting the power grid's post-fault trajectories](!https://www.sciencedirect.com/science/article/abs/pii/S0925231223002503) + +原始代码仓:[Github Link](!https://github.com/cmoyacal/DeepONet-Grid-UQ) + + + +### 研究背景与动机 + +电力系统作为关键基础设施,其稳定性和可靠性对现代社会至关重要。然而,电网经常面临罕见但严重的故障和扰动,这些事件可能导致系统不稳定,甚至引发大规模停电。 + +传统的动态安全分析需要求解复杂的非线性微分代数方程组,计算成本极高,难以实现实时分析。随着电网的转型,电力公司迫切需要能够进行近实时的动态安全评估。 + +现有的机器学习方法主要关注二分类问题(稳定/不稳定),缺乏对故障后轨迹的定量预测能力。系统运营商和规划者需要了解故障后各种状态变量的轨迹,以评估电压或频率是否会违反预定义限制并触发负荷切除等保护措施。 + +### 方法细节 + +#### 工作原理 + +DeepONet-Grid-UQ基于深度算子网络(Deep Operator Network)理论,其核心思想是将电力系统故障后的动态行为建模为一个算子映射问题: + +- **输入函数空间**:故障前和故障期间的轨迹数据 u(t) ∈ U +- **输出函数空间**:故障后预测轨迹 y(t) ∈ Y +- **算子映射**:G: U → Y + +该网络学习从输入轨迹到输出轨迹的非线性映射关系,即: +``` +y(t) = G[u](t) +``` + +#### 网络架构设计 + +图1: 网络结构图 +![网络架构图](images/arch.png) + +DeepONet采用分支-主干(Branch-Trunk)架构: + +**分支网络(Branch Network)**: +- 输入:故障前和故障期间的轨迹序列 u(t₁), u(t₂), ..., u(tₙ) +- 功能:提取输入轨迹的特征表示 +- 输出:特征向量 b ∈ ℝᵖ + +**主干网络(Trunk Network)**: +- 输入:时间点 t +- 功能:学习时间基函数 +- 输出:时间特征向量 τ(t) ∈ ℝᵖ + +**输出计算**: +``` +G[u](t) = ⟨b, τ(t)⟩ = Σᵢ₌₁ᵖ bᵢτᵢ(t) +``` + +其中 ⟨·,·⟩ 表示内积运算。 + +#### 不确定性量化方法 + +原始工作介绍了两种不确定性量化方法: + +**1. 贝叶斯DeepONet (B-DeepONet)**: +- 使用随机梯度哈密顿蒙特卡洛(SGHMC)采样 +- 从网络参数的后验分布中采样:θ ∼ p(θ|D) +- 预测不确定性通过多次前向传播获得: + ``` + y(t) = ∫ G[u](t; θ) p(θ|D) dθ + ``` + +**2. 概率DeepONet (Prob-DeepONet)**: +- 网络同时输出预测均值 μ(t) 和预测标准差 σ(t) +- 假设输出服从高斯分布:y(t) ∼ N(μ(t), σ²(t)) +- 损失函数采用负对数似然: + ``` + L = -log p(y|μ, σ) = -log N(y; μ, σ²) + ``` + +#### 训练策略 + +**概率训练过程**: +1. **数据预处理**:将轨迹数据分割为训练/验证集 +2. **前向传播**:计算预测均值和标准差 +3. **损失计算**:使用负对数似然损失 +4. **反向传播**:更新网络参数 +5. **不确定性评估**:在测试集上评估预测不确定性 + + +### 优势与贡献 + +- **贡献**:首次将深度算子网络应用于电力系统故障分析,建立了从输入轨迹到输出轨迹的映射关系; 在DeepONet框架中引入不确定性量化,提供了预测置信度评估; 直接从原始轨迹数据学习,无需复杂的特征工程。 + +- **优势**:相比传统数值方法,推理速度提升; 能够处理未见过的故障场景,具有良好的泛化性能; 极短的响应时间(t=2.2s),可满足电力系统实时控制需求。 + +### 应用场景 + +#### 电力系统故障分析 + +**输入数据**: +- 故障前:发电机转速、电压幅值、相角等状态变量 +- 故障期间:故障类型、故障位置、故障持续时间 + +**输出预测**: +- 故障后:系统稳定性、发电机同步性、电压恢复特性 +- 不确定性:预测置信区间、风险评估 + + + +## 特性影响分析 +`DeepOnet-Grid`网络不影响其他接口。 + +## 设计方案 + +### 详细设计 +根据原始`DeepOnet-Grid`网络设计接口。 + +### 可靠性/可用性 + +#### 异常情况: +- 当输入不是两个时,报错; +- 第一个输入不是2维tensor时,报错。 +- 第二个输入不是2维tensor且列数不为1时,报错。 + +### 对外接口 + + +#### 接口说明 + +|序号|基本项|内容| +|----|----|----| +| 1 |接口定义| Prob_DeepONet | +| 2 |接口描述| 概率DeepONet网络接口| +| 3 |输入输出参数| branch, trunk, use_bias | +| 4 |方法| construct(xu, xy) -> (mu, std) | +| 5 |接口定义| B_DeepONet | +| 6 |接口描述| 贝叶斯DeepONet网络接口| +| 7 |输入输出参数| branch, trunk, use_bias | +| 8 |方法| construct(xu, xy) -> s| + + + +![UML图](images/uml.png) + + + +## 测试 + +### 测试用例设计(API接口必选) + +介绍相关API接口的配套测试用例。 + +#### UT用例 +|用例编号|用例类型|用例名称|测试对象|测试功能|测试条件|预期结果| +|----|----|----|----|----|----|----| +|1|UT|test_forward|网络前向功能|网络|固定输入和权重参数|前向结果与预期结果相同| +|2|UT|test_train|网络训练功能|网络|固定训练数据和权重参数,指定优化方法|loss收敛趋势与预期结果相同| + + + + + + + + diff --git a/MindEnergy/application/deeponet-grid/issue_cn_application.md b/MindEnergy/application/deeponet-grid/issue_cn_application.md new file mode 100644 index 000000000..c482c2651 --- /dev/null +++ b/MindEnergy/application/deeponet-grid/issue_cn_application.md @@ -0,0 +1,161 @@ +# DeepOnet-Grid + +表1-1: 设计的特性列表 + +| ISSUE编号 | ISSUE标题 | 特性等级 | 支持后端 | CANN版本 | 支持模式 | 支持平台 | MindEnergy规划版本 | MindSpore支持版本 | +|------|------|------|------|------|------|----| ------|------| +| | 【MindEnergy】DeepOnet-Grid Use Case | | Atlas 800T A2 | 8.1.RC1.beta1 | Pynative/Graph | Linux | 0.1.0 | >=2.5.0 | + +## 案例特性概述 + +### 需求来源及价值概述 + +本工作构建了一个高效的网络DeepONet-Grid,用于对故障后的电力系统进行动态安全分析,该网络 + + (i) 接收故障前和故障期间收集的轨迹作为输入,并且 + (ii) 输出预测的故障后轨迹。 + +此外,本网络还通过不确定性量化(Uncertainty Quantification)为其方法赋予了在效率与可靠/可信预测之间取得平衡的能力。 + +原始论文:[DeepONet-grid-UQ: A trustworthy deep operator framework for predicting the power grid's post-fault trajectories](!https://www.sciencedirect.com/science/article/abs/pii/S0925231223002503) + +原始代码仓:[Github Link](!https://github.com/cmoyacal/DeepONet-Grid-UQ) + + + +### 研究背景与动机 + +电力系统作为关键基础设施,其稳定性和可靠性对现代社会至关重要。然而,电网经常面临罕见但严重的故障和扰动,这些事件可能导致系统不稳定,甚至引发大规模停电。 + +传统的动态安全分析需要求解复杂的非线性微分代数方程组,计算成本极高,难以实现实时分析。随着电网的转型,电力公司迫切需要能够进行近实时的动态安全评估。 + +现有的机器学习方法主要关注二分类问题(稳定/不稳定),缺乏对故障后轨迹的定量预测能力。系统运营商和规划者需要了解故障后各种状态变量的轨迹,以评估电压或频率是否会违反预定义限制并触发负荷切除等保护措施。 + +### 方法细节 + +#### 工作原理 + +DeepONet-Grid-UQ基于深度算子网络(Deep Operator Network)理论,其核心思想是将电力系统故障后的动态行为建模为一个算子映射问题: + +- **输入函数空间**:故障前和故障期间的轨迹数据 u(t) ∈ U +- **输出函数空间**:故障后预测轨迹 y(t) ∈ Y +- **算子映射**:G: U → Y + +该网络学习从输入轨迹到输出轨迹的非线性映射关系,即: +``` +y(t) = G[u](t) +``` + +#### 网络架构设计 + +图1: 网络结构图 +![网络架构图](images/arch.png) + +DeepONet采用分支-主干(Branch-Trunk)架构: + +**分支网络(Branch Network)**: +- 输入:故障前和故障期间的轨迹序列 u(t₁), u(t₂), ..., u(tₙ) +- 功能:提取输入轨迹的特征表示 +- 输出:特征向量 b ∈ ℝᵖ + +**主干网络(Trunk Network)**: +- 输入:时间点 t +- 功能:学习时间基函数 +- 输出:时间特征向量 τ(t) ∈ ℝᵖ + +**输出计算**: +``` +G[u](t) = ⟨b, τ(t)⟩ = Σᵢ₌₁ᵖ bᵢτᵢ(t) +``` + +其中 ⟨·,·⟩ 表示内积运算。 + +#### 不确定性量化方法 + +原始工作介绍了两种不确定性量化方法,本工作只实现了概率DeepONet(Prob-DeepONet)不确定性量化方法: + +**1. 贝叶斯DeepONet (B-DeepONet)**: +- 使用随机梯度哈密顿蒙特卡洛(SGHMC)采样 +- 从网络参数的后验分布中采样:θ ∼ p(θ|D) +- 预测不确定性通过多次前向传播获得: + ``` + y(t) = ∫ G[u](t; θ) p(θ|D) dθ + ``` + +**2. 概率DeepONet (Prob-DeepONet)**: +- 网络同时输出预测均值 μ(t) 和预测标准差 σ(t) +- 假设输出服从高斯分布:y(t) ∼ N(μ(t), σ²(t)) +- 损失函数采用负对数似然: + ``` + L = -log p(y|μ, σ) = -log N(y; μ, σ²) + ``` + +#### 训练策略 + +**概率训练过程**: +1. **数据预处理**:将轨迹数据分割为训练/验证集 +2. **前向传播**:计算预测均值和标准差 +3. **损失计算**:使用负对数似然损失 +4. **反向传播**:更新网络参数 +5. **不确定性评估**:在测试集上评估预测不确定性 + + +### 优势与贡献 + +- **贡献**:首次将深度算子网络应用于电力系统故障分析,建立了从输入轨迹到输出轨迹的映射关系; 在DeepONet框架中引入不确定性量化,提供了预测置信度评估; 直接从原始轨迹数据学习,无需复杂的特征工程。 + +- **优势**:相比传统数值方法,推理速度提升; 能够处理未见过的故障场景,具有良好的泛化性能; 极短的响应时间(t=2.2s),可满足电力系统实时控制需求。 + +### 应用场景 + +#### 电力系统故障分析 + +**输入数据**: +- 故障前:发电机转速、电压幅值、相角等状态变量 +- 故障期间:故障类型、故障位置、故障持续时间 + +**输出预测**: +- 故障后:系统稳定性、发电机同步性、电压恢复特性 +- 不确定性:预测置信区间、风险评估 + + +### 目录结构 + +```shell +. +├──images +│ ├──arch.png +├──src +│ ├──data.py +│ ├──metrics.py +│ ├──model.py +│ ├──trainer.py +│ ├──utils.py +│ ├──__init__.py +├──configs +│ ├──config.yaml +├──README.md +├──Prob_DeepONet_Grid.ipynb +├──B_DeepONet_Grid.ipynb +└──train.py +``` + + + +## 结果指标 +介绍案例训练/推理的关键性能/精度指标。 +| 参数 | 指标 | +| :-----------: | :----------------------------------------------: | +| 硬件资源 | Atlas 800T A2 | +| MindSpore版本 | >=2.5.0 | +| 数据集 | [] | +| 参数量 | [] | +| 训练参数 | batch_size=[], steps_per_epoch=[], epochs=[] | +| 优化器 | Adam | +| 训练损失(MSE) | [] | +| 验证损失(MSE) | [] | +| 速度(ms/step) | [] | + + + + diff --git a/MindEnergy/application/deeponet-grid/requirements.txt b/MindEnergy/application/deeponet-grid/requirements.txt new file mode 100644 index 000000000..3b1dc7048 --- /dev/null +++ b/MindEnergy/application/deeponet-grid/requirements.txt @@ -0,0 +1,22 @@ +# MindSpore Framework +mindspore>=2.5.0 + +# Data processing +numpy>=1.21.0 +pandas>=1.3.0 +scipy>=1.7.0 + +# Configuration and utilities +pyyaml>=6.0 +tqdm>=4.62.0 + +# Optional: for visualization +matplotlib>=3.5.0 +seaborn>=0.11.0 + +# Optional: for tensorboard logging +tensorboard>=2.8.0 + +# Optional: for Jupyter notebooks +jupyter>=1.0.0 +ipykernel>=6.0.0 \ No newline at end of file diff --git a/MindEnergy/application/deeponet-grid/src/__init__.py b/MindEnergy/application/deeponet-grid/src/__init__.py new file mode 100644 index 000000000..cb419fcbe --- /dev/null +++ b/MindEnergy/application/deeponet-grid/src/__init__.py @@ -0,0 +1,19 @@ +from .trainer import DeepONetTrainer +from .data import DataGenerator +from .model import DeepONet, Prob_DeepONet +from .metrics import MetricsCalculator, compute_metrics, test, test_one, compute_r2_score, compute_mae, compute_mse, compute_calibration_error + +__all__ = [ + "DeepONetTrainer", + "DataGenerator", + "DeepONet", + "Prob_DeepONet", + "MetricsCalculator", + "compute_metrics", + "test", + "test_one", + "compute_r2_score", + "compute_mae", + "compute_mse", + "compute_calibration_error" +] \ No newline at end of file diff --git a/MindEnergy/application/deeponet-grid/src/data.py b/MindEnergy/application/deeponet-grid/src/data.py new file mode 100644 index 000000000..069a64df8 --- /dev/null +++ b/MindEnergy/application/deeponet-grid/src/data.py @@ -0,0 +1,485 @@ +import numpy as np +import pandas as pd +from scipy import stats +import mindspore as ms +from mindspore import context +from mindspore.dataset import GeneratorDataset +import mindspore.numpy as mnp +from mindspore.ops import operations as ops +from typing import Tuple, Optional, Dict, Any +import os +import yaml + +def analyze_npz_data(data_path): + """Analyze npz data file and print statistical information + + Args: + data_path (str): Path to the npz file + """ + print(f"\n{'='*50}") + print(f"Analyzing data from: {data_path}") + print(f"{'='*50}") + + # Load data + data = np.load(data_path) + + # Print available keys + print("\nAvailable data fields:") + print("-" * 30) + for key in data.files: + print(f"- {key}") + + # Analyze each field + print("\nStatistical Analysis:") + print("-" * 50) + + # Create a list to store all statistics + stats_list = [] + + for key in data.files: + arr = data[key] + print(f"\nField: {key}") + print(f"Shape: {arr.shape}") + print(f"Data type: {arr.dtype}") + + # Basic statistics + stats_dict = { + 'Field': key, + 'Shape': arr.shape, + 'Mean': np.mean(arr), + 'Median': np.median(arr), + 'Std': np.std(arr), + 'Min': np.min(arr), + 'Max': np.max(arr), + '25%': np.percentile(arr, 25), + '75%': np.percentile(arr, 75), + 'Skewness': stats.skew(arr.flatten()), + 'Kurtosis': stats.kurtosis(arr.flatten()) + } + stats_list.append(stats_dict) + + # Print statistics + print(f"Mean: {stats_dict['Mean']:.6f}") + print(f"Median: {stats_dict['Median']:.6f}") + print(f"Standard Deviation: {stats_dict['Std']:.6f}") + print(f"Min: {stats_dict['Min']:.6f}") + print(f"Max: {stats_dict['Max']:.6f}") + print(f"25th percentile: {stats_dict['25%']:.6f}") + print(f"75th percentile: {stats_dict['75%']:.6f}") + print(f"Skewness: {stats_dict['Skewness']:.6f}") + print(f"Kurtosis: {stats_dict['Kurtosis']:.6f}") + + # Check for NaN and Inf values + nan_count = np.isnan(arr).sum() + inf_count = np.isinf(arr).sum() + if nan_count > 0 or inf_count > 0: + print(f"\nWarning: Found {nan_count} NaN values and {inf_count} Inf values") + + # Print value range distribution + print("\nValue Range Distribution:") + hist, bins = np.histogram(arr.flatten(), bins=10) + for i in range(len(hist)): + print(f"[{bins[i]:.2f}, {bins[i+1]:.2f}]: {hist[i]} values") + + return stats_list + +class DataGenerator: + """Data generator for MindSpore training""" + def __init__(self, u: np.ndarray, y: np.ndarray, s: np.ndarray, dtype: str = 'float32'): + self.u = u + self.y = y + self.s = s + self.len = len(u) + self.dtype = ms.float32 if dtype == 'float32' else ms.float64 + + def __getitem__(self, index): + # Return data as MindSpore tensors with float32 dtype by default + # Ensure proper shapes for DeepONet + u_data = self.u[index] + y_data = self.y[index] + s_data = self.s[index] + + # Debug: print shapes before processing + # print(f"DEBUG: u_data.shape={u_data.shape}, y_data.shape={y_data.shape}, s_data.shape={s_data.shape}") + + # Ensure y_data is 1D for DeepONet + if len(y_data.shape) != 1: + raise ValueError(f"y_data must be 1D, got shape {y_data.shape}") + + # Ensure s_data is 1D + if len(s_data.shape) != 1: + raise ValueError(f"s_data must be 1D, got shape {s_data.shape}") + + # Convert to MindSpore tensors with specified dtype + u_tensor = ms.Tensor(u_data, self.dtype) + y_tensor = ms.Tensor(y_data, self.dtype) + s_tensor = ms.Tensor(s_data, self.dtype) + + return u_tensor, y_tensor, s_tensor + + def __len__(self): + return self.len + +def generate_synthetic_data(n_samples: int = 1000, n_sensors: int = 33, + n_points: int = 1, seed: int = 1234) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Generate synthetic data for testing + + Args: + n_samples: Number of samples + n_sensors: Number of sensors + n_points: Number of evaluation points + seed: Random seed + + Returns: + Tuple of (u, y, s) where: + - u: Input function values at sensor locations + - y: Evaluation points + - s: True solution values + """ + np.random.seed(seed) + + # Generate input functions (u) - random functions + u = np.random.randn(n_samples, n_sensors) + + # Generate evaluation points (y) + y = np.random.rand(n_samples, n_points) * 2.0 # Random points in [0, 2] + + # Generate true solutions (s) - simple example using sin function + s = np.sin(u.mean(axis=1, keepdims=True) * y) + + # Prepare data for DeepONet (expand to single query points) + expanded_u, expanded_y, expanded_s, _ = prepare_deeponet_data(u, y, s) + return expanded_u, expanded_y, expanded_s + +def load_real_data(data_path: str) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Load real data from npz file + + Args: + data_path: Path to the npz file + + Returns: + Tuple of (u, y, s) where: + - u: Input function values at sensor locations + - y: Evaluation points + - s: True solution values + """ + if not os.path.exists(data_path): + raise FileNotFoundError(f"Data file not found: {data_path}") + + data = np.load(data_path) + + # Check for required fields + required_fields = ['u', 'y', 's'] + for field in required_fields: + if field not in data.files: + raise ValueError(f"Required field '{field}' not found in data file") + + return data['u'], data['y'], data['s'] + +def normalize_data_mindspore(u: np.ndarray, y: np.ndarray, s: np.ndarray, + method: str = 'standard', dtype: str = 'float32') -> Tuple[np.ndarray, np.ndarray, np.ndarray, Dict]: + """Normalize data using MindSpore operations + + Args: + u: Input function values + y: Evaluation points + s: Solution values + method: Normalization method ('standard', 'minmax', 'none') + + Returns: + Tuple of (u_norm, y_norm, s_norm, scalers) + """ + scalers = {} + + if method == 'none': + return u, y, s, scalers + + if dtype == 'float32': + dtype = ms.float32 + elif dtype == 'float64': + dtype = ms.float64 + + # Convert to MindSpore tensors for computation + u_tensor = ms.Tensor(u, dtype) + y_tensor = ms.Tensor(y, dtype) + s_tensor = ms.Tensor(s, dtype) + + # MindSpore operations + reduce_mean = ops.ReduceMean(keep_dims=True) + reduce_std = ops.ReduceStd(keep_dims=True) + reduce_min = ops.ReduceMin(keep_dims=True) + reduce_max = ops.ReduceMax(keep_dims=True) + + if method == 'standard': + # Standard normalization: (x - mean) / std + u_mean = reduce_mean(u_tensor, 0) + u_std = reduce_std(u_tensor) + u_norm = (u_tensor - u_mean) / (u_std + 1e-8) + + y_mean = reduce_mean(y_tensor, 0) + y_std = reduce_std(y_tensor) + y_norm = (y_tensor - y_mean) / (y_std + 1e-8) + + s_mean = reduce_mean(s_tensor, 0) + s_std = reduce_std(s_tensor) + s_norm = (s_tensor - s_mean) / (s_std + 1e-8) + + # Store scalers for later use + scalers['u'] = {'mean': u_mean.asnumpy(), 'std': u_std.asnumpy()} + scalers['y'] = {'mean': y_mean.asnumpy(), 'std': y_std.asnumpy()} + scalers['s'] = {'mean': s_mean.asnumpy(), 'std': s_std.asnumpy()} + + elif method == 'minmax': + # Min-max normalization: (x - min) / (max - min) + u_min = reduce_min(u_tensor, 0) + u_max = reduce_max(u_tensor, 0) + u_norm = (u_tensor - u_min) / (u_max - u_min + 1e-8) + + y_min = reduce_min(y_tensor, 0) + y_max = reduce_max(y_tensor, 0) + y_norm = (y_tensor - y_min) / (y_max - y_min + 1e-8) + + s_min = reduce_min(s_tensor, 0) + s_max = reduce_max(s_tensor, 0) + s_norm = (s_tensor - s_min) / (s_max - s_min + 1e-8) + + # Store scalers for later use + scalers['u'] = {'min': u_min.asnumpy(), 'max': u_max.asnumpy()} + scalers['y'] = {'min': y_min.asnumpy(), 'max': y_max.asnumpy()} + scalers['s'] = {'min': s_min.asnumpy(), 'max': s_max.asnumpy()} + + # Convert back to numpy + u_norm = u_norm.asnumpy() + y_norm = y_norm.asnumpy() + s_norm = s_norm.asnumpy() + + return u_norm, y_norm, s_norm, scalers + +def split_data(u: np.ndarray, y: np.ndarray, s: np.ndarray, + train_split: float = 0.8, val_split: float = 0.1, + test_split: float = 0.1, random_state: int = 42) -> Dict[str, Tuple]: + """Split data into train, validation, and test sets + + Args: + u: Input function values + y: Evaluation points + s: Solution values + train_split: Training set fraction + val_split: Validation set fraction + test_split: Test set fraction + random_state: Random seed + + Returns: + Dictionary containing train, validation, and test data + """ + assert abs(train_split + val_split + test_split - 1.0) < 1e-6, "Splits must sum to 1.0" + + # Set random seed + np.random.seed(random_state) + + n_samples = len(u) + indices = np.random.permutation(n_samples) + + n_train = int(train_split * n_samples) + n_val = int(val_split * n_samples) + + train_indices = indices[:n_train] + val_indices = indices[n_train:n_train + n_val] + test_indices = indices[n_train + n_val:] + + data_splits = { + 'train': (u[train_indices], y[train_indices], s[train_indices]), + 'val': (u[val_indices], y[val_indices], s[val_indices]), + 'test': (u[test_indices], y[test_indices], s[test_indices]) + } + + return data_splits + +def create_mindspore_datasets(data_splits: Dict[str, Tuple], batch_size: int = 32) -> Dict[str, GeneratorDataset]: + """Create MindSpore datasets from data splits + + Args: + data_splits: Dictionary containing train, val, test data + batch_size: Batch size for training + + Returns: + Dictionary containing MindSpore datasets + """ + datasets = {} + + for split_name, (u, y, s) in data_splits.items(): + # Create data generator + data_gen = DataGenerator(u, y, s) + + # Create MindSpore dataset with three columns: u, y, s + if split_name == 'train': + dataset = GeneratorDataset( + source=data_gen, + column_names=["u", "y", "s"], + shuffle=True + ).batch(batch_size) + else: + dataset = GeneratorDataset( + source=data_gen, + column_names=["u", "y", "s"], + shuffle=False + ).batch(batch_size) + + datasets[split_name] = dataset + + return datasets + +def save_data_analysis(data_path: str, output_path: str = None): + """Save data analysis to file + + Args: + data_path: Path to data file + output_path: Path to save analysis results + """ + if output_path is None: + output_path = data_path.replace('.npz', '_analysis.csv') + + stats_list = analyze_npz_data(data_path) + df = pd.DataFrame(stats_list) + df.to_csv(output_path, index=False) + print(f"Analysis saved to: {output_path}") + +def prepare_deeponet_data(u, y, s, time_points=None): + """ + Prepare data for DeepONet training from user's data format + + Args: + u: Input functions, shape (n_samples, n_sensors) + y: Time points, shape (n_samples, n_points) - this will be converted to individual query points + s: Target values, shape (n_samples, n_points) + time_points: Optional time points array, if None will use y as time points + + Returns: + u_expanded: Expanded input functions, shape (n_samples * n_points, n_sensors) + y_expanded: Individual query points, shape (n_samples * n_points, 1) + s_expanded: Target values, shape (n_samples * n_points, 1) + metadata: Dictionary with data information + """ + + n_samples, n_sensors = u.shape + y_n_samples, y_n_points = y.shape + s_n_samples, s_n_points = s.shape + + if y_n_samples != s_n_samples or n_samples != y_n_samples: + raise ValueError(f"u, y and s must have the same number of samples, got {n_samples}, {y_n_samples} and {s_n_samples}") + + if y_n_points != s_n_points: + raise ValueError(f"y and s must have the same number of points, got {y_n_points} and {s_n_points}") + + n_points = y_n_points + + # Expand data for DeepONet training + u_expanded = [] + y_expanded = [] + s_expanded = [] + + for i in range(n_samples): + for j in range(n_points): + u_expanded.append(u[i]) # Same input function for all time points + y_expanded.append([y[i][j]]) # Single time point - 确保是2D [value] + s_expanded.append([s[i, j]]) # Target value at this time point - 确保是2D [value] + + u_expanded = np.array(u_expanded) + y_expanded = np.array(y_expanded) + s_expanded = np.array(s_expanded) + + metadata = { + 'n_samples': n_samples, + 'n_sensors': n_sensors, + 'n_points': n_points, + 'original_shapes': { + 'u': u.shape, + 'y': y.shape, + 's': s.shape + }, + 'expanded_shapes': { + 'u': u_expanded.shape, + 'y': y_expanded.shape, + 's': s_expanded.shape + } + } + return u_expanded, y_expanded, s_expanded, metadata + +def trajectory_prediction(u: np.ndarray, time_points: np.ndarray, model) -> np.ndarray: + """ + Predict trajectory for a single sample using DeepONet model + + Args: + u: Input function values for single sample, shape (n_sensors,) + time_points: Time points to predict, shape (n_time_points,) + model: Trained DeepONet model + + Returns: + predictions: Predicted values at time points, shape (n_time_points, 1) + """ + import mindspore as ms + + # Ensure u is 2D for model input + if len(u.shape) == 1: + u = u.reshape(1, -1) # (1, n_sensors) + + # Convert to MindSpore tensor + u_tensor = ms.Tensor(u, ms.float32) + + predictions = [] + + # Predict for each time point + for t in time_points: + # Create single query point + y_t = ms.Tensor([[t]], ms.float32) # (1, 1) + + # Forward pass + mean_pred, log_std_pred = model(u_tensor, y_t) + + # Get prediction value + pred_val = float(mean_pred[0, 0]) + predictions.append(pred_val) + + return np.array(predictions).reshape(-1, 1) + +def batch_trajectory_prediction(u_batch: np.ndarray, time_points: np.ndarray, model) -> np.ndarray: + """ + Predict trajectories for a batch of samples using DeepONet model + + Args: + u_batch: Input function values for batch, shape (batch_size, n_sensors) + time_points: Time points to predict, shape (n_time_points,) + model: Trained DeepONet model + + Returns: + predictions: Predicted values for all samples at time points, shape (batch_size, n_time_points, 1) + """ + batch_size = u_batch.shape[0] + n_time_points = len(time_points) + + predictions = np.zeros((batch_size, n_time_points, 1)) + + # Predict for each sample in batch + for i in range(batch_size): + u_single = u_batch[i] # (n_sensors,) + pred_single = trajectory_prediction(u_single, time_points, model) # (n_time_points, 1) + predictions[i] = pred_single + + return predictions + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser(description='Data loading and preprocessing utilities') + parser.add_argument('--data_path', type=str, required=True, + help='Path to the npz data file') + parser.add_argument('--analyze', action='store_true', + help='Analyze data and save results') + args = parser.parse_args() + + if args.analyze: + analyze_npz_data(args.data_path) + save_data_analysis(args.data_path) + else: + # Test data loading + u, y, s = load_real_data(args.data_path) + print(f"Loaded data: u shape {u.shape}, y shape {y.shape}, s shape {s.shape}") \ No newline at end of file diff --git a/MindEnergy/application/deeponet-grid/src/metrics.py b/MindEnergy/application/deeponet-grid/src/metrics.py new file mode 100644 index 000000000..00c7ff7f9 --- /dev/null +++ b/MindEnergy/application/deeponet-grid/src/metrics.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +""" +Evaluation metrics for DeepONet-Grid-UQ +""" + +import mindspore as ms +import mindspore.ops as ops +import mindspore.numpy as mnp +import numpy as np +from typing import List, Tuple, Dict, Any, Optional + +class MetricsCalculator: + """Metrics calculator for DeepONet evaluation""" + + def __init__(self): + self.norm = ops.LpNorm(axis=-1, keep_dims=False) + self.norm_l1 = ops.LpNorm(axis=-1, keep_dims=False, p=1) + self.reduce_mean = ops.ReduceMean() + self.abs = ops.Abs() + self.exp = ops.Exp() + + def l2_relative_error(self, y_true: ms.Tensor, y_pred: ms.Tensor) -> float: + diff = (y_true - y_pred).reshape(-1) + true = y_true.reshape(-1) + numerator = ops.norm(diff, ord=2) + denominator = ops.norm(true, ord=2) + value = numerator / denominator + if hasattr(value, 'asnumpy'): + value = value.asnumpy() + if hasattr(value, 'item'): + value = value.item() + return float(value) + + def l1_relative_error(self, y_true: ms.Tensor, y_pred: ms.Tensor) -> float: + diff = (y_true - y_pred).reshape(-1) + true = y_true.reshape(-1) + numerator = ops.norm(diff, ord=1) + denominator = ops.norm(true, ord=1) + value = numerator / denominator + if hasattr(value, 'asnumpy'): + value = value.asnumpy() + if hasattr(value, 'item'): + value = value.item() + return float(value) + + def fraction_in_CI(self, s: ms.Tensor, s_mean: ms.Tensor, s_std: ms.Tensor, + xi: float = 2.0, verbose: bool = False) -> float: + """Compute fraction of true trajectory in predicted confidence interval + + Args: + s: True values + s_mean: Predicted mean + s_std: Predicted standard deviation + xi: Confidence interval multiplier (default: 2.0 for 95% CI) + verbose: Whether to print results + + Returns: + Fraction of points within confidence interval + """ + # Reshape to 1D if needed + s = s.reshape(-1) + s_mean = s_mean.reshape(-1) + s_std = s_std.reshape(-1) + + # Check if points are within confidence interval + within_CI = self.abs(s - s_mean) <= xi * s_std + ratio = float(self.reduce_mean(within_CI.astype(ms.float32))) + + if verbose: + print(f"% of the true traj. within the error bars is {100 * ratio:.3f}") + + return ratio + + def trajectory_rel_error(self, s_true: ms.Tensor, s_pred: ms.Tensor, + verbose: bool = False) -> Tuple[float, float]: + """Compute trajectory relative errors + + Args: + s_true: True trajectory + s_pred: Predicted trajectory + verbose: Whether to print results + + Returns: + Tuple of (L1_error, L2_error) + """ + s_true_flat = s_true.reshape(-1) + s_pred_flat = s_pred.reshape(-1) + + l1_error = self.l1_relative_error(s_true_flat, s_pred_flat) + l2_error = self.l2_relative_error(s_true_flat, s_pred_flat) + + if verbose: + print(f"The L1 relative error is {l1_error:.5f}") + print(f"The L2 relative error is {l2_error:.5f}") + + return l1_error, l2_error + +def compute_metrics(s_true: List[ms.Tensor], s_pred: List[ms.Tensor], + metrics: List[str], verbose: bool = False) -> List[List[float]]: + """Compute metrics for multiple trajectories + + Args: + s_true: List of true trajectories + s_pred: List of predicted trajectories + metrics: List of metric names ('l1', 'l2') + verbose: Whether to print results + + Returns: + List of [max, min, mean] for each metric + """ + calculator = MetricsCalculator() + out = [] + + for metric_name in metrics: + temp = [] + for k in range(len(s_true)): + if metric_name.lower() == 'l1': + error = calculator.l1_relative_error(s_true[k], s_pred[k]) + elif metric_name.lower() == 'l2': + error = calculator.l2_relative_error(s_true[k], s_pred[k]) + else: + raise ValueError(f"Unsupported metric: {metric_name}") + temp.append(error) + + # Convert to numpy for statistics + temp_np = np.array(temp) + out.append([ + np.round(100 * np.max(temp_np), decimals=5), + np.round(100 * np.min(temp_np), decimals=5), + np.round(100 * np.mean(temp_np), decimals=5), + ]) + + if verbose: + try: + print(f"l1-relative errors: max={out[0][0]:.3f}, min={out[0][1]:.3f}, mean={out[0][2]:.3f}") + print(f"l2-relative errors: max={out[1][0]:.3f}, min={out[1][1]:.3f}, mean={out[1][2]:.3f}") + except: + print("not the correct metrics") + + return out + +def update_metrics_history(history: Dict[str, List[float]], state: List[float]) -> Dict[str, List[float]]: + """Update metrics history + + Args: + history: Current history dictionary + state: New state [max, min, mean] + + Returns: + Updated history + """ + if "max" not in history: + history["max"] = [] + if "min" not in history: + history["min"] = [] + if "mean" not in history: + history["mean"] = [] + + history["max"].append(state[0]) + history["min"].append(state[1]) + history["mean"].append(state[2]) + + return history + +def test(model: ms.nn.Cell, u: List[ms.Tensor], y: List[ms.Tensor]) -> Tuple[List[ms.Tensor], List[ms.Tensor]]: + """Test model on multiple trajectories + + Args: + model: DeepONet model + u: List of input functions + y: List of evaluation points + + Returns: + Tuple of (mean_predictions, std_predictions) + """ + mean = [] + std = [] + + for input_k in zip(u, y): + # Forward pass without computing gradients + mean_k, log_std_k = model(input_k) + std_k = ops.Exp()(log_std_k) + + mean.append(mean_k) + std.append(std_k) + + return mean, std + +def test_one(model: ms.nn.Cell, u_k: ms.Tensor, y_k: ms.Tensor) -> Tuple[ms.Tensor, ms.Tensor]: + """Test model on single trajectory + + Args: + model: DeepONet model + u_k: Input function + y_k: Evaluation point + + Returns: + Tuple of (mean_prediction, std_prediction) + """ + mean, log_std = model((u_k, y_k)) + std = ops.Exp()(log_std) + + return mean, std + +def compute_calibration_error(s_true: ms.Tensor, s_mean: ms.Tensor, s_std: ms.Tensor, + n_bins: int = 10) -> float: + """Compute calibration error for uncertainty quantification + + Args: + s_true: True values + s_mean: Predicted mean + s_std: Predicted standard deviation + n_bins: Number of bins for calibration + + Returns: + Calibration error + """ + # Normalize residuals + residuals = (s_true - s_mean) / s_std + + # Compute empirical CDF + sorted_residuals = ops.Sort()(residuals.reshape(-1))[0] + n_points = sorted_residuals.shape[0] + + # Create bins + bin_edges = mnp.linspace(0, 1, n_bins + 1) + bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2 + + # Compute empirical probabilities + empirical_probs = [] + for i in range(n_bins): + start_idx = int(bin_edges[i] * n_points) + end_idx = int(bin_edges[i + 1] * n_points) + empirical_probs.append((end_idx - start_idx) / n_points) + + empirical_probs = mnp.array(empirical_probs) + + # Compute theoretical probabilities (uniform for well-calibrated model) + theoretical_probs = mnp.ones(n_bins) / n_bins + + # Compute calibration error + calibration_error = ops.ReduceSum()((empirical_probs - theoretical_probs) ** 2) + + return float(calibration_error) + +def compute_r2_score(y_true: ms.Tensor, y_pred: ms.Tensor) -> float: + """Compute R² score + + Args: + y_true: True values + y_pred: Predicted values + + Returns: + R² score + """ + ss_res = ops.ReduceSum()((y_true - y_pred) ** 2) + ss_tot = ops.ReduceSum()((y_true - ops.ReduceMean()(y_true)) ** 2) + + r2 = 1 - ss_res / ss_tot + return float(r2) + +def compute_mae(y_true: ms.Tensor, y_pred: ms.Tensor) -> float: + """Compute Mean Absolute Error + + Args: + y_true: True values + y_pred: Predicted values + + Returns: + MAE value + """ + mae = ops.ReduceMean()(ops.Abs()(y_true - y_pred)) + return float(mae) + +def compute_mse(y_true: ms.Tensor, y_pred: ms.Tensor) -> float: + """Compute Mean Squared Error + + Args: + y_true: True values + y_pred: Predicted values + + Returns: + MSE value + """ + mse = ops.ReduceMean()((y_true - y_pred) ** 2) + return float(mse) diff --git a/MindEnergy/application/deeponet-grid/src/model.py b/MindEnergy/application/deeponet-grid/src/model.py new file mode 100644 index 000000000..111fa1ada --- /dev/null +++ b/MindEnergy/application/deeponet-grid/src/model.py @@ -0,0 +1,225 @@ +import mindspore as ms +from mindspore import nn, mint, Parameter, ops +import mindspore.numpy as np +from mindspore.common.initializer import initializer, XavierNormal, Zero + +from typing import Any + +# MLP +class MLP(nn.Cell): + def __init__(self, layer_size: list, activation: str) -> None: + super(MLP, self).__init__() + layers = [] + for k in range(len(layer_size) - 2): + layers.append(nn.Dense(layer_size[k], layer_size[k+1], has_bias=True)) + layers.append(get_activation(activation)) + layers.append(nn.Dense(layer_size[-2], layer_size[-1], has_bias=True)) + self.net = nn.SequentialCell(layers) + self.net.apply(self._init_weights) + + def _init_weights(self, m: Any) -> None: + if isinstance(m, nn.Dense): + m.weight.set_data(initializer(XavierNormal(), m.weight.shape, m.weight.dtype)) + m.bias.set_data(initializer(Zero(), m.bias.shape, m.bias.dtype)) + + def construct(self, x: ms.Tensor) -> ms.Tensor: + return self.net(x) + +# modified MLP +class modified_MLP(nn.Cell): + def __init__(self, layer_size: list, activation: str) -> None: + super(modified_MLP, self).__init__() + layers = [] + for k in range(len(layer_size) - 1): + layers.append(nn.Dense(layer_size[k], layer_size[k+1], has_bias=True)) + self.net = nn.SequentialCell(layers) + + self.U = nn.SequentialCell([nn.Dense(layer_size[0], layer_size[1], has_bias=True)]) + self.V = nn.SequentialCell([nn.Dense(layer_size[0], layer_size[1], has_bias=True)]) + self.activation = get_activation(activation) + + self.net.apply(self._init_weights) + self.U.apply(self._init_weights) + self.V.apply(self._init_weights) + + def _init_weights(self, m: Any) -> None: + if isinstance(m, nn.Dense): + m.weight.set_data(initializer(XavierNormal(), m.weight.shape, m.weight.dtype)) + m.bias.set_data(initializer(Zero(), m.bias.shape, m.bias.dtype)) + + def construct(self, x: ms.Tensor) -> ms.Tensor: + u = self.activation(self.U(x)) + v = self.activation(self.V(x)) + for k in range(len(self.net) - 1): + y = self.net[k](x) + y = self.activation(y) + x = y * u + (1 - y) * v + y = self.net[-1](x) + return y + +# get activation function from str +def get_activation(identifier: str) -> Any: + """get activation function.""" + return{ + "elu": nn.ELU(), + "relu": nn.ReLU(), + "selu": nn.SeLU(), + "sigmoid": nn.Sigmoid(), + "leaky": nn.LeakyReLU(), + "tanh": nn.Tanh(), + "sin": sin_act(), + # "softplus": nn.Softplus(), + "Rrelu": nn.RReLU(), + "gelu": nn.GELU(), + "silu": nn.SiLU(), + "Mish": nn.Mish(), + }[identifier] + +# sin activation function +class sin_act(nn.Cell): + def __init__(self): + super(sin_act, self).__init__() + + def construct(self, x: ms.Tensor) -> ms.Tensor: + return mint.sin(x) + + +class DeepONet(nn.Cell): + """ + Base DeepONet class that serves as an interface for different DeepONet implementations. + """ + def __init__(self, branch: dict, trunk: dict, use_bias: bool=True) -> None: + super(DeepONet, self).__init__() + # Branch + if branch["type"] == "MLP": + self.branch = MLP(branch["layer_size"][:-2], branch["activation"]) + elif branch["type"] == "modified": + self.branch = modified_MLP(branch["layer_size"][:-2], branch["activation"]) + else: + raise ValueError(f"Unsupported branch type: {branch['type']}. Supported: 'MLP', 'modified'.") + # Trunk + if trunk["type"] == "MLP": + self.trunk = MLP(trunk["layer_size"][:-2], trunk["activation"]) + elif trunk["type"] == "modified": + self.trunk = modified_MLP(trunk["layer_size"][:-2], trunk["activation"]) + else: + raise ValueError(f"Unsupported trunk type: {trunk['type']}. Supported: 'MLP', 'modified'.") + + self.use_bias = use_bias + + def _init_weights(self, m): + """Initialize weights for dense layers.""" + if isinstance(m, nn.Dense): + m.weight.set_data(initializer(XavierNormal(), m.weight.shape, m.weight.dtype)) + m.bias.set_data(initializer(Zero(), m.bias.shape, m.bias.dtype)) + + def construct(self, xu, xy): + """ + Forward pass interface. To be implemented by subclasses. + 前向传播接口,由子类实现。 + + Args: + xu: Branch input tensor + xy: Trunk input tensor + + Returns: + Model output (to be defined by subclasses) + """ + raise NotImplementedError("construct method must be implemented by subclasses") + + +class Prob_DeepONet(DeepONet): + def __init__(self, branch: dict, trunk: dict, use_bias: bool=True) -> None: + super(Prob_DeepONet, self).__init__(branch, trunk, use_bias) + + # Add probabilistic components + if use_bias: + self.bias_mu = Parameter(ms.Tensor(np.randn(1), ms.float32), requires_grad=True) + self.bias_std = Parameter(ms.Tensor(np.randn(1), ms.float32), requires_grad=True) + + self.branch_mu = nn.SequentialCell([ + get_activation(branch["activation"]), + nn.Dense(branch["layer_size"][-3], branch["layer_size"][-2], has_bias=True), + get_activation(branch["activation"]), + nn.Dense(branch["layer_size"][-2], branch["layer_size"][-1], has_bias=True) + ]) + + self.branch_std = nn.SequentialCell([ + get_activation(branch["activation"]), + nn.Dense(branch["layer_size"][-3], branch["layer_size"][-2], has_bias=True), + get_activation(branch["activation"]), + nn.Dense(branch["layer_size"][-2], branch["layer_size"][-1], has_bias=True) + ]) + + self.trunk_mu = nn.SequentialCell([ + get_activation(trunk["activation"]), + nn.Dense(trunk["layer_size"][-3], trunk["layer_size"][-2], has_bias=True), + get_activation(trunk["activation"]), + nn.Dense(trunk["layer_size"][-2], trunk["layer_size"][-1], has_bias=True) + ]) + + self.trunk_std = nn.SequentialCell([ + get_activation(trunk["activation"]), + nn.Dense(trunk["layer_size"][-3], trunk["layer_size"][-2], has_bias=True), + get_activation(trunk["activation"]), + nn.Dense(trunk["layer_size"][-2], trunk["layer_size"][-1], has_bias=True) + ]) + + self.branch_mu.apply(self._init_weights) + self.branch_std.apply(self._init_weights) + self.trunk_mu.apply(self._init_weights) + self.trunk_std.apply(self._init_weights) + + def _init_weights(self, m): + if isinstance(m, nn.Dense): + m.weight.set_data(initializer(XavierNormal(), m.weight.shape, m.weight.dtype)) + m.bias.set_data(initializer(Zero(), m.bias.shape, m.bias.dtype)) + + def construct(self, xu, xy) -> list: + u, y = xu, xy + b = self.branch(u) + t = self.trunk(y) + # branch prediction and UQ + b_mu = self.branch_mu(b) + b_std = self.branch_std(b) + # trunk prediction and UQ + t_mu = self.trunk_mu(t) + t_std = self.trunk_std(t) + + # dot product using element-wise multiplication and sum + # Use ReduceSum operation for proper reduction + reduce_sum = ops.ReduceSum(keep_dims=True) + mu = reduce_sum(b_mu * t_mu, 1) # Reduce along feature dimension + log_std = reduce_sum(b_std * t_std, 1) # Reduce along feature dimension + + if self.use_bias: + mu += self.bias_mu + log_std += self.bias_std + return (mu, log_std) + + +# test codes for Prob_DeepONet +if __name__ == "__main__": + branch = { + "type": "modified", + "layer_size": [33, 200, 200, 200, 100], + "activation": "sin" + } + trunk = { + "type": "modified", + "layer_size": [100, 200, 200, 200, 100], + "activation": "sin" + } + use_bias = True + + model = Prob_DeepONet(branch, trunk, use_bias) + # xu = ms.Tensor(np.ones((10, 33)), ms.float32) + # xy = ms.Tensor(np.ones((10, 100)), ms.float32) + xu = ms.Tensor(np.randn(10, 33), ms.float32) + xy = ms.Tensor(np.randn(10, 100), ms.float32) + mu, log_std = model(xu, xy) + print(mu) + print(log_std) + + + \ No newline at end of file diff --git a/MindEnergy/application/deeponet-grid/src/trainer.py b/MindEnergy/application/deeponet-grid/src/trainer.py new file mode 100644 index 000000000..841bb9739 --- /dev/null +++ b/MindEnergy/application/deeponet-grid/src/trainer.py @@ -0,0 +1,628 @@ +import mindspore as ms +import mindspore.nn as nn +import mindspore.ops as ops +from mindspore import context, Parameter +from mindspore.dataset import GeneratorDataset +import mindspore.numpy as mnp +from mindspore.train.callback import Callback, LossMonitor, TimeMonitor +from mindspore.train import Model +from mindspore.train.loss_scale_manager import DynamicLossScaleManager +from mindspore.train.amp import auto_mixed_precision +from mindspore.common.initializer import initializer, XavierNormal, Zero +import numpy as np +from typing import Any, List, Tuple, Optional, Dict +import os +import yaml +import time +from tqdm.auto import trange +import logging + + +# Import metrics +from .metrics import compute_metrics, update_metrics_history, test, test_one, compute_calibration_error, compute_r2_score, compute_mae, compute_mse + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +class CustomCallback(Callback): + """Custom callback for training monitoring""" + def __init__(self, print_every: int = 10, save_every: int = 100): + super(CustomCallback, self).__init__() + self.print_every = print_every + self.save_every = save_every + self.step = 0 + self.losses = [] + + def step_end(self, run_context): + """Called at the end of each step""" + self.step += 1 + cb_params = run_context.original_args() + loss = cb_params.net_outputs + + if isinstance(loss, (list, tuple)): + loss = loss[0] + + self.losses.append(float(loss)) + + if self.step % self.print_every == 0: + logger.info(f"Step {self.step}, Loss: {float(loss):.6f}") + +class ProbabilisticLoss(nn.Cell): + """Negative log likelihood loss for probabilistic DeepONet + Equivalent to torch.distributions.Normal(mean_pred, torch.exp(log_std_pred)).log_prob(target).mean() + """ + def __init__(self): + super(ProbabilisticLoss, self).__init__() + self.log = ops.Log() + self.exp = ops.Exp() + self.square = ops.Square() + self.reduce_mean = ops.ReduceMean() + self.log_2pi = np.log(2 * np.pi) + + def construct(self, mean_pred, log_std_pred, target): + """Compute negative log likelihood loss using normal distribution + + Args: + mean_pred: Predicted mean + log_std_pred: Predicted log standard deviation + target: Target values + + Returns: + Negative mean log probability + """ + # Convert log_std to std + std_pred = self.exp(log_std_pred) + + # Compute log probability of normal distribution + # log_prob = -0.5 * ((x - mu) / sigma)^2 - log(sigma) - 0.5 * log(2*pi) + log_prob = -0.5 * self.square((target - mean_pred) / std_pred) - log_std_pred - 0.5 * self.log_2pi + + # Return negative mean log probability (equivalent to -dist.log_prob(target).mean()) + return -self.reduce_mean(log_prob) + +class MSELoss(nn.Cell): + """Mean squared error loss""" + def __init__(self): + super(MSELoss, self).__init__() + self.mse = nn.MSELoss() + + def construct(self, mean_pred, log_std_pred, target): + """Compute MSE loss (ignores uncertainty predictions) + + Args: + mean_pred: Predicted mean + log_std_pred: Predicted log standard deviation (ignored) + target: Target values + + Returns: + MSE loss value + """ + return self.mse(mean_pred, target) + +class DeepONetTrainer: + """Trainer class for DeepONet with uncertainty quantification""" + def __init__(self, + model: nn.Cell, + config: Dict[str, Any], + save_dir: str = "outputs"): + + self.model = model + self.config = config + self.save_dir = save_dir + self.training_config = config['training'] + + # Create save directory + os.makedirs(save_dir, exist_ok=True) + + # Initialize optimizer + self.optimizer = self._create_optimizer() + + # Initialize loss function + self.loss_fn = self._create_loss_function() + + # Initialize learning rate scheduler + self.scheduler = self._create_scheduler() + + # Initialize metrics + self.metrics = { + 'train_loss': [], + 'val_loss': [], + 'best_loss': float('inf'), + 'patience_counter': 0 + } + + # Set up device context + self._setup_device() + + def _setup_device(self): + """Set up device context""" + device_config = self.config['device'] + context.set_context( + mode=getattr(context, device_config['mode']), + device_target=device_config['target'] + ) + logger.info(f"Device set to: {device_config['target']}, Mode: {device_config['mode']}") + + def _create_optimizer(self) -> nn.Optimizer: + """Create optimizer based on configuration""" + optimizer_type = self.training_config.get('optimizer', 'adam') + learning_rate = float(self.training_config['learning_rate']) # Ensure it's a float + weight_decay = self.training_config.get('weight_decay', 0.0) + + if optimizer_type.lower() == 'adam': + return nn.Adam( + self.model.trainable_params(), + learning_rate=learning_rate, + weight_decay=weight_decay + ) + elif optimizer_type.lower() == 'sgd': + return nn.SGD( + self.model.trainable_params(), + learning_rate=learning_rate, + weight_decay=weight_decay + ) + elif optimizer_type.lower() == 'adamw': + return nn.AdamWeightDecay( + self.model.trainable_params(), + learning_rate=learning_rate, + weight_decay=weight_decay + ) + else: + raise ValueError(f"Unsupported optimizer: {optimizer_type}") + + def _create_loss_function(self) -> nn.Cell: + """Create loss function based on configuration""" + loss_type = self.training_config.get('loss_type', 'nll') + + if loss_type.lower() == 'nll': + return ProbabilisticLoss() + elif loss_type.lower() == 'mse': + return MSELoss() + else: + raise ValueError(f"Unsupported loss type: {loss_type}") + + def _create_scheduler(self) -> Optional[Any]: + """Create learning rate scheduler""" + scheduler_type = self.training_config.get('scheduler', None) + + if scheduler_type is None: + return None + + if scheduler_type == 'reduce_lr_on_plateau': + # Custom implementation for reduce LR on plateau + return ReduceLROnPlateau( + self.optimizer, + mode='min', + patience=self.training_config.get('patience', 10), + factor=self.training_config.get('factor', 0.8), + verbose=self.training_config.get('verbose', True) + ) + elif scheduler_type == 'cosine': + return nn.CosineAnnealingLR( + self.optimizer, + T_max=self.training_config['epochs'], + eta_min=self.training_config.get('min_lr', 1e-7) + ) + elif scheduler_type == 'step': + return nn.StepLR( + self.optimizer, + step_size=self.training_config.get('step_size', 100), + gamma=self.training_config.get('factor', 0.8) + ) + else: + logger.warning(f"Unsupported scheduler: {scheduler_type}") + return None + + def _reduce_lr_on_plateau(self, val_loss: float): + """Reduce learning rate if validation loss plateaus""" + if val_loss < self.metrics['best_loss']: + self.metrics['best_loss'] = val_loss + self.metrics['patience_counter'] = 0 + else: + self.metrics['patience_counter'] += 1 + + if self.metrics['patience_counter'] >= self.training_config['patience']: + # Reduce learning rate + current_lr = float(self.optimizer.learning_rate) # Ensure it's a float + factor = float(self.training_config['factor']) # Ensure it's a float + min_lr = float(self.training_config.get('min_lr', 1e-7)) # Ensure it's a float + new_lr = max(current_lr * factor, min_lr) + + if new_lr < current_lr: + self.optimizer.learning_rate.set_data(ms.Tensor(new_lr, ms.float32)) + logger.info(f"Reducing learning rate to {new_lr:.2e}") + self.metrics['patience_counter'] = 0 + + def train_step(self, u: ms.Tensor, y: ms.Tensor, + target: ms.Tensor) -> Tuple[ms.Tensor, ms.Tensor, ms.Tensor]: + """Single training step + + Args: + u: Input function values + y: Evaluation points + target: Target values + + Returns: + Tuple of (loss, mean_pred, log_std_pred) + """ + def forward_fn(): + mean_pred, log_std_pred = self.model(u, y) + loss = self.loss_fn(mean_pred, log_std_pred, target) + return loss, mean_pred, log_std_pred + + grad_fn = ops.value_and_grad(forward_fn, None, self.optimizer.parameters, + has_aux=True) + + (loss, mean_pred, log_std_pred), grads = grad_fn() + self.optimizer(grads) + + return loss, mean_pred, log_std_pred + + def validate(self, val_dataset: GeneratorDataset) -> float: + """Validate model on validation dataset + + Args: + val_dataset: Validation dataset + + Returns: + Average validation loss + """ + self.model.set_train(False) + total_loss = 0.0 + num_batches = 0 + + for u, y, target in val_dataset: + mean_pred, log_std_pred = self.model(u, y) + loss = self.loss_fn(mean_pred, log_std_pred, target) + total_loss += float(loss) + num_batches += 1 + + self.model.set_train(True) + return total_loss / num_batches if num_batches > 0 else float('inf') + + def train(self, + train_dataset: GeneratorDataset, + val_dataset: Optional[GeneratorDataset] = None) -> Dict[str, List[float]]: + """Train the model + + Args: + train_dataset: Training dataset + val_dataset: Validation dataset (optional) + + Returns: + Training history + """ + import time + epochs = self.training_config['epochs'] + print_every = self.training_config.get('print_every', 10) + eval_every = self.training_config.get('eval_every', 100) # 100 steps + verbose = self.training_config.get('verbose', True) + + # Log model parameter count + total_params = sum(p.size for p in self.model.get_parameters()) + trainable_params = sum(p.size for p in self.model.trainable_params()) + logging.info(f"Total model parameters: {total_params}") + logging.info(f"Trainable parameters: {trainable_params}") + # Log training data size + train_data_size = train_dataset.get_dataset_size() * train_dataset.get_batch_size() + logging.info(f"Training data size (number of samples): {train_data_size}") + logging.info(f"Training batch size : {train_dataset.get_batch_size()}") + + # Print all parameter names, shapes, and sizes for debugging + print("Model parameter details:") + for p in self.model.get_parameters(): + print(f"{p.name}: shape={p.shape}, size={p.size}") + + if verbose: + print(f"\n***** Probabilistic Training for {epochs} epochs *****\n") + + # Initialize best values and logger + best = {} + best["prob loss"] = float('inf') + + logger_hist = {} + logger_hist["prob loss"] = [] + logger_hist["val loss"] = [] + + global_step = 0 + + for epoch in range(epochs): + self.model.set_train() + epoch_loss = 0 + batch_count = 0 + for u, y, target in train_dataset: + step_start_time = time.time() + loss, mean_pred, log_std_pred = self.train_step(u, y, target) + step_time = time.time() - step_start_time + epoch_loss += float(loss) + batch_count += 1 + global_step += 1 + # show loss + if global_step % print_every == 0: + # Negative log likelihood loss can be negative, so we print its negation for clarity. + msg = (f"Epoch {epoch+1}, Step {global_step}, Batch {batch_count}, " + f"Loss: {float(loss):.6f}, " + f"Step time: {step_time:.3f}s") + # print(msg) + logging.info(msg) + # evaluate + if val_dataset is not None and global_step % eval_every == 0: + val_loss = self.validate(val_dataset) + logger_hist["val loss"].append((global_step, val_loss)) + # Negative log likelihood loss can be negative + msg = (f"[Eval] Epoch {epoch+1}, Step {global_step}, Val-Loss: {float(val_loss):.6f} ") + # print(msg) + logging.info(msg) + # save best ckpt + if val_loss < best["prob loss"]: + best["prob loss"] = val_loss + self.save_model('best_model.ckpt') + # Compute average epoch loss + try: + avg_epoch_loss = epoch_loss / batch_count + except ZeroDivisionError as e: + print(f"error: {e}, batch size larger than number of training examples") + continue + logger_hist["prob loss"].append(avg_epoch_loss) + # show after each epoch + if verbose: + # Negative log likelihood loss can be negative + print(f'Epoch {epoch+1}/{epochs}:') + print(f' Train-Loss: {float(avg_epoch_loss):.6f} ') + print(f' Best-Loss: {float(best["prob loss"]):.6f} ') + return logger_hist + + def save_model(self, filename: str): + """Save model checkpoint + + Args: + filename: Name of the checkpoint file + """ + save_path = os.path.join(self.save_dir, filename) + ms.save_checkpoint(self.model, save_path) + logger.info(f"Model saved to: {save_path}") + + def load_model(self, filename: str): + """Load model checkpoint + + Args: + filename: Name of the checkpoint file + """ + load_path = os.path.join(self.save_dir, filename) + if os.path.exists(load_path): + ms.load_checkpoint(load_path, self.model) + logger.info(f"Model loaded from: {load_path}") + else: + logger.warning(f"Checkpoint not found: {load_path}") + + def predict(self, u: ms.Tensor, y: ms.Tensor) -> Tuple[ms.Tensor, ms.Tensor]: + """Make predictions + + Args: + u: Input function values + y: Evaluation points + + Returns: + Tuple of (mean_pred, log_std_pred) + """ + self.model.set_train(False) + mean_pred, log_std_pred = self.model(u, y) + self.model.set_train(True) + return mean_pred, log_std_pred + + def evaluate(self, test_dataset: GeneratorDataset) -> Dict[str, float]: + """Evaluate model on test dataset + + Args: + test_dataset: Test dataset + + Returns: + Dictionary of evaluation metrics + """ + self.model.set_train(False) + + # Collect predictions and targets + all_predictions = [] + all_targets = [] + all_means = [] + all_stds = [] + + for u, y, target in test_dataset: + # Forward pass + mean_pred, log_std_pred = self.model(u, y) + std_pred = ops.Exp()(log_std_pred) + + # Store results + all_predictions.append(mean_pred) + all_targets.append(target) + all_means.append(mean_pred) + all_stds.append(std_pred) + + # Concatenate all batches + if all_predictions: + predictions = ops.Concat(axis=0)(all_predictions) + targets = ops.Concat(axis=0)(all_targets) + means = ops.Concat(axis=0)(all_means) + stds = ops.Concat(axis=0)(all_stds) + else: + return {} + + # Compute basic metrics + metrics = {} + metrics['mse'] = compute_mse(targets, predictions) + metrics['mae'] = compute_mae(targets, predictions) + metrics['r2'] = compute_r2_score(targets, predictions) + + # Compute relative errors + from .metrics import MetricsCalculator + calculator = MetricsCalculator() + metrics['l1_relative_error'] = calculator.l1_relative_error(targets, predictions) + metrics['l2_relative_error'] = calculator.l2_relative_error(targets, predictions) + + # Compute uncertainty quantification metrics + metrics['calibration_error'] = compute_calibration_error(targets, means, stds) + metrics['fraction_in_CI'] = calculator.fraction_in_CI(targets, means, stds, xi=2.0) + + # Compute trajectory errors if data is structured as trajectories + try: + l1_traj, l2_traj = calculator.trajectory_rel_error(targets, predictions) + metrics['trajectory_l1_error'] = l1_traj + metrics['trajectory_l2_error'] = l2_traj + except: + pass + + return metrics + + def test_trajectories(self, u_test: List[ms.Tensor], y_test: List[ms.Tensor], + s_test: List[ms.Tensor]) -> Tuple[List[ms.Tensor], List[ms.Tensor]]: + """Test model on trajectory data + + Args: + u_test: List of input functions for test trajectories + y_test: List of evaluation points for test trajectories + s_test: List of true solutions for test trajectories + + Returns: + Tuple of (mean_predictions, std_predictions) + """ + # MindSpore does not have set_eval(), use set_train(False) for evaluation mode + self.model.set_train(False) + + mean_predictions, std_predictions = test(self.model, u_test, y_test) + + return mean_predictions, std_predictions + + def compute_trajectory_metrics(self, s_test: List[ms.Tensor], + mean_predictions: List[ms.Tensor], + std_predictions: List[ms.Tensor], + verbose: bool = False) -> Dict[str, Any]: + """Compute metrics for trajectory predictions + + Args: + s_test: List of true solutions + mean_predictions: List of predicted means + std_predictions: List of predicted standard deviations + verbose: Whether to print results + + Returns: + Dictionary of trajectory metrics + """ + # Compute L1 and L2 relative errors + metrics_state = compute_metrics(s_test, mean_predictions, ['l1', 'l2'], verbose=verbose) + + # Compute fraction in confidence interval for each trajectory + from .metrics import MetricsCalculator + calculator = MetricsCalculator() + + ci_fractions = [] + for i in range(len(s_test)): + ci_frac = calculator.fraction_in_CI(s_test[i], mean_predictions[i], std_predictions[i]) + ci_fractions.append(ci_frac) + + avg_ci_fraction = np.mean(ci_fractions) + + return { + 'l1_metrics': metrics_state[0], # [max, min, mean] + 'l2_metrics': metrics_state[1], # [max, min, mean] + 'avg_ci_fraction': avg_ci_fraction, + 'ci_fractions': ci_fractions + } + +class ReduceLROnPlateau: + """Reduce learning rate when a metric has stopped improving""" + + def __init__(self, optimizer, mode='min', factor=0.1, patience=10, verbose=False, + min_lr=0, eps=1e-8): + self.optimizer = optimizer + self.mode = mode + self.factor = float(factor) # Ensure it's a float + self.patience = int(patience) # Ensure it's an int + self.verbose = verbose + self.min_lr = float(min_lr) # Ensure it's a float + self.eps = float(eps) # Ensure it's a float + + self.best = None + self.num_bad_epochs = 0 + self.mode_worse = float('inf') if mode == 'min' else float('-inf') + + def step(self, metrics): + """Update learning rate based on metrics""" + if self.best is None: + self.best = float(metrics) # Ensure it's a float + elif self._is_better(metrics, self.best): + self.best = float(metrics) # Ensure it's a float + self.num_bad_epochs = 0 + else: + self.num_bad_epochs += 1 + + if self.num_bad_epochs >= self.patience: + self._reduce_lr() + self.num_bad_epochs = 0 + + def _is_better(self, current, best): + """Check if current metrics are better than best""" + current = float(current) # Ensure it's a float + best = float(best) # Ensure it's a float + if self.mode == 'min': + return current < best - self.eps + else: + return current > best + self.eps + + def _reduce_lr(self): + """Reduce learning rate for all parameter groups""" + for param_group in self.optimizer.param_groups: + old_lr = float(param_group['lr']) # Ensure it's a float + new_lr = max(old_lr * self.factor, self.min_lr) + param_group['lr'] = new_lr + + if self.verbose and old_lr != new_lr: + logger.info(f'Reducing learning rate from {old_lr:.6f} to {new_lr:.6f}') + +def create_trainer(model: nn.Cell, config: Dict[str, Any], save_dir: str = "outputs") -> DeepONetTrainer: + """Create trainer instance + + Args: + model: DeepONet model + config: Configuration dictionary + save_dir: Directory to save outputs + + Returns: + Trainer instance + """ + return DeepONetTrainer(model, config, save_dir) + +if __name__ == "__main__": + # Test trainer + from model import Prob_DeepONet + from data import generate_synthetic_data, create_mindspore_datasets, split_data + + # Load config + with open('configs/config.yaml', 'r') as f: + config = yaml.safe_load(f) + + # Generate test data + u, y, s = generate_synthetic_data(n_samples=100, n_sensors=50, n_points=1) + data_splits = split_data(u, y, s, train_split=0.8, val_split=0.1, test_split=0.1) + datasets = create_mindspore_datasets(data_splits, batch_size=16) + + # Create model + branch_config = config['model']['branch'] + trunk_config = config['model']['trunk'] + + # Update layer sizes based on data + branch_config['layer_size'][0] = u.shape[-1] # Input dimension + trunk_config['layer_size'][0] = y.shape[-1] # Input dimension + + model = Prob_DeepONet(branch_config, trunk_config, config['model']['use_bias']) + + # Create trainer + trainer = create_trainer(model, config) + + # Train model + history = trainer.train(datasets['train'], datasets['val']) + + # Evaluate model + metrics = trainer.evaluate(datasets['test']) + + print("Training completed successfully!") diff --git a/MindEnergy/application/deeponet-grid/src/utils.py b/MindEnergy/application/deeponet-grid/src/utils.py new file mode 100644 index 000000000..e4a48cde2 --- /dev/null +++ b/MindEnergy/application/deeponet-grid/src/utils.py @@ -0,0 +1,374 @@ +#!/usr/bin/env python3 +""" +Utility functions for DeepONet-Grid-UQ +DeepONet-Grid-UQ工具函数 +""" + +import re +import matplotlib.pyplot as plt +import numpy as np +from typing import List, Tuple, Dict, Optional +import json +import os + +def parse_loss_log(log_file_path: str) -> Tuple[List[float], List[int], List[int]]: + """ + Parse loss log file and extract loss values, epochs, and steps + + Args: + log_file_path: Path to the log file + + Returns: + Tuple of (losses, epochs, steps) where: + - losses: List of loss values + - epochs: List of epoch numbers + - steps: List of step numbers + """ + losses = [] + epochs = [] + steps = [] + + if not os.path.exists(log_file_path): + print(f"Log file not found: {log_file_path}") + return losses, epochs, steps + + # Regular expression to match the log format + # INFO:root:Epoch 1, Step 100, Batch 100, Loss: -0.524214, Step time: 0.040s + pattern = r'INFO:root:Epoch (\d+), Step (\d+), Batch \d+, Loss: ([-\d.]+), Step time: \d+\.\d+s' + + with open(log_file_path, 'r', encoding='utf-8') as f: + for line in f: + match = re.search(pattern, line) + if match: + epoch = int(match.group(1)) + step = int(match.group(2)) + loss = float(match.group(3)) + + epochs.append(epoch) + steps.append(step) + losses.append(loss) + + print(f"Parsed {len(losses)} loss entries from log file") + return losses, epochs, steps + +def parse_val_loss_log(log_file_path: str) -> Tuple[List[float], List[int], List[int]]: + """ + Parse validation loss log file and extract validation loss values + + Args: + log_file_path: Path to the log file + + Returns: + Tuple of (val_losses, epochs, steps) where: + - val_losses: List of validation loss values + - epochs: List of epoch numbers + - steps: List of step numbers + """ + val_losses = [] + epochs = [] + steps = [] + + if not os.path.exists(log_file_path): + print(f"Log file not found: {log_file_path}") + return val_losses, epochs, steps + + # Regular expression to match validation loss log format + # INFO:root:[Eval] Epoch 1, Step 100, Val-Loss: -0.705899 (negated for display) + pattern = r'INFO:root:\[Eval\] Epoch (\d+), Step (\d+), Val-Loss: ([-\d.]+) \(negated for display\)' + + with open(log_file_path, 'r', encoding='utf-8') as f: + for line in f: + match = re.search(pattern, line) + if match: + epoch = int(match.group(1)) + step = int(match.group(2)) + val_loss = float(match.group(3)) + + epochs.append(epoch) + steps.append(step) + val_losses.append(val_loss) + + print(f"Parsed {len(val_losses)} validation loss entries from log file") + return val_losses, epochs, steps + +def plot_loss_curves(log_file_path: str, + save_path: Optional[str] = None, + show_plot: bool = True, + figsize: Tuple[int, int] = (12, 8)) -> None: + """ + Plot training and validation loss curves from log file + + Args: + log_file_path: Path to the log file + save_path: Path to save the plot (optional) + show_plot: Whether to display the plot + figsize: Figure size (width, height) + """ + # Parse loss data + losses, epochs, steps = parse_loss_log(log_file_path) + val_losses, val_epochs, val_steps = parse_val_loss_log(log_file_path) + + if not losses: + print("No loss data found in log file") + return + + # Create figure + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=figsize) + + # Plot training loss + ax1.plot(steps, losses, 'b-', label='Training Loss', linewidth=1, alpha=0.8) + ax1.set_xlabel('Training Steps') + ax1.set_ylabel('Loss (negated for display)') + ax1.set_title('Training Loss Curve') + ax1.grid(True, alpha=0.3) + ax1.legend() + + # Plot validation loss if available + if val_losses: + ax1.plot(val_steps, val_losses, 'r-', label='Validation Loss', linewidth=1, alpha=0.8) + ax1.legend() + + # Plot loss vs epochs + unique_epochs = list(set(epochs)) + epoch_losses = [] + for epoch in unique_epochs: + epoch_indices = [i for i, e in enumerate(epochs) if e == epoch] + epoch_loss = np.mean([losses[i] for i in epoch_indices]) + epoch_losses.append(epoch_loss) + + ax2.plot(unique_epochs, epoch_losses, 'g-', label='Average Epoch Loss', linewidth=2) + ax2.set_xlabel('Epochs') + ax2.set_ylabel('Average Loss (negated for display)') + ax2.set_title('Average Loss per Epoch') + ax2.grid(True, alpha=0.3) + ax2.legend() + + plt.tight_layout() + + # Save plot if specified + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + print(f"Loss curve saved to: {save_path}") + + # Show plot if requested + if show_plot: + plt.show() + + plt.close() + +def plot_loss_statistics(log_file_path: str, + save_path: Optional[str] = None, + show_plot: bool = True, + figsize: Tuple[int, int] = (15, 10)) -> None: + """ + Plot detailed loss statistics including histograms and moving averages + + Args: + log_file_path: Path to the log file + save_path: Path to save the plot (optional) + show_plot: Whether to display the plot + figsize: Figure size (width, height) + """ + # Parse loss data + losses, epochs, steps = parse_loss_log(log_file_path) + val_losses, val_epochs, val_steps = parse_val_loss_log(log_file_path) + + if not losses: + print("No loss data found in log file") + return + + # Create figure with subplots + fig = plt.figure(figsize=figsize) + + # 1. Training loss over steps + ax1 = plt.subplot(2, 3, 1) + ax1.plot(steps, losses, 'b-', alpha=0.7, linewidth=0.8) + ax1.set_xlabel('Training Steps') + ax1.set_ylabel('Loss (negated for display)') + ax1.set_title('Training Loss') + ax1.grid(True, alpha=0.3) + + # 2. Moving average of training loss + ax2 = plt.subplot(2, 3, 2) + window_size = min(50, len(losses) // 10) # Adaptive window size + if window_size > 1: + moving_avg = np.convolve(losses, np.ones(window_size)/window_size, mode='valid') + steps_avg = steps[window_size-1:] + ax2.plot(steps_avg, moving_avg, 'r-', linewidth=1.5) + ax2.set_xlabel('Training Steps') + ax2.set_ylabel('Moving Average Loss') + ax2.set_title(f'Moving Average (window={window_size})') + ax2.grid(True, alpha=0.3) + + # 3. Loss histogram + ax3 = plt.subplot(2, 3, 3) + ax3.hist(losses, bins=30, alpha=0.7, color='blue', edgecolor='black') + ax3.set_xlabel('Loss Value') + ax3.set_ylabel('Frequency') + ax3.set_title('Loss Distribution') + ax3.grid(True, alpha=0.3) + + # 4. Loss vs epochs + ax4 = plt.subplot(2, 3, 4) + unique_epochs = list(set(epochs)) + epoch_losses = [] + for epoch in unique_epochs: + epoch_indices = [i for i, e in enumerate(epochs) if e == epoch] + epoch_loss = np.mean([losses[i] for i in epoch_indices]) + epoch_losses.append(epoch_loss) + + ax4.plot(unique_epochs, epoch_losses, 'g-', linewidth=2, marker='o') + ax4.set_xlabel('Epochs') + ax4.set_ylabel('Average Loss') + ax4.set_title('Average Loss per Epoch') + ax4.grid(True, alpha=0.3) + + # 5. Validation loss (if available) + ax5 = plt.subplot(2, 3, 5) + if val_losses: + ax5.plot(val_steps, val_losses, 'orange', linewidth=1.5, label='Validation Loss') + ax5.set_xlabel('Training Steps') + ax5.set_ylabel('Validation Loss') + ax5.set_title('Validation Loss') + ax5.grid(True, alpha=0.3) + ax5.legend() + else: + ax5.text(0.5, 0.5, 'No validation loss data', ha='center', va='center', transform=ax5.transAxes) + ax5.set_title('Validation Loss (No Data)') + + # 6. Loss statistics + ax6 = plt.subplot(2, 3, 6) + ax6.axis('off') + stats_text = f""" +Loss Statistics: +---------------- +Total Steps: {len(losses)} +Total Epochs: {max(epochs) if epochs else 0} +Min Loss: {min(losses):.6f} +Max Loss: {max(losses):.6f} +Mean Loss: {np.mean(losses):.6f} +Std Loss: {np.std(losses):.6f} +Final Loss: {losses[-1]:.6f} +""" + if val_losses: + stats_text += f""" +Validation Stats: +---------------- +Val Steps: {len(val_losses)} +Min Val Loss: {min(val_losses):.6f} +Max Val Loss: {max(val_losses):.6f} +Mean Val Loss: {np.mean(val_losses):.6f} +Final Val Loss: {val_losses[-1]:.6f} +""" + + ax6.text(0.05, 0.95, stats_text, transform=ax6.transAxes, + fontsize=10, verticalalignment='top', fontfamily='monospace') + + plt.tight_layout() + + # Save plot if specified + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + print(f"Loss statistics saved to: {save_path}") + + # Show plot if requested + if show_plot: + plt.show() + + plt.close() + +def load_training_history(history_file_path: str) -> Dict: + """ + Load training history from JSON file + + Args: + history_file_path: Path to the training history JSON file + + Returns: + Dictionary containing training history + """ + if not os.path.exists(history_file_path): + print(f"History file not found: {history_file_path}") + return {} + + with open(history_file_path, 'r') as f: + history = json.load(f) + + return history + +def plot_training_history(history_file_path: str, + save_path: Optional[str] = None, + show_plot: bool = True) -> None: + """ + Plot training history from JSON file + + Args: + history_file_path: Path to the training history JSON file + save_path: Path to save the plot (optional) + show_plot: Whether to display the plot + """ + history = load_training_history(history_file_path) + + if not history: + print("No training history data found") + return + + fig, axes = plt.subplots(1, len(history), figsize=(5*len(history), 4)) + if len(history) == 1: + axes = [axes] + + for i, (key, values) in enumerate(history.items()): + axes[i].plot(values, 'b-', linewidth=1.5) + axes[i].set_xlabel('Epochs') + axes[i].set_ylabel(key.replace('_', ' ').title()) + axes[i].set_title(f'{key.replace("_", " ").title()} vs Epochs') + axes[i].grid(True, alpha=0.3) + + plt.tight_layout() + + # Save plot if specified + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + print(f"Training history plot saved to: {save_path}") + + # Show plot if requested + if show_plot: + plt.show() + + plt.close() + +def main(): + """Example usage of utility functions""" + print("DeepONet-Grid-UQ Utility Functions") + print("=" * 40) + + # Example usage + log_file = "outputs/training.log" + + if os.path.exists(log_file): + print(f"Found log file: {log_file}") + + # Parse and print basic statistics + losses, epochs, steps = parse_loss_log(log_file) + if losses: + print(f"Loss statistics:") + print(f" Total entries: {len(losses)}") + print(f" Min loss: {min(losses):.6f}") + print(f" Max loss: {max(losses):.6f}") + print(f" Mean loss: {np.mean(losses):.6f}") + print(f" Final loss: {losses[-1]:.6f}") + + # Plot loss curves + plot_loss_curves(log_file, save_path="loss_curves.png") + plot_loss_statistics(log_file, save_path="loss_statistics.png") + + # Check for training history + history_file = "outputs/training_history.json" + if os.path.exists(history_file): + print(f"Found training history: {history_file}") + plot_training_history(history_file, save_path="training_history.png") + + print("Utility functions completed!") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/MindEnergy/application/deeponet-grid/test_system.py b/MindEnergy/application/deeponet-grid/test_system.py new file mode 100644 index 000000000..a717da7d6 --- /dev/null +++ b/MindEnergy/application/deeponet-grid/test_system.py @@ -0,0 +1,550 @@ +#!/usr/bin/env python3 +""" +System test script for DeepONet-Grid-UQ +测试系统各个组件是否正常工作 +""" + +import os +import sys +import yaml +import numpy as np +import mindspore as ms +from mindspore import context + +# Add src to path +sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) + +from src.model import Prob_DeepONet +from src.data import generate_synthetic_data, normalize_data_mindspore, split_data, create_mindspore_datasets, load_real_data, prepare_deeponet_data, trajectory_prediction, batch_trajectory_prediction +from src.trainer import create_trainer +from src.metrics import compute_metrics, update_metrics_history, test, test_one, MetricsCalculator + +def test_config_loading(): + """Test configuration loading""" + print("Testing configuration loading...") + + config_path = 'configs/config.yaml' + if os.path.exists(config_path): + with open(config_path, 'r') as f: + config = yaml.safe_load(f) + + print(f" Configuration loaded successfully") + print(f" Model config: {config.get('model', 'Not found')}") + print(f" Training config: {config.get('training', 'Not found')}") + print("Correct: Configuration loading test passed") + else: + print(f"✗ Configuration file not found: {config_path}") + return False + + return True + +def test_data_generation(): + """Test data generation functionality""" + print("Testing data generation...") + + # Generate synthetic data + u, y, s = generate_synthetic_data(n_samples=100, n_sensors=50, n_points=1, seed=42) + + print(f" Generated data shapes: u={u.shape}, y={y.shape}, s={s.shape}") + + # Test data splitting + data_splits = split_data(u, y, s, train_split=0.8, val_split=0.1, test_split=0.1) + print(f" Data splits: {list(data_splits.keys())}") + + # Test MindSpore dataset creation + datasets = create_mindspore_datasets(data_splits, batch_size=16) + print(f" Created datasets: {list(datasets.keys())}") + + print("Correct: Data generation test passed") + return datasets + +def test_data_normalization(): + # use test_data_generation to generate data + + # Generate synthetic data + u, y, s = generate_synthetic_data(n_samples=100, n_sensors=50, n_points=1, seed=42) + + print(f" Generated data shapes: u={u.shape}, y={y.shape}, s={s.shape}") + + # normalize data + + # try different normalization methods + u_norm, y_norm, s_norm, scalers = normalize_data_mindspore(u, y, s, method='standard') + print(f" Normalized data shapes: u={u_norm.shape}, y={y_norm.shape}, s={s_norm.shape}") + + u_norm, y_norm, s_norm, scalers = normalize_data_mindspore(u, y, s, method='minmax') + print(f" Normalized data shapes: u={u_norm.shape}, y={y_norm.shape}, s={s_norm.shape}") + + print("Correct: Data normalization test passed") + +def test_load_real_data(): + """Test real data loading functionality""" + print("Testing real data loading...") + + # Check for common data file locations + possible_data_paths = [ + "/Users/congwang/Documents/codes/M_deeponetgrid/deeponet_ms/data/grid_real/data/trustworthydatasetv1/test-data-voltage-m-33-mix.npz", + "/Users/congwang/Documents/codes/M_deeponetgrid/deeponet_ms/data/grid_real/data/trustworthydatasetv1/train-data-voltage-m-33-Q-100-mix.npz" + ] + + data_found = False + for data_path in possible_data_paths: + if os.path.exists(data_path): + print(f" Found data file: {data_path}") + try: + # Load data + u, y, s = load_real_data(data_path) + print(f" Loaded data shapes: u={u.shape}, y={y.shape}, s={s.shape}") + + # Test data properties + print(f" Data types: u={u.dtype}, y={y.dtype}, s={s.dtype}") + print(f" Data ranges: u=[{u.min():.3f}, {u.max():.3f}], y=[{y.min():.3f}, {y.max():.3f}], s=[{s.min():.3f}, {s.max():.3f}]") + + # Test data splitting + data_splits = split_data(u, y, s, train_split=0.8, val_split=0.1, test_split=0.1) + print(f" Data splits: {list(data_splits.keys())}") + + # Test MindSpore dataset creation + datasets = create_mindspore_datasets(data_splits, batch_size=16) + print(f" Created datasets: {list(datasets.keys())}") + + data_found = True + print("Correct: Real data loading test passed") + # return datasets + + except Exception as e: + print(f" Error loading {data_path}: {e}") + continue + + if not data_found: + print(" No data files found in common locations") + print(" Creating synthetic data for testing...") + + # Create synthetic data as fallback + u, y, s = generate_synthetic_data(n_samples=100, n_sensors=50, n_points=1, seed=42) + data_splits = split_data(u, y, s, train_split=0.8, val_split=0.1, test_split=0.1) + datasets = create_mindspore_datasets(data_splits, batch_size=16) + + print("Correct: Real data loading test passed (using synthetic data)") + return datasets + +def test_trajectory_prediction(): + """Test trajectory prediction functionality""" + print("Testing trajectory prediction...") + + # Load real data + data_path = "/Users/congwang/Documents/codes/M_deeponetgrid/deeponet_ms/data/grid_real/data/trustworthydatasetv1/test-data-voltage-m-33-mix.npz" + u, y, s = load_real_data(data_path) + + # Prepare data + u_expanded, y_expanded, s_expanded, metadata = prepare_deeponet_data(u, y, s) + + print(f" Prepared data shapes: u={u_expanded.shape}, y={y_expanded.shape}, s={s_expanded.shape}") + print(f" Metadata: {metadata}") + + # Test trajectory_prediction function + print(" Testing trajectory_prediction function...") + + # Create a simple model for testing (if available) + try: + from src.model import Prob_DeepONet + + # Create a simple model + branch_config = { + "type": "modified", + "layer_size": [33, 100, 100, 50], # Smaller for testing + "activation": "sin" + } + + trunk_config = { + "type": "modified", + "layer_size": [1, 100, 100, 50], # Smaller for testing + "activation": "sin" + } + + model = Prob_DeepONet(branch_config, trunk_config, use_bias=True) + + # Test single sample trajectory prediction + u_single = u[0] # First sample, shape (33,) + time_points = np.array([0.0, 0.5, 1.0, 1.5, 2.0]) + + predictions = trajectory_prediction(u_single, time_points, model) + print(f" Single sample prediction shape: {predictions.shape}") + print(f" Predictions: {predictions.flatten()}") + + # Test batch trajectory prediction + u_batch = u[:3] # First 3 samples, shape (3, 33) + batch_predictions = batch_trajectory_prediction(u_batch, time_points, model) + print(f" Batch prediction shape: {batch_predictions.shape}") + + print(" Correct: Trajectory prediction functions work correctly") + + except ImportError: + print(" Warning: Model not available, skipping trajectory prediction test") + except Exception as e: + print(f" Warning: Trajectory prediction test failed: {e}") + + print("Correct: Trajectory prediction test passed") + +def test_model_creation(): + """Test model creation functionality""" + print("Testing model creation...") + + # Model configuration based on real data (following original author's formula) + # depth = 3 means 3 hidden layers: [input] + [width] * depth + [output] + branch_config = { + "type": "modified", + "layer_size": [33, 200, 200, 200, 100], # [m] + [width] * depth + [n_basis] + "activation": "sin" + } + + trunk_config = { + "type": "modified", + "layer_size": [1, 200, 200, 200, 100], # [dim] + [width] * depth + [n_basis] + "activation": "sin" + } + + # Create model + model = Prob_DeepONet(branch_config, trunk_config, use_bias=True) + print(f" Model created successfully") + + # Test forward pass - single query point + print("\n Testing single query point prediction:") + u = ms.Tensor(np.random.randn(10, 33), ms.float32) # 10 samples, 33 sensors + y = ms.Tensor(np.random.randn(10, 1), ms.float32) # 10 samples, 1 query point each + + mean_pred, log_std_pred = model(u, y) + print(f" Input shapes: u={u.shape}, y={y.shape}") + print(f" Output shapes: mean={mean_pred.shape}, log_std={log_std_pred.shape}") + print(f" Correct: DeepONet outputs scalar values for each query point") + + # Test forward pass - demonstrate the limitation + print("\n Testing multiple query points (this will fail as expected):") + u = ms.Tensor(np.random.randn(10, 33), ms.float32) # 10 samples, 33 sensors + y = ms.Tensor(np.random.randn(10, 100), ms.float32) # 10 samples, 100 query points each + + try: + mean_pred, log_std_pred = model(u, y) + print(f" Warning: Unexpected success! This should have failed.") + except Exception as e: + print(f" Correct: Expected error: {str(e)[:100]}...") + print(f" Correct: This demonstrates that DeepONet expects single query points") + + # Demonstrate how to get full trajectory + print("\n Demonstrating full trajectory prediction:") + print(f" To get 100 time points, you need 100 forward passes:") + print(f" - For each time point t in [0, 1, 2, ..., 99]:") + print(f" y_t = [[t]] # Single query point") + print(f" pred_t = model(u, y_t) # Forward pass") + print(f" - Collect all pred_t to get trajectory") + + # Show a working example + print("\n Working example - predict 5 time points:") + u_single = ms.Tensor(np.random.randn(1, 33), ms.float32) # 1 sample, 33 sensors + time_points = [0.0, 0.5, 1.0, 1.5, 2.0] + + predictions = [] + for t in time_points: + y_t = ms.Tensor([[t]], ms.float32) # Single query point + mean_pred, log_std_pred = model(u_single, y_t) + pred_val = float(mean_pred[0, 0]) + predictions.append(pred_val) + print(f" Time {t}: prediction = {pred_val:.4f}") + + print(f" Correct: Successfully predicted trajectory: {len(predictions)} points") + + print("Correct: Model creation test passed") + return model + +def test_trainer_creation(): + """Test trainer creation functionality""" + print("Testing trainer creation...") + + # Load config + config_path = 'configs/config.yaml' + if os.path.exists(config_path): + with open(config_path, 'r') as f: + config = yaml.safe_load(f) + else: + # Create minimal config for testing based on original notebook + config = { + 'model': { + 'm': 50, + 'dim': 1, + 'width': 200, + 'depth': 3, + 'n_basis': 100, + 'branch_type': 'modified', + 'trunk_type': 'modified', + 'activation': 'sin', + 'use_bias': True + }, + 'training': { + 'learning_rate': 0.00005, # 5e-5 + 'batch_size': 16, + 'epochs': 10, + 'print_every': 5, + 'eval_every': 5, + 'patience': 10, + 'factor': 0.8, + 'verbose': True, + 'loss_type': 'nll', + 'optimizer': 'adam', + 'weight_decay': 0.0 + }, + 'device': { + 'target': 'CPU', + 'mode': 'PYNATIVE_MODE' + }, + 'output': { + 'save_dir': 'test_outputs', + 'save_best': True + } + } + + # Create model + model = test_model_creation() + + # Create trainer + trainer = create_trainer(model, config, save_dir='test_outputs') + print(f" Trainer created successfully") + + print("Correct: Trainer creation test passed") + return trainer, config + +def test_training_workflow(): + """Test complete training workflow""" + print("Testing training workflow...") + + # Set up device context + context.set_context(mode=context.PYNATIVE_MODE, device_target="CPU") + + # Load data using approach from test_real_data_loading but with only 20 samples + print(" Loading data for training test...") + + # Check for common data file locations + data_paths = [ + "/Users/congwang/Documents/codes/M_deeponetgrid/deeponet_ms/data/grid_real/data/trustworthydatasetv1/train-data-voltage-m-33-Q-100-mix.npz" + ] + + data_found = False + for data_path in data_paths: + if os.path.exists(data_path): + print(f" Found data file: {data_path}") + try: + # Load data + u, y, s = load_real_data(data_path) + print(f" Loaded data shapes: u={u.shape}, y={y.shape}, s={s.shape}") + + # Take only 10 samples for testing + u = u[:10] + y = y[:10] + s = s[:10] + print(f" Using 20 samples: u={u.shape}, y={y.shape}, s={s.shape}") + + # Prepare data for DeepONet (convert multi-time-point to single query points) + print(" Preparing data for DeepONet...") + u_expanded, y_expanded, s_expanded, metadata = prepare_deeponet_data(u, y, s) + print(f" Prepared data: u={u_expanded.shape}, y={y_expanded.shape}, s={s_expanded.shape}") + + # data normalization + # u_expanded, y_expanded, s_expanded, scalers = normalize_data_mindspore(u_expanded, y_expanded, s_expanded, method='standard') + + # Test data splitting + data_splits = split_data(u_expanded, y_expanded, s_expanded, train_split=0.8, val_split=0.1, test_split=0.1) + print(f" Data splits: {list(data_splits.keys())}") + + # Test MindSpore dataset creation + datasets = create_mindspore_datasets(data_splits, batch_size=8) # Smaller batch size for 20 samples + print(f" Created datasets: {list(datasets.keys())}") + + data_found = True + break + + except Exception as e: + print(f" Error loading {data_path}: {e}") + continue + + if not data_found: + print(" No data files found in common locations") + print(" Creating synthetic data for testing...") + + # Create synthetic data as fallback with only 20 samples + u, y, s = generate_synthetic_data(n_samples=20, n_sensors=33, n_points=1, seed=42) + data_splits = split_data(u, y, s, train_split=0.8, val_split=0.1, test_split=0.1) + datasets = create_mindspore_datasets(data_splits, batch_size=8) + + # Create trainer using the same approach as test_trainer_creation() + print(" Creating trainer...") + + # Load config + config_path = 'configs/config.yaml' + if os.path.exists(config_path): + with open(config_path, 'r') as f: + config = yaml.safe_load(f) + else: + # Create minimal config for testing based on original notebook + config = { + 'model': { + 'm': 33, # Match the data dimension + 'dim': 1, + 'width': 200, + 'depth': 3, + 'n_basis': 100, + 'branch_type': 'modified', + 'trunk_type': 'modified', + 'activation': 'sin', + 'use_bias': True + }, + 'training': { + 'learning_rate': 0.00005, # 5e-5 + 'batch_size': 8, # Smaller batch size for testing + 'epochs': 5, # Fewer epochs for testing + 'print_every': 1, + 'eval_every': 1, + 'patience': 10, + 'factor': 0.8, + 'verbose': True, + 'loss_type': 'nll', + 'optimizer': 'adam', + 'weight_decay': 0.0 + }, + 'device': { + 'target': 'CPU', + 'mode': 'PYNATIVE_MODE' + }, + 'output': { + 'save_dir': 'test_outputs', + 'save_best': True + } + } + + # Create model + branch_config = { + "type": "modified", + "layer_size": [33, 200, 200, 200, 100], # [m] + [width] * depth + [n_basis] + "activation": "sin" + } + + trunk_config = { + "type": "modified", + "layer_size": [1, 200, 200, 200, 100], # [dim] + [width] * depth + [n_basis] + "activation": "sin" + } + + model = Prob_DeepONet(branch_config, trunk_config, use_bias=True) + + # Create trainer + trainer = create_trainer(model, config, save_dir='test_outputs') + print(f" Trainer created successfully") + + # Run a few training steps + print(" Running training steps...") + try: + # Get one batch from training dataset + for u, y, target in datasets['train']: + print(f"u shape: {u.shape}, y shape: {y.shape}, target shape: {target.shape}") + loss, mean_pred, log_std_pred = trainer.train_step(u, y, target) + print(f" Training step completed, loss: {float(loss):.6f}") + print(f" Prediction shapes: mean={mean_pred.shape}, log_std={log_std_pred.shape}") + print(f" Target shape: {target.shape}") + break + + # Test validation + val_loss = trainer.validate(datasets['val']) + print(f" Validation loss: {val_loss:.6f}") + + print("Correct: Training workflow test passed") + + except Exception as e: + print(f"✗ Training workflow test failed: {e}") + import traceback + traceback.print_exc() + return False + + return True + +def test_metrics(): + """Test metrics functionality""" + print("Testing metrics...") + + # Create test data + y_true = ms.Tensor(np.random.randn(100, 1), ms.float32) + y_pred = ms.Tensor(np.random.randn(100, 1), ms.float32) + s_std = ms.Tensor(np.abs(np.random.randn(100, 1)), ms.float32) + + # Test MetricsCalculator + calculator = MetricsCalculator() + + # Test L1 and L2 relative errors + l1_error = calculator.l1_relative_error(y_true, y_pred) + l2_error = calculator.l2_relative_error(y_true, y_pred) + print(f" L1 relative error: {l1_error:.6f}") + print(f" L2 relative error: {l2_error:.6f}") + + # Test fraction in confidence interval + ci_fraction = calculator.fraction_in_CI(y_true, y_pred, s_std, xi=2.0) + print(f" Fraction in CI: {ci_fraction:.6f}") + + # Test trajectory relative error + l1_traj, l2_traj = calculator.trajectory_rel_error(y_true, y_pred) + print(f" Trajectory L1: {l1_traj:.6f}, L2: {l2_traj:.6f}") + + # Test compute_metrics for multiple trajectories + s_true_list = [y_true, y_true * 0.5] + s_pred_list = [y_pred, y_pred * 0.5] + metrics_result = compute_metrics(s_true_list, s_pred_list, ['l1', 'l2'], verbose=True) + print(f" Metrics result: {metrics_result}") + + # Test update_metrics_history + history = {} + history = update_metrics_history(history, metrics_result[0]) + print(f" Updated history: {history}") + + print("Correct: Metrics test passed") + return True + +def main(): + """Run all tests""" + print("=" * 50) + print("DeepONet-Grid-UQ System Test") + print("=" * 50) + + tests = [ + ("Configuration Loading", test_config_loading), # passed + ("Data Generation", test_data_generation), # passed + # TODO: test data normalization + # ("Data Normalization", test_data_normalization), + ("Load Real Data", test_load_real_data), # passed + ("Trajectory Prediction", test_trajectory_prediction), # passed + ("Model Creation", test_model_creation), # passed + ("Trainer Creation", test_trainer_creation), # passed + ("Training Workflow", test_training_workflow), # passed + ("Metrics", test_metrics), # passed + ] + + passed = 0 + total = len(tests) + + for test_name, test_func in tests: + print(f"\n{test_name}:") + print("-" * 30) + try: + result = test_func() + if result is not False: + passed += 1 + except Exception as e: + print(f"✗ {test_name} failed with error: {e}") + + print("\n" + "=" * 50) + print(f"Test Results: {passed}/{total} tests passed") + + if passed == total: + print("All tests passed! System is ready to use.") + else: + print("Warning: Some tests failed. Please check the errors above.") + + print("=" * 50) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/MindEnergy/application/deeponet-grid/train.py b/MindEnergy/application/deeponet-grid/train.py new file mode 100644 index 000000000..6f28b0091 --- /dev/null +++ b/MindEnergy/application/deeponet-grid/train.py @@ -0,0 +1,325 @@ +#!/usr/bin/env python3 +""" +Main training script for DeepONet-Grid-UQ +基于MindSpore框架的DeepONet训练主程序 +""" + +import os +import sys +import yaml +import argparse +import logging +from pathlib import Path + +import mindspore as ms +from mindspore import context + +# Add src to path +sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) + +from src.model import Prob_DeepONet +from src.data import load_real_data, prepare_deeponet_data, split_data, create_mindspore_datasets +from src.trainer import create_trainer + +# Set up logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('training.log'), + logging.StreamHandler(sys.stdout) + ] +) +logger = logging.getLogger(__name__) + +def load_config(config_path: str) -> dict: + """Load configuration from YAML file + + Args: + config_path: Path to configuration file + + Returns: + Configuration dictionary + """ + with open(config_path, 'r', encoding='utf-8') as f: + config = yaml.safe_load(f) + return config + +def setup_device(config: dict): + """Set up MindSpore device context + + Args: + config: Configuration dictionary + """ + device_config = config['device'] + + # Set context + context.set_context( + mode=getattr(context, device_config['mode']), + device_target=device_config['target'] + ) + + logger.info(f"Device set to: {device_config['target']}") + logger.info(f"Mode set to: {device_config['mode']}") + +def find_training_data(): + """Find training data file""" + possible_data_paths = [ + "data/train-data-voltage-m-33-Q-100-mix.npz", + ] + + for data_path in possible_data_paths: + if os.path.exists(data_path): + logger.info(f"Found training data: {data_path}") + return data_path + + raise FileNotFoundError("No training data file found. Please check the data paths.") + +def load_and_preprocess_real_data(config: dict): + """Load and preprocess real training data + + Args: + config: Configuration dictionary + + Returns: + Tuple of (datasets, metadata) + """ + # Find training data + data_path = find_training_data() if config['data']['data_path'] is None else config['data']['data_path'] + + # Load raw data + logger.info(f"Loading data from: {data_path}") + u, y, s = load_real_data(data_path) + logger.info(f"Original data shapes: u={u.shape}, y={y.shape}, s={y.shape}") + + # Prepare data for DeepONet (convert multi-time-point to single query points) + logger.info("Preparing data for DeepONet...") + u_expanded, y_expanded, s_expanded, prep_metadata = prepare_deeponet_data(u, y, s) + logger.info(f"Prepared data shapes: u={u_expanded.shape}, y={y_expanded.shape}, s={s_expanded.shape}") + + # Normalize data + # logger.info("Normalizing data...") + # u_norm, y_norm, s_norm, scalers = normalize_data_mindspore(u_expanded, y_expanded, s_expanded, method='standard') + + # Split data + logger.info("Splitting data...") + data_splits = split_data( + u_expanded, y_expanded, s_expanded, + train_split=config['data']['train_split'], + val_split=config['data']['val_split'], + test_split=config['data']['test_split'] + ) + + # Create MindSpore datasets + logger.info("Creating MindSpore datasets...") + datasets = create_mindspore_datasets( + data_splits, + batch_size=config['training']['batch_size'] + ) + + # Prepare metadata + metadata = { + 'input_dim': u_expanded.shape[-1], + 'output_dim': s_expanded.shape[-1], + 'n_samples': len(u_expanded), + 'n_sensors': u.shape[-1], # Original sensor count + 'data_shapes': { + 'u': u_expanded.shape, + 'y': y_expanded.shape, + 's': s_expanded.shape + } + } + + # Add preparation metadata + if prep_metadata: + metadata.update(prep_metadata) + + return datasets, metadata + +def create_model(config: dict, metadata: dict) -> Prob_DeepONet: + """Create DeepONet model based on configuration and data metadata + + Args: + config: Configuration dictionary + metadata: Data metadata containing input/output dimensions + + Returns: + DeepONet model instance + """ + model_config = config['model'] + + # Get network parameters from metadata if available, otherwise use config defaults + m = metadata.get('n_sensors', model_config.get('m', 33)) # Number of sensors + dim = model_config.get('dim', 1) # Input dimension for trunk (always 1 for DeepONet) + width = model_config.get('width', 200) # Network width + depth = model_config.get('depth', 3) # Network depth (number of hidden layers) + n_basis = model_config.get('n_basis', 100) # Number of basis functions + + # Get network types + branch_type = model_config.get('branch_type', 'modified') + trunk_type = model_config.get('trunk_type', 'modified') + activation = model_config.get('activation', 'sin') + + # Compute layer sizes automatically (following original author's formula) + # [input] + [width] * depth + [output] + # depth = 3 means 3 hidden layers + branch_layer_size = [m] + [width] * depth + [n_basis] + trunk_layer_size = [dim] + [width] * depth + [n_basis] + + # Create branch configuration + branch_config = { + 'type': branch_type, + 'layer_size': branch_layer_size, + 'activation': activation + } + + # Create trunk configuration + trunk_config = { + 'type': trunk_type, + 'layer_size': trunk_layer_size, + 'activation': activation + } + + logger.info(f"Branch network: {branch_layer_size}") + logger.info(f"Trunk network: {trunk_layer_size}") + logger.info(f"Branch type: {branch_type}, Trunk type: {trunk_type}") + logger.info(f"Activation: {activation}") + logger.info(f"Depth: {depth} (number of hidden layers)") + logger.info(f"Input sensors (m): {m}") + logger.info(f"Trunk input dimension (dim): {dim}") + + # Create model + model = Prob_DeepONet( + branch=branch_config, + trunk=trunk_config, + use_bias=model_config['use_bias'] + ) + + logger.info("Model created successfully") + return model + +def train(): + """Main training function""" + parser = argparse.ArgumentParser(description='Train DeepONet-Grid-UQ model') + parser.add_argument('--config', type=str, default='configs/config.yaml', + help='Path to configuration file') + parser.add_argument('--data_path', type=str, default=None, + help='Path to data file (overrides config)') + parser.add_argument('--output_dir', type=str, default=None, + help='Output directory (overrides config)') + parser.add_argument('--epochs', type=int, default=10, + help='Number of epochs (default: 10)') + parser.add_argument('--batch_size', type=int, default=None, + help='Batch size (overrides config)') + parser.add_argument('--learning_rate', type=float, default=None, + help='Learning rate (overrides config)') + parser.add_argument('--resume', type=str, default=None, + help='Resume training from checkpoint') + parser.add_argument('--test_only', action='store_true', + help='Only run evaluation on test set') + + args = parser.parse_args() + + # Load configuration + logger.info(f"Loading configuration from {args.config}") + config = load_config(args.config) + + # Override config with command line arguments + if args.data_path: + config['data']['data_path'] = args.data_path + config['data']['use_synthetic'] = False + + if args.output_dir: + config['output']['save_dir'] = args.output_dir + + # Set epochs to 10 as requested + config['training']['epochs'] = args.epochs + + if args.batch_size: + config['training']['batch_size'] = args.batch_size + + if args.learning_rate: + config['training']['learning_rate'] = args.learning_rate + + # Set up device + setup_device(config) + + # Load and preprocess real data + logger.info("Loading and preprocessing real training data...") + datasets, metadata = load_and_preprocess_real_data(config) + + logger.info(f"Data loaded successfully:") + logger.info(f" Input dimension: {metadata['input_dim']}") + logger.info(f" Output dimension: {metadata['output_dim']}") + logger.info(f" Total samples: {metadata['n_samples']}") + logger.info(f" Number of sensors: {metadata['n_sensors']}") + + # Create model + logger.info("Creating model...") + model = create_model(config, metadata) + + # Create trainer + logger.info("Creating trainer...") + trainer = create_trainer( + model=model, + config=config, + save_dir=config['output']['save_dir'] + ) + + # Load checkpoint if resuming + if args.resume: + logger.info(f"Resuming from checkpoint: {args.resume}") + trainer.load_model(args.resume) + + if args.test_only: + # Only run evaluation + logger.info("Running evaluation only...") + metrics = trainer.evaluate(datasets['test']) + + # Save evaluation results + import json + results_path = os.path.join(config['output']['save_dir'], 'test_results.json') + with open(results_path, 'w') as f: + json.dump(metrics, f, indent=2) + logger.info(f"Test results saved to: {results_path}") + + else: + # print config before training + logger.info(f"Training config: {config}") + + # Train model + logger.info(f"Starting training for {config['training']['epochs']} epochs...") + history = trainer.train( + train_dataset=datasets['train'], + val_dataset=datasets['val'] + ) + + # Save training history + import json + history_path = os.path.join(config['output']['save_dir'], 'training_history.json') + with open(history_path, 'w') as f: + json.dump(history, f, indent=2) + logger.info(f"Training history saved to: {history_path}") + + # Evaluate on test set + logger.info("Evaluating on test set...") + test_metrics = trainer.evaluate(datasets['test']) + + # Save test results + test_results_path = os.path.join(config['output']['save_dir'], 'test_results.json') + with open(test_results_path, 'w') as f: + json.dump(test_metrics, f, indent=2) + logger.info(f"Test results saved to: {test_results_path}") + + # Save final model + trainer.save_model('final_model.ckpt') + + logger.info("Training completed successfully!") + + # Print final results + logger.info("Final Results:") + for metric, value in test_metrics.items(): + logger.info(f" {metric.upper()}: {value:.6f}") + +if __name__ == "__main__": + train() \ No newline at end of file -- Gitee From 0ac51f52f9e99eed79f7a4722be1cd4207318902 Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Thu, 10 Jul 2025 11:28:42 +0800 Subject: [PATCH 02/25] Edit document --- .../{deeonet-grid.md => README_en.md} | 0 .../application/deeponet-grid/issue_cn_api.md | 178 ------------------ .../deeponet-grid/issue_cn_application.md | 161 ---------------- .../deeponet-grid/requirements.txt | 3 - 4 files changed, 342 deletions(-) rename MindEnergy/application/deeponet-grid/{deeonet-grid.md => README_en.md} (100%) delete mode 100644 MindEnergy/application/deeponet-grid/issue_cn_api.md delete mode 100644 MindEnergy/application/deeponet-grid/issue_cn_application.md diff --git a/MindEnergy/application/deeponet-grid/deeonet-grid.md b/MindEnergy/application/deeponet-grid/README_en.md similarity index 100% rename from MindEnergy/application/deeponet-grid/deeonet-grid.md rename to MindEnergy/application/deeponet-grid/README_en.md diff --git a/MindEnergy/application/deeponet-grid/issue_cn_api.md b/MindEnergy/application/deeponet-grid/issue_cn_api.md deleted file mode 100644 index ec7e51803..000000000 --- a/MindEnergy/application/deeponet-grid/issue_cn_api.md +++ /dev/null @@ -1,178 +0,0 @@ -# DeepOnet-Grid - -表1-1: 设计的特性列表 - -| ISSUE编号 | ISSUE标题 | 特性等级 | 支持后端 | CANN版本 | 支持模式 | 支持平台 | MindEnergy规划版本 | MindSpore支持版本 | -|------|------|------|------|------|------|----| ------|------| -| | 【MindEnergy】DeepOnet-Grid | | Atlas 800T A2 | 8.1.RC1.beta1 | Pynative/Graph | Linux | 0.1.0 | >=2.5.0 | - -## 案例特性概述 - -### 需求来源及价值概述 - -本工作构建了一个高效的网络DeepONet-Grid,用于对故障后的电力系统进行动态安全分析,该网络 - - (i) 接收故障前和故障期间收集的轨迹作为输入,并且 - (ii) 输出预测的故障后轨迹。 - -此外,本网络还通过不确定性量化(Uncertainty Quantification)为其方法赋予了在效率与可靠/可信预测之间取得平衡的能力。 - -原始论文:[DeepONet-grid-UQ: A trustworthy deep operator framework for predicting the power grid's post-fault trajectories](!https://www.sciencedirect.com/science/article/abs/pii/S0925231223002503) - -原始代码仓:[Github Link](!https://github.com/cmoyacal/DeepONet-Grid-UQ) - - - -### 研究背景与动机 - -电力系统作为关键基础设施,其稳定性和可靠性对现代社会至关重要。然而,电网经常面临罕见但严重的故障和扰动,这些事件可能导致系统不稳定,甚至引发大规模停电。 - -传统的动态安全分析需要求解复杂的非线性微分代数方程组,计算成本极高,难以实现实时分析。随着电网的转型,电力公司迫切需要能够进行近实时的动态安全评估。 - -现有的机器学习方法主要关注二分类问题(稳定/不稳定),缺乏对故障后轨迹的定量预测能力。系统运营商和规划者需要了解故障后各种状态变量的轨迹,以评估电压或频率是否会违反预定义限制并触发负荷切除等保护措施。 - -### 方法细节 - -#### 工作原理 - -DeepONet-Grid-UQ基于深度算子网络(Deep Operator Network)理论,其核心思想是将电力系统故障后的动态行为建模为一个算子映射问题: - -- **输入函数空间**:故障前和故障期间的轨迹数据 u(t) ∈ U -- **输出函数空间**:故障后预测轨迹 y(t) ∈ Y -- **算子映射**:G: U → Y - -该网络学习从输入轨迹到输出轨迹的非线性映射关系,即: -``` -y(t) = G[u](t) -``` - -#### 网络架构设计 - -图1: 网络结构图 -![网络架构图](images/arch.png) - -DeepONet采用分支-主干(Branch-Trunk)架构: - -**分支网络(Branch Network)**: -- 输入:故障前和故障期间的轨迹序列 u(t₁), u(t₂), ..., u(tₙ) -- 功能:提取输入轨迹的特征表示 -- 输出:特征向量 b ∈ ℝᵖ - -**主干网络(Trunk Network)**: -- 输入:时间点 t -- 功能:学习时间基函数 -- 输出:时间特征向量 τ(t) ∈ ℝᵖ - -**输出计算**: -``` -G[u](t) = ⟨b, τ(t)⟩ = Σᵢ₌₁ᵖ bᵢτᵢ(t) -``` - -其中 ⟨·,·⟩ 表示内积运算。 - -#### 不确定性量化方法 - -原始工作介绍了两种不确定性量化方法: - -**1. 贝叶斯DeepONet (B-DeepONet)**: -- 使用随机梯度哈密顿蒙特卡洛(SGHMC)采样 -- 从网络参数的后验分布中采样:θ ∼ p(θ|D) -- 预测不确定性通过多次前向传播获得: - ``` - y(t) = ∫ G[u](t; θ) p(θ|D) dθ - ``` - -**2. 概率DeepONet (Prob-DeepONet)**: -- 网络同时输出预测均值 μ(t) 和预测标准差 σ(t) -- 假设输出服从高斯分布:y(t) ∼ N(μ(t), σ²(t)) -- 损失函数采用负对数似然: - ``` - L = -log p(y|μ, σ) = -log N(y; μ, σ²) - ``` - -#### 训练策略 - -**概率训练过程**: -1. **数据预处理**:将轨迹数据分割为训练/验证集 -2. **前向传播**:计算预测均值和标准差 -3. **损失计算**:使用负对数似然损失 -4. **反向传播**:更新网络参数 -5. **不确定性评估**:在测试集上评估预测不确定性 - - -### 优势与贡献 - -- **贡献**:首次将深度算子网络应用于电力系统故障分析,建立了从输入轨迹到输出轨迹的映射关系; 在DeepONet框架中引入不确定性量化,提供了预测置信度评估; 直接从原始轨迹数据学习,无需复杂的特征工程。 - -- **优势**:相比传统数值方法,推理速度提升; 能够处理未见过的故障场景,具有良好的泛化性能; 极短的响应时间(t=2.2s),可满足电力系统实时控制需求。 - -### 应用场景 - -#### 电力系统故障分析 - -**输入数据**: -- 故障前:发电机转速、电压幅值、相角等状态变量 -- 故障期间:故障类型、故障位置、故障持续时间 - -**输出预测**: -- 故障后:系统稳定性、发电机同步性、电压恢复特性 -- 不确定性:预测置信区间、风险评估 - - - -## 特性影响分析 -`DeepOnet-Grid`网络不影响其他接口。 - -## 设计方案 - -### 详细设计 -根据原始`DeepOnet-Grid`网络设计接口。 - -### 可靠性/可用性 - -#### 异常情况: -- 当输入不是两个时,报错; -- 第一个输入不是2维tensor时,报错。 -- 第二个输入不是2维tensor且列数不为1时,报错。 - -### 对外接口 - - -#### 接口说明 - -|序号|基本项|内容| -|----|----|----| -| 1 |接口定义| Prob_DeepONet | -| 2 |接口描述| 概率DeepONet网络接口| -| 3 |输入输出参数| branch, trunk, use_bias | -| 4 |方法| construct(xu, xy) -> (mu, std) | -| 5 |接口定义| B_DeepONet | -| 6 |接口描述| 贝叶斯DeepONet网络接口| -| 7 |输入输出参数| branch, trunk, use_bias | -| 8 |方法| construct(xu, xy) -> s| - - - -![UML图](images/uml.png) - - - -## 测试 - -### 测试用例设计(API接口必选) - -介绍相关API接口的配套测试用例。 - -#### UT用例 -|用例编号|用例类型|用例名称|测试对象|测试功能|测试条件|预期结果| -|----|----|----|----|----|----|----| -|1|UT|test_forward|网络前向功能|网络|固定输入和权重参数|前向结果与预期结果相同| -|2|UT|test_train|网络训练功能|网络|固定训练数据和权重参数,指定优化方法|loss收敛趋势与预期结果相同| - - - - - - - - diff --git a/MindEnergy/application/deeponet-grid/issue_cn_application.md b/MindEnergy/application/deeponet-grid/issue_cn_application.md deleted file mode 100644 index c482c2651..000000000 --- a/MindEnergy/application/deeponet-grid/issue_cn_application.md +++ /dev/null @@ -1,161 +0,0 @@ -# DeepOnet-Grid - -表1-1: 设计的特性列表 - -| ISSUE编号 | ISSUE标题 | 特性等级 | 支持后端 | CANN版本 | 支持模式 | 支持平台 | MindEnergy规划版本 | MindSpore支持版本 | -|------|------|------|------|------|------|----| ------|------| -| | 【MindEnergy】DeepOnet-Grid Use Case | | Atlas 800T A2 | 8.1.RC1.beta1 | Pynative/Graph | Linux | 0.1.0 | >=2.5.0 | - -## 案例特性概述 - -### 需求来源及价值概述 - -本工作构建了一个高效的网络DeepONet-Grid,用于对故障后的电力系统进行动态安全分析,该网络 - - (i) 接收故障前和故障期间收集的轨迹作为输入,并且 - (ii) 输出预测的故障后轨迹。 - -此外,本网络还通过不确定性量化(Uncertainty Quantification)为其方法赋予了在效率与可靠/可信预测之间取得平衡的能力。 - -原始论文:[DeepONet-grid-UQ: A trustworthy deep operator framework for predicting the power grid's post-fault trajectories](!https://www.sciencedirect.com/science/article/abs/pii/S0925231223002503) - -原始代码仓:[Github Link](!https://github.com/cmoyacal/DeepONet-Grid-UQ) - - - -### 研究背景与动机 - -电力系统作为关键基础设施,其稳定性和可靠性对现代社会至关重要。然而,电网经常面临罕见但严重的故障和扰动,这些事件可能导致系统不稳定,甚至引发大规模停电。 - -传统的动态安全分析需要求解复杂的非线性微分代数方程组,计算成本极高,难以实现实时分析。随着电网的转型,电力公司迫切需要能够进行近实时的动态安全评估。 - -现有的机器学习方法主要关注二分类问题(稳定/不稳定),缺乏对故障后轨迹的定量预测能力。系统运营商和规划者需要了解故障后各种状态变量的轨迹,以评估电压或频率是否会违反预定义限制并触发负荷切除等保护措施。 - -### 方法细节 - -#### 工作原理 - -DeepONet-Grid-UQ基于深度算子网络(Deep Operator Network)理论,其核心思想是将电力系统故障后的动态行为建模为一个算子映射问题: - -- **输入函数空间**:故障前和故障期间的轨迹数据 u(t) ∈ U -- **输出函数空间**:故障后预测轨迹 y(t) ∈ Y -- **算子映射**:G: U → Y - -该网络学习从输入轨迹到输出轨迹的非线性映射关系,即: -``` -y(t) = G[u](t) -``` - -#### 网络架构设计 - -图1: 网络结构图 -![网络架构图](images/arch.png) - -DeepONet采用分支-主干(Branch-Trunk)架构: - -**分支网络(Branch Network)**: -- 输入:故障前和故障期间的轨迹序列 u(t₁), u(t₂), ..., u(tₙ) -- 功能:提取输入轨迹的特征表示 -- 输出:特征向量 b ∈ ℝᵖ - -**主干网络(Trunk Network)**: -- 输入:时间点 t -- 功能:学习时间基函数 -- 输出:时间特征向量 τ(t) ∈ ℝᵖ - -**输出计算**: -``` -G[u](t) = ⟨b, τ(t)⟩ = Σᵢ₌₁ᵖ bᵢτᵢ(t) -``` - -其中 ⟨·,·⟩ 表示内积运算。 - -#### 不确定性量化方法 - -原始工作介绍了两种不确定性量化方法,本工作只实现了概率DeepONet(Prob-DeepONet)不确定性量化方法: - -**1. 贝叶斯DeepONet (B-DeepONet)**: -- 使用随机梯度哈密顿蒙特卡洛(SGHMC)采样 -- 从网络参数的后验分布中采样:θ ∼ p(θ|D) -- 预测不确定性通过多次前向传播获得: - ``` - y(t) = ∫ G[u](t; θ) p(θ|D) dθ - ``` - -**2. 概率DeepONet (Prob-DeepONet)**: -- 网络同时输出预测均值 μ(t) 和预测标准差 σ(t) -- 假设输出服从高斯分布:y(t) ∼ N(μ(t), σ²(t)) -- 损失函数采用负对数似然: - ``` - L = -log p(y|μ, σ) = -log N(y; μ, σ²) - ``` - -#### 训练策略 - -**概率训练过程**: -1. **数据预处理**:将轨迹数据分割为训练/验证集 -2. **前向传播**:计算预测均值和标准差 -3. **损失计算**:使用负对数似然损失 -4. **反向传播**:更新网络参数 -5. **不确定性评估**:在测试集上评估预测不确定性 - - -### 优势与贡献 - -- **贡献**:首次将深度算子网络应用于电力系统故障分析,建立了从输入轨迹到输出轨迹的映射关系; 在DeepONet框架中引入不确定性量化,提供了预测置信度评估; 直接从原始轨迹数据学习,无需复杂的特征工程。 - -- **优势**:相比传统数值方法,推理速度提升; 能够处理未见过的故障场景,具有良好的泛化性能; 极短的响应时间(t=2.2s),可满足电力系统实时控制需求。 - -### 应用场景 - -#### 电力系统故障分析 - -**输入数据**: -- 故障前:发电机转速、电压幅值、相角等状态变量 -- 故障期间:故障类型、故障位置、故障持续时间 - -**输出预测**: -- 故障后:系统稳定性、发电机同步性、电压恢复特性 -- 不确定性:预测置信区间、风险评估 - - -### 目录结构 - -```shell -. -├──images -│ ├──arch.png -├──src -│ ├──data.py -│ ├──metrics.py -│ ├──model.py -│ ├──trainer.py -│ ├──utils.py -│ ├──__init__.py -├──configs -│ ├──config.yaml -├──README.md -├──Prob_DeepONet_Grid.ipynb -├──B_DeepONet_Grid.ipynb -└──train.py -``` - - - -## 结果指标 -介绍案例训练/推理的关键性能/精度指标。 -| 参数 | 指标 | -| :-----------: | :----------------------------------------------: | -| 硬件资源 | Atlas 800T A2 | -| MindSpore版本 | >=2.5.0 | -| 数据集 | [] | -| 参数量 | [] | -| 训练参数 | batch_size=[], steps_per_epoch=[], epochs=[] | -| 优化器 | Adam | -| 训练损失(MSE) | [] | -| 验证损失(MSE) | [] | -| 速度(ms/step) | [] | - - - - diff --git a/MindEnergy/application/deeponet-grid/requirements.txt b/MindEnergy/application/deeponet-grid/requirements.txt index 3b1dc7048..cb441bc13 100644 --- a/MindEnergy/application/deeponet-grid/requirements.txt +++ b/MindEnergy/application/deeponet-grid/requirements.txt @@ -14,9 +14,6 @@ tqdm>=4.62.0 matplotlib>=3.5.0 seaborn>=0.11.0 -# Optional: for tensorboard logging -tensorboard>=2.8.0 - # Optional: for Jupyter notebooks jupyter>=1.0.0 ipykernel>=6.0.0 \ No newline at end of file -- Gitee From d1944a70196fc1bc2551000bdc538114d0569c8b Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Thu, 10 Jul 2025 14:10:14 +0800 Subject: [PATCH 03/25] Remove unuseful codes. --- .../DeepONet_Grid_UQ_Probabilistic.ipynb | 4 +- .../application/deeponet-grid/src/__init__.py | 4 +- .../application/deeponet-grid/src/data.py | 4 +- .../application/deeponet-grid/src/metrics.py | 40 ------------------- .../application/deeponet-grid/src/model.py | 1 - .../application/deeponet-grid/src/trainer.py | 20 +--------- .../application/deeponet-grid/test_system.py | 3 +- MindEnergy/application/deeponet-grid/train.py | 1 - 8 files changed, 6 insertions(+), 71 deletions(-) diff --git a/MindEnergy/application/deeponet-grid/DeepONet_Grid_UQ_Probabilistic.ipynb b/MindEnergy/application/deeponet-grid/DeepONet_Grid_UQ_Probabilistic.ipynb index 8d4c7ee40..7a2fdb4ba 100644 --- a/MindEnergy/application/deeponet-grid/DeepONet_Grid_UQ_Probabilistic.ipynb +++ b/MindEnergy/application/deeponet-grid/DeepONet_Grid_UQ_Probabilistic.ipynb @@ -30,7 +30,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -41,7 +41,7 @@ "# from mindenergy.models import Prob_DeepONet\n", "from src.data import load_real_data, prepare_deeponet_data, normalize_data_mindspore, split_data, create_mindspore_datasets, trajectory_prediction, batch_trajectory_prediction\n", "from src.trainer import create_trainer\n", - "from src.metrics import compute_metrics, update_metrics_history, test, test_one, MetricsCalculator" + "from src.metrics import compute_metrics, update_metrics_history, MetricsCalculator" ] }, { diff --git a/MindEnergy/application/deeponet-grid/src/__init__.py b/MindEnergy/application/deeponet-grid/src/__init__.py index cb419fcbe..bb5ac7bce 100644 --- a/MindEnergy/application/deeponet-grid/src/__init__.py +++ b/MindEnergy/application/deeponet-grid/src/__init__.py @@ -1,7 +1,7 @@ from .trainer import DeepONetTrainer from .data import DataGenerator from .model import DeepONet, Prob_DeepONet -from .metrics import MetricsCalculator, compute_metrics, test, test_one, compute_r2_score, compute_mae, compute_mse, compute_calibration_error +from .metrics import MetricsCalculator, compute_metrics, compute_r2_score, compute_mae, compute_mse, compute_calibration_error __all__ = [ "DeepONetTrainer", @@ -10,8 +10,6 @@ __all__ = [ "Prob_DeepONet", "MetricsCalculator", "compute_metrics", - "test", - "test_one", "compute_r2_score", "compute_mae", "compute_mse", diff --git a/MindEnergy/application/deeponet-grid/src/data.py b/MindEnergy/application/deeponet-grid/src/data.py index 069a64df8..58722a6ba 100644 --- a/MindEnergy/application/deeponet-grid/src/data.py +++ b/MindEnergy/application/deeponet-grid/src/data.py @@ -416,9 +416,7 @@ def trajectory_prediction(u: np.ndarray, time_points: np.ndarray, model) -> np.n Returns: predictions: Predicted values at time points, shape (n_time_points, 1) - """ - import mindspore as ms - + """ # Ensure u is 2D for model input if len(u.shape) == 1: u = u.reshape(1, -1) # (1, n_sensors) diff --git a/MindEnergy/application/deeponet-grid/src/metrics.py b/MindEnergy/application/deeponet-grid/src/metrics.py index 00c7ff7f9..377983e4a 100644 --- a/MindEnergy/application/deeponet-grid/src/metrics.py +++ b/MindEnergy/application/deeponet-grid/src/metrics.py @@ -162,46 +162,6 @@ def update_metrics_history(history: Dict[str, List[float]], state: List[float]) return history -def test(model: ms.nn.Cell, u: List[ms.Tensor], y: List[ms.Tensor]) -> Tuple[List[ms.Tensor], List[ms.Tensor]]: - """Test model on multiple trajectories - - Args: - model: DeepONet model - u: List of input functions - y: List of evaluation points - - Returns: - Tuple of (mean_predictions, std_predictions) - """ - mean = [] - std = [] - - for input_k in zip(u, y): - # Forward pass without computing gradients - mean_k, log_std_k = model(input_k) - std_k = ops.Exp()(log_std_k) - - mean.append(mean_k) - std.append(std_k) - - return mean, std - -def test_one(model: ms.nn.Cell, u_k: ms.Tensor, y_k: ms.Tensor) -> Tuple[ms.Tensor, ms.Tensor]: - """Test model on single trajectory - - Args: - model: DeepONet model - u_k: Input function - y_k: Evaluation point - - Returns: - Tuple of (mean_prediction, std_prediction) - """ - mean, log_std = model((u_k, y_k)) - std = ops.Exp()(log_std) - - return mean, std - def compute_calibration_error(s_true: ms.Tensor, s_mean: ms.Tensor, s_std: ms.Tensor, n_bins: int = 10) -> float: """Compute calibration error for uncertainty quantification diff --git a/MindEnergy/application/deeponet-grid/src/model.py b/MindEnergy/application/deeponet-grid/src/model.py index 111fa1ada..fe742f628 100644 --- a/MindEnergy/application/deeponet-grid/src/model.py +++ b/MindEnergy/application/deeponet-grid/src/model.py @@ -116,7 +116,6 @@ class DeepONet(nn.Cell): def construct(self, xu, xy): """ Forward pass interface. To be implemented by subclasses. - 前向传播接口,由子类实现。 Args: xu: Branch input tensor diff --git a/MindEnergy/application/deeponet-grid/src/trainer.py b/MindEnergy/application/deeponet-grid/src/trainer.py index 841bb9739..fc81cc13f 100644 --- a/MindEnergy/application/deeponet-grid/src/trainer.py +++ b/MindEnergy/application/deeponet-grid/src/trainer.py @@ -19,7 +19,7 @@ import logging # Import metrics -from .metrics import compute_metrics, update_metrics_history, test, test_one, compute_calibration_error, compute_r2_score, compute_mae, compute_mse +from .metrics import compute_metrics, update_metrics_history, compute_calibration_error, compute_r2_score, compute_mae, compute_mse # Set up logging logging.basicConfig(level=logging.INFO) @@ -474,24 +474,6 @@ class DeepONetTrainer: return metrics - def test_trajectories(self, u_test: List[ms.Tensor], y_test: List[ms.Tensor], - s_test: List[ms.Tensor]) -> Tuple[List[ms.Tensor], List[ms.Tensor]]: - """Test model on trajectory data - - Args: - u_test: List of input functions for test trajectories - y_test: List of evaluation points for test trajectories - s_test: List of true solutions for test trajectories - - Returns: - Tuple of (mean_predictions, std_predictions) - """ - # MindSpore does not have set_eval(), use set_train(False) for evaluation mode - self.model.set_train(False) - - mean_predictions, std_predictions = test(self.model, u_test, y_test) - - return mean_predictions, std_predictions def compute_trajectory_metrics(self, s_test: List[ms.Tensor], mean_predictions: List[ms.Tensor], diff --git a/MindEnergy/application/deeponet-grid/test_system.py b/MindEnergy/application/deeponet-grid/test_system.py index a717da7d6..5d456ed68 100644 --- a/MindEnergy/application/deeponet-grid/test_system.py +++ b/MindEnergy/application/deeponet-grid/test_system.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 """ System test script for DeepONet-Grid-UQ -测试系统各个组件是否正常工作 """ import os @@ -17,7 +16,7 @@ sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) from src.model import Prob_DeepONet from src.data import generate_synthetic_data, normalize_data_mindspore, split_data, create_mindspore_datasets, load_real_data, prepare_deeponet_data, trajectory_prediction, batch_trajectory_prediction from src.trainer import create_trainer -from src.metrics import compute_metrics, update_metrics_history, test, test_one, MetricsCalculator +from src.metrics import compute_metrics, update_metrics_history, MetricsCalculator def test_config_loading(): """Test configuration loading""" diff --git a/MindEnergy/application/deeponet-grid/train.py b/MindEnergy/application/deeponet-grid/train.py index 6f28b0091..0ab2c50b9 100644 --- a/MindEnergy/application/deeponet-grid/train.py +++ b/MindEnergy/application/deeponet-grid/train.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 """ Main training script for DeepONet-Grid-UQ -基于MindSpore框架的DeepONet训练主程序 """ import os -- Gitee From a6e39fadd25b4c7bbf5dcc8559f1563fee746a12 Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Thu, 10 Jul 2025 16:31:15 +0800 Subject: [PATCH 04/25] Edit codes. --- .../application/deeponet-grid/src/data.py | 63 +++++++++++-------- .../application/deeponet-grid/src/utils.py | 10 +-- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/MindEnergy/application/deeponet-grid/src/data.py b/MindEnergy/application/deeponet-grid/src/data.py index 58722a6ba..32997fe23 100644 --- a/MindEnergy/application/deeponet-grid/src/data.py +++ b/MindEnergy/application/deeponet-grid/src/data.py @@ -373,20 +373,17 @@ def prepare_deeponet_data(u, y, s, time_points=None): n_points = y_n_points - # Expand data for DeepONet training - u_expanded = [] - y_expanded = [] - s_expanded = [] + # expand time points to batch dimension + # from (n_samples, n_sensors) and (n_samples, n_points) + # to (n_samples * n_points, n_sensors) and (n_samples * n_points, 1) - for i in range(n_samples): - for j in range(n_points): - u_expanded.append(u[i]) # Same input function for all time points - y_expanded.append([y[i][j]]) # Single time point - 确保是2D [value] - s_expanded.append([s[i, j]]) # Target value at this time point - 确保是2D [value] + # repeat u_batch to match each time point + u_expanded = np.repeat(u, n_points, axis=0) # (n_samples * n_points, n_sensors) + + # create query points for each time point of each sample + y_expanded = np.tile(y.reshape(-1, 1), (n_samples, 1)) # (n_samples * n_points, 1) + s_expanded = np.tile(s.reshape(-1, 1), (n_samples, 1)) # (n_samples * n_points, 1) - u_expanded = np.array(u_expanded) - y_expanded = np.array(y_expanded) - s_expanded = np.array(s_expanded) metadata = { 'n_samples': n_samples, @@ -416,7 +413,8 @@ def trajectory_prediction(u: np.ndarray, time_points: np.ndarray, model) -> np.n Returns: predictions: Predicted values at time points, shape (n_time_points, 1) - """ + """ + # Ensure u is 2D for model input if len(u.shape) == 1: u = u.reshape(1, -1) # (1, n_sensors) @@ -440,30 +438,43 @@ def trajectory_prediction(u: np.ndarray, time_points: np.ndarray, model) -> np.n return np.array(predictions).reshape(-1, 1) -def batch_trajectory_prediction(u_batch: np.ndarray, time_points: np.ndarray, model) -> np.ndarray: +def batch_trajectory_prediction(model, u_batch, time_points): """ - Predict trajectories for a batch of samples using DeepONet model + Batch prediction of trajectories Args: - u_batch: Input function values for batch, shape (batch_size, n_sensors) - time_points: Time points to predict, shape (n_time_points,) model: Trained DeepONet model - + u_batch: Input function batch, shape (batch_size, n_sensors) + time_points: Time points array, shape (n_time_points,) + Returns: - predictions: Predicted values for all samples at time points, shape (batch_size, n_time_points, 1) + mean_predictions: Predicted mean, shape (batch_size, n_time_points, 1) """ batch_size = u_batch.shape[0] n_time_points = len(time_points) - predictions = np.zeros((batch_size, n_time_points, 1)) + # expand time points to batch dimension + # from (batch_size, n_sensors) and (n_time_points,) + # to (batch_size * n_time_points, n_sensors) and (batch_size * n_time_points, 1) - # Predict for each sample in batch - for i in range(batch_size): - u_single = u_batch[i] # (n_sensors,) - pred_single = trajectory_prediction(u_single, time_points, model) # (n_time_points, 1) - predictions[i] = pred_single + # repeat u_batch to match each time point + u_expanded = np.repeat(u_batch, n_time_points, axis=0) # (batch_size * n_time_points, n_sensors) - return predictions + # create query points for each time point of each sample + y_expanded = np.tile(time_points.reshape(-1, 1), (batch_size, 1)) # (batch_size * n_time_points, 1) + + # convert to MindSpore tensors + u_tensor = ms.Tensor(u_expanded, ms.float32) + y_tensor = ms.Tensor(y_expanded, ms.float32) + + # one inference to get all predictions + mean_pred, _ = model(u_tensor, y_tensor) + + # reshape results + mean_predictions = mean_pred.reshape(batch_size, n_time_points, 1) + + return mean_predictions + if __name__ == "__main__": import argparse diff --git a/MindEnergy/application/deeponet-grid/src/utils.py b/MindEnergy/application/deeponet-grid/src/utils.py index e4a48cde2..5cfb08973 100644 --- a/MindEnergy/application/deeponet-grid/src/utils.py +++ b/MindEnergy/application/deeponet-grid/src/utils.py @@ -73,8 +73,8 @@ def parse_val_loss_log(log_file_path: str) -> Tuple[List[float], List[int], List return val_losses, epochs, steps # Regular expression to match validation loss log format - # INFO:root:[Eval] Epoch 1, Step 100, Val-Loss: -0.705899 (negated for display) - pattern = r'INFO:root:\[Eval\] Epoch (\d+), Step (\d+), Val-Loss: ([-\d.]+) \(negated for display\)' + # INFO:root:[Eval] Epoch 1, Step 100, Val-Loss: -0.705899 + pattern = r'INFO:root:\[Eval\] Epoch (\d+), Step (\d+), Val-Loss: ([-\d.]+) ' with open(log_file_path, 'r', encoding='utf-8') as f: for line in f: @@ -118,7 +118,7 @@ def plot_loss_curves(log_file_path: str, # Plot training loss ax1.plot(steps, losses, 'b-', label='Training Loss', linewidth=1, alpha=0.8) ax1.set_xlabel('Training Steps') - ax1.set_ylabel('Loss (negated for display)') + ax1.set_ylabel('Loss') ax1.set_title('Training Loss Curve') ax1.grid(True, alpha=0.3) ax1.legend() @@ -138,7 +138,7 @@ def plot_loss_curves(log_file_path: str, ax2.plot(unique_epochs, epoch_losses, 'g-', label='Average Epoch Loss', linewidth=2) ax2.set_xlabel('Epochs') - ax2.set_ylabel('Average Loss (negated for display)') + ax2.set_ylabel('Average Loss') ax2.set_title('Average Loss per Epoch') ax2.grid(True, alpha=0.3) ax2.legend() @@ -184,7 +184,7 @@ def plot_loss_statistics(log_file_path: str, ax1 = plt.subplot(2, 3, 1) ax1.plot(steps, losses, 'b-', alpha=0.7, linewidth=0.8) ax1.set_xlabel('Training Steps') - ax1.set_ylabel('Loss (negated for display)') + ax1.set_ylabel('Loss') ax1.set_title('Training Loss') ax1.grid(True, alpha=0.3) -- Gitee From 9acff8fa73c16e2594ecfb8f323f7a62631ac4c8 Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Thu, 10 Jul 2025 16:46:30 +0800 Subject: [PATCH 05/25] Edit tests for trajectory_prediction. --- MindEnergy/application/deeponet-grid/test_system.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/MindEnergy/application/deeponet-grid/test_system.py b/MindEnergy/application/deeponet-grid/test_system.py index 5d456ed68..c359074eb 100644 --- a/MindEnergy/application/deeponet-grid/test_system.py +++ b/MindEnergy/application/deeponet-grid/test_system.py @@ -135,12 +135,6 @@ def test_trajectory_prediction(): data_path = "/Users/congwang/Documents/codes/M_deeponetgrid/deeponet_ms/data/grid_real/data/trustworthydatasetv1/test-data-voltage-m-33-mix.npz" u, y, s = load_real_data(data_path) - # Prepare data - u_expanded, y_expanded, s_expanded, metadata = prepare_deeponet_data(u, y, s) - - print(f" Prepared data shapes: u={u_expanded.shape}, y={y_expanded.shape}, s={s_expanded.shape}") - print(f" Metadata: {metadata}") - # Test trajectory_prediction function print(" Testing trajectory_prediction function...") @@ -165,15 +159,16 @@ def test_trajectory_prediction(): # Test single sample trajectory prediction u_single = u[0] # First sample, shape (33,) - time_points = np.array([0.0, 0.5, 1.0, 1.5, 2.0]) + y_single = y[0] # First sample, shape (100,) - predictions = trajectory_prediction(u_single, time_points, model) + predictions = trajectory_prediction(u_single, y_single, model) print(f" Single sample prediction shape: {predictions.shape}") print(f" Predictions: {predictions.flatten()}") # Test batch trajectory prediction u_batch = u[:3] # First 3 samples, shape (3, 33) - batch_predictions = batch_trajectory_prediction(u_batch, time_points, model) + y_batch = y[:3] # First 3 samples, shape (3, 100) + batch_predictions = batch_trajectory_prediction(u_batch, y_batch, model) print(f" Batch prediction shape: {batch_predictions.shape}") print(" Correct: Trajectory prediction functions work correctly") -- Gitee From a977cea155f08294ad638b253b4f9bec17968908 Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Thu, 17 Jul 2025 11:16:54 +0800 Subject: [PATCH 06/25] Fix comments. --- .../DeepONet_Grid_UQ_Probabilistic.ipynb | 19 +- .../application/deeponet-grid/README.md | 4 +- .../application/deeponet-grid/README_en.md | 262 +++++++- .../deeponet-grid/configs/config.yaml | 23 +- .../application/deeponet-grid/inference.py | 426 +++++++++++++ .../application/deeponet-grid/src/__init__.py | 35 +- .../application/deeponet-grid/src/data.py | 498 ++++++++-------- .../application/deeponet-grid/src/metrics.py | 175 +++--- .../application/deeponet-grid/src/model.py | 241 ++++---- .../application/deeponet-grid/src/trainer.py | 558 +++++++----------- .../application/deeponet-grid/src/utils.py | 331 ++++++----- .../application/deeponet-grid/test_system.py | 544 ----------------- MindEnergy/application/deeponet-grid/train.py | 404 ++++++------- 13 files changed, 1788 insertions(+), 1732 deletions(-) create mode 100644 MindEnergy/application/deeponet-grid/inference.py delete mode 100644 MindEnergy/application/deeponet-grid/test_system.py diff --git a/MindEnergy/application/deeponet-grid/DeepONet_Grid_UQ_Probabilistic.ipynb b/MindEnergy/application/deeponet-grid/DeepONet_Grid_UQ_Probabilistic.ipynb index 7a2fdb4ba..3f77320a5 100644 --- a/MindEnergy/application/deeponet-grid/DeepONet_Grid_UQ_Probabilistic.ipynb +++ b/MindEnergy/application/deeponet-grid/DeepONet_Grid_UQ_Probabilistic.ipynb @@ -30,7 +30,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -39,9 +39,9 @@ "\n", "from src.model import Prob_DeepONet\n", "# from mindenergy.models import Prob_DeepONet\n", - "from src.data import load_real_data, prepare_deeponet_data, normalize_data_mindspore, split_data, create_mindspore_datasets, trajectory_prediction, batch_trajectory_prediction\n", + "from src.data import load_real_data, prepare_deeponet_data, normalize_data, split_data, create_datasets, trajectory_prediction, batch_trajectory_prediction\n", "from src.trainer import create_trainer\n", - "from src.metrics import compute_metrics, update_metrics_history, MetricsCalculator" + "from src.metrics import compute_metrics, update_metrics_history, test, MetricsCalculator" ] }, { @@ -245,7 +245,7 @@ "\n", "# Normalize data (optional - you can comment this out if not needed)\n", "# print(\"Normalizing data...\")\n", - "# u_train_norm, y_train_norm, s_train_norm, scalers = normalize_data_mindspore(\n", + "# u_train_norm, y_train_norm, s_train_norm, scalers = normalize_data(\n", "# u_train_expanded, y_train_expanded, s_train_expanded, method='standard'\n", "# )\n", "\n", @@ -253,12 +253,12 @@ "print(\"Splitting data...\")\n", "data_splits = split_data(\n", " u_train_expanded, y_train_expanded, s_train_expanded,\n", - " train_split=0.1, val_split=0.5, test_split=0.4\n", + " train_ratio=0.1, val_split=0.5, test_split=0.4\n", ")\n", "\n", "# Create MindSpore datasets\n", "print(\"Creating MindSpore datasets...\")\n", - "datasets = create_mindspore_datasets(data_splits, batch_size=batch_size)\n", + "datasets = create_datasets(data_splits, batch_size=batch_size)\n", "print(f\"Created datasets: {list(datasets.keys())}\")" ] }, @@ -364,10 +364,9 @@ " 'learning_rate': learning_rate,\n", " 'batch_size': batch_size,\n", " 'epochs': n_epochs,\n", - " 'print_every': 10,\n", - " 'eval_every': 100,\n", - " 'patience': 100,\n", - " 'factor': 0.8,\n", + " 'log_interval': 10,\n", + " 'eval_interval': 100,\n", + " \n", " 'verbose': verbose,\n", " 'loss_type': 'nll',\n", " 'optimizer': 'adam',\n", diff --git a/MindEnergy/application/deeponet-grid/README.md b/MindEnergy/application/deeponet-grid/README.md index 64d6685ad..0ef0f7188 100644 --- a/MindEnergy/application/deeponet-grid/README.md +++ b/MindEnergy/application/deeponet-grid/README.md @@ -1,6 +1,6 @@ -# DeepONet-Grid-UQ with MindSpore +# DeepONet-Grid-UQ -基于MindSpore框架的DeepONet-Grid不确定性量化实现。 +用于电力系统故障后进行预测的DeepONet-Grid网络 ## 案例特性概述 diff --git a/MindEnergy/application/deeponet-grid/README_en.md b/MindEnergy/application/deeponet-grid/README_en.md index 1fb8bfbc3..943191992 100644 --- a/MindEnergy/application/deeponet-grid/README_en.md +++ b/MindEnergy/application/deeponet-grid/README_en.md @@ -1,40 +1,262 @@ -# DeepOnet-Grid-UQ +# DeepONet-Grid-UQ -## Description +DeepONet-Grid network for power system fault prediction -This work build an efficient DeepONet that - - (i) takes as inputs the trajectories collected before and during the fault and - (ii) outputs the predicted post-fault trajectories. +## Project Structure -In addition, they also endow their method with the much-needed ability to balance efficiency with reliable/trustworthy predictions via Ucertainty Quantification. +``` +deeponet-grid/ +├── configs/ +│ └── config.yaml # Configuration file +├── src/ +│ ├── model.py # DeepONet model definition +│ ├── data.py # Data loading and preprocessing +│ ├── utils.py # Utility functions +│ ├── trainer.py # Trainer implementation +│ └── metrics.py # Evaluation metrics +├── train.py # Main training script +├── inference.py # Inference script +├── test_system.py # System test script +├── requirements.txt # Dependency list +├── README_en.md # This file +└── outputs/ # Output directory (auto-created) +``` -Original Paper : [DeepONet-grid-UQ: A trustworthy deep operator framework for predicting the power grid’s post-fault trajectories](!https://www.sciencedirect.com/science/article/abs/pii/S0925231223002503) -Original Code on torch: [Github](!https://github.com/cmoyacal/DeepONet-Grid-UQ) +## Installation +1 Install MindSpore framework: + ```bash + pip install mindspore + ``` +## Configuration +Edit the `configs/config.yaml` file to configure model parameters: -## Implementation +### Model Configuration +- `branch`: Branch network configuration (processes input functions) +- `trunk`: Trunk network configuration (processes evaluation points) +- `use_bias`: Whether to use bias terms -### Enviroment +### Training Configuration +- `learning_rate`: Learning rate +- `batch_size`: Batch size +- `epochs`: Number of training epochs +- `optimizer`: Optimizer type (adam, sgd, adamw) +- `loss_type`: Loss function type (nll, mse) -MindSpore 2.6.0 +### Data Configuration +- `use_synthetic`: Whether to use synthetic data +- `data_path`: Data file path +- `normalize`: Whether to normalize data +## Usage -### Network -From the original work, they have built two networks: +### 1. Training with Real Data - (1) Bayesian DeepONet (B-DeepONet) - uses stochastic gradient Hamiltonian Monte-Carlo to sample from the posterior distribution of the DeepONet trainable parameters +```bash +python train.py --data_path /path/to/your/data.npz --epochs 500 +``` - (2) Probabilistic DeepONet (Prob-DeepONet) that uses a probabilistic training strategy to enable quantifying uncertainty at virtually no extra computational cost. +### 2. Custom Configuration -Currently, we only support Porb-DeepONet in `src/model.py`. +```bash +python train.py \ + --config configs/config.yaml \ + --data_path data/real_data.npz \ + --output_dir outputs/my_experiment \ + --epochs 1000\ + --batch_size 64 --learning_rate 1e-4 +``` -### Data Loader & Training Codes & Evaluation +### 3sume Training from Checkpoint -under development +```bash +python train.py --resume outputs/best_model.ckpt +``` +###4un Evaluation Only +```bash +python train.py --test_only --resume outputs/best_model.ckpt +``` +###5Inference +```bash +# Single data point inference +python inference.py --checkpoint outputs/best_model.ckpt \ + --data_path data/test-data.npz \ + --single_inference \ + --data_index 0 + +# Dataset inference +python inference.py --checkpoint outputs/best_model.ckpt \ + --data_path data/test-data.npz \ + --output_dir inference_results +``` + +## Data Format + +Data files should be in `.npz` format with the following fields: + +- `u`: Input function values, shape `(n_samples, n_sensors)` +- `y`: Evaluation points, shape `(n_samples, n_points, n_dim)` +- `s`: True solution values, shape `(n_samples, n_points, n_output)` + +Example: +```python +import numpy as np + +# Generate sample data +n_samples =1000 +n_sensors = 20 +n_points = 1 + +u = np.random.randn(n_samples, n_sensors) +y = np.random.rand(n_samples, n_points, 1) * 2.0 +s = np.sin(u.mean(axis=1, keepdims=True) * y.squeeze(-1)) +s = s.reshape(n_samples, n_points, 1 Save data +np.savez(data.npz, u=u, y=y, s=s) +``` + +## Model Architecture + +DeepONet consists of two main components: + +1. **Branch Network**: Processes input function `u(x)` +2**Trunk Network**: Processes evaluation points `y` + +Output is calculated through dot product: +``` +G(u)(y) = Σᵢ bᵢ(u) tᵢ(y) +``` + +Where `bᵢ(u)` is the branch network output and `tᵢ(y)` is the trunk network output. + +## Uncertainty Quantification + +The model provides uncertainty quantification capabilities: + +- **Mean Prediction**: Expected value predicted by the model +- **Standard Deviation Prediction**: Uncertainty measure of predictions + +Supported loss functions: +- **Negative Log Likelihood (NLL)**: For uncertainty quantification +- **Mean Squared Error (MSE)**: Standard regression loss + +## Output Files + +After training, the following files are generated in the output directory: + +- `best_model.ckpt`: Best model checkpoint +- `final_model.ckpt`: Final model checkpoint +- `training_history.json`: Training history +- `test_results.json`: Test set evaluation results +- `training.log`: Training log + +## Evaluation Metrics + +- **MSE**: Mean squared error +- **MAE**: Mean absolute error +- **R²**: Coefficient of determination +- **Calibration Error**: Calibration error + +## Troubleshooting + +### Common Issues +1nsufficient Memory**: Reduce `batch_size` +2**Slow Training**: Use GPU or Ascend devices +3. **Convergence Difficulties**: Adjust learning rate or use learning rate scheduler + +### Debug Mode + +Set environment variable to enable detailed logging: +```bash +export MINDSPORE_LOG_LEVEL=DEBUG +python train.py +``` + +## System Testing + +Run comprehensive system tests: +```bash +python test_system.py +``` + +This will test: +- Configuration loading +- Data generation and loading +- Model creation +- Training workflow +- Inference functionality +- Metrics calculation + +## Extension Features + +### Custom Loss Functions + +Add new loss function classes in `src/trainer.py`: + +```python +class CustomLoss(nn.Cell): + def construct(self, mean_pred, log_std_pred, target): + # Custom loss calculation + return loss +``` + +### Custom Data Loaders + +Add new data loading functions in `src/data.py`: + +```python +def load_custom_data(data_path): + # Custom data loading logic + return u, y, s +``` + +## Key Features + +### 1. Adaptive Learning Rate Scheduling +The project supports learning rate scheduling strategies: + +- **CosineAnnealingLR**: Cosine annealing scheduling + +Configuration example: +```yaml +training: + scheduler: "cosine" # Scheduler type + patience: 100 # Patience value + factor: 08 # Decay factor + min_lr: 1e-7 # Minimum learning rate +``` + +### 2. Probabilistic Uncertainty Quantification +- Outputs mean and standard deviation +- Supports uncertainty quantification +- Uses negative log likelihood loss + +### 3. Data Processing +- Automatic multi-time-point data processing +- Data normalization +- Train/validation/test splitting + +### 4. Training Monitoring +- Print loss every 10s +- Validate every100ps +- Automatically save best model + +### 5. Trajectory Prediction +- Supports single sample and batch trajectory prediction +- Uncertainty quantification +- Confidence interval calculation + +## Comparison with Original PyTorch Version + +| Feature | PyTorch Version | MindSpore Version | +|---------|-----------------|-------------------| +| Framework | PyTorch | MindSpore | +| Device Support | GPU/CPU | CPU/GPU/Ascend | +| Model Structure | Same | Same | +| Loss Function | NLL | NLL | +| Data Processing | Manual expansion | Automatic expansion | +| Training Monitoring | Basic | Enhanced (step-level monitoring) | diff --git a/MindEnergy/application/deeponet-grid/configs/config.yaml b/MindEnergy/application/deeponet-grid/configs/config.yaml index bea0b4313..0009886ec 100644 --- a/MindEnergy/application/deeponet-grid/configs/config.yaml +++ b/MindEnergy/application/deeponet-grid/configs/config.yaml @@ -23,24 +23,27 @@ training: learning_rate: 0.00005 # Learning rate (5e-5) batch_size: 1024 # Batch size epochs: 100 # Number of training epochs - print_every: 100 # Print progress every N steps - eval_every: 1000 # Evaluate every N steps + log_interval: 100 # Print progress every N steps + eval_interval: 1000 # Evaluate every N steps # Learning rate scheduler - patience: 100 # Early stopping patience - factor: 0.8 # Learning rate reduction factor - - # Other settings + scheduler: "cosine" + min_lr: 1e-7 # Minimum learning rate + max_lr: 1e-3 # Maximum learning rate + total_step: 10000 # Total number of steps + step_per_epoch: 1000 # Number of steps per epoch + decay_epoch: 1000 # Number of epochs for decay verbose: true # Whether to output detailed information loss_type: "nll" # Loss function type: "nll" (negative log likelihood) optimizer: "adam" # Optimizer weight_decay: 0.0 # Weight decay + amp_level: "O0" # Mixed precision level: "O0", "O1", "O2" # Data Configuration data: - data_path: null # Real data path (if null, use synthetic data) + data_path: "data/train-data-voltage-m-33-Q-100-mix.npz" # Real data path (if null, use synthetic data) use_synthetic: true # Whether to use synthetic data - train_split: 0.1 # Training set ratio + train_ratio: 0.1 # Training set ratio val_split: 0.5 # Validation set ratio test_split: 0.4 # Test set ratio @@ -50,10 +53,6 @@ data: n_points: 100 # Number of time points seed: 42 # Random seed -# Device Configuration -device: - target: "CPU" # Device target: "CPU", "GPU", "Ascend" - mode: "PYNATIVE_MODE" # Running mode: "PYNATIVE_MODE", "GRAPH_MODE" # Output Configuration output: diff --git a/MindEnergy/application/deeponet-grid/inference.py b/MindEnergy/application/deeponet-grid/inference.py new file mode 100644 index 000000000..9bb5dbfa9 --- /dev/null +++ b/MindEnergy/application/deeponet-grid/inference.py @@ -0,0 +1,426 @@ +#!/usr/bin/env python3 +""" +Main inference script for DeepONet-Grid-UQ +""" +import argparse +import json +import logging +import os +import sys +from pathlib import Path +from typing import List, Optional, Tuple, Union + +import mindspore as ms +import numpy as np +import yaml +from mindspore import context + +from src.data import load_and_preprocess_real_data +from src.model import Prob_DeepONet +from src.utils import load_config, load_trained_model + +# Add src to path +sys.path.append(os.path.join(os.path.dirname(__file__), "src")) + + +# Set up logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[ + logging.FileHandler("inference.log"), + logging.StreamHandler( + sys.stdout)], +) +logger = logging.getLogger(__name__) + +# Set device context +context.set_context(mode=context.PYNATIVE_MODE, device_target="CPU") +logger.info(f"Mode set to: {context.get_context('mode')}") +logger.info(f"Device set to: {context.get_context('device_target')}") + + +def create_model(config: dict, metadata: dict) -> Prob_DeepONet: + """Create DeepONet model based on configuration and data metadata + + Args: + config: Configuration dictionary + metadata: Data metadata containing input/output dimensions + + Returns: + DeepONet model instance + """ + model_config = config["model"] + + # Get network parameters from metadata if available, otherwise use config + # defaults + m = metadata.get( + "n_sensors", model_config.get( + "m", 33)) # Number of sensors + # Input dimension for trunk (always 1 for DeepONet) + dim = model_config.get("dim", 1) + width = model_config.get("width", 200) # Network width + # Network depth (number of hidden layers) + depth = model_config.get("depth", 3) + n_basis = model_config.get("n_basis", 100) # Number of basis functions + + # Get network types + branch_type = model_config.get("branch_type", "modified") + trunk_type = model_config.get("trunk_type", "modified") + activation = model_config.get("activation", "sin") + + # Compute layer sizes automatically + branch_layer_size = [m] + [width] * depth + [n_basis] + trunk_layer_size = [dim] + [width] * depth + [n_basis] + + # Create branch configuration + branch_config = { + "type": branch_type, + "layer_size": branch_layer_size, + "activation": activation, + } + + # Create trunk configuration + trunk_config = { + "type": trunk_type, + "layer_size": trunk_layer_size, + "activation": activation, + } + + logger.info(f"Branch network: {branch_layer_size}") + logger.info(f"Trunk network: {trunk_layer_size}") + logger.info(f"Branch type: {branch_type}, Trunk type: {trunk_type}") + logger.info(f"Activation: {activation}") + logger.info(f"Depth: {depth} (number of hidden layers)") + logger.info(f"Input sensors (m): {m}") + logger.info(f"Trunk input dimension (dim): {dim}") + + # Create model + model = Prob_DeepONet( + branch=branch_config, + trunk=trunk_config, + use_bias=model_config["use_bias"]) + + logger.info("Model created successfully") + return model + + +def single_inference( + model: Prob_DeepONet, u: np.ndarray, y: np.ndarray +) -> Tuple[np.ndarray, np.ndarray]: + + # Convert to MindSpore tensors + u_tensor = ms.Tensor(u, ms.float32) + y_tensor = ms.Tensor(y, ms.float32) + + # Set model to evaluation mode + model.set_train(False) + + # Forward pass + mean_pred, log_std_pred = model(u_tensor, y_tensor) + + # Convert log_std to std + std_pred = ms.ops.Exp()(log_std_pred) + + # Convert back to numpy + mean_np = mean_pred.asnumpy() + std_np = std_pred.asnumpy() + + return mean_np, std_np + + +def batch_inference( + model: Prob_DeepONet, u: np.ndarray, y: np.ndarray +) -> Tuple[np.ndarray, np.ndarray]: + """Perform inference on batch of data + + Args: + model: Trained DeepONet model + u: Input function values (shape: [batch_size, n_sensors]) + y: Evaluation points (shape: [batch_size, n_points, 1]) + + Returns: + Tuple of (mean_pred, std_pred) + """ + # Convert to MindSpore tensors + u_tensor = ms.Tensor(u, ms.float32) + y_tensor = ms.Tensor(y, ms.float32) + + # Set model to evaluation mode + model.set_train(False) + + # Forward pass + mean_pred, log_std_pred = model(u_tensor, y_tensor) + + # Convert log_std to std + std_pred = ms.ops.Exp()(log_std_pred) + + # Convert back to numpy + mean_np = mean_pred.asnumpy() + std_np = std_pred.asnumpy() + + return mean_np, std_np + + +def get_single_data_from_dataset( + datasets: dict, data_index: int = 0 +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Extract single data point from dataset + + Args: + datasets: Dictionary containing train/val/test datasets + data_index: Index of the data point to extract (default: 0) + + Returns: + Tuple of (u, y, target) for the specified data point + """ + # Use test dataset for inference + test_dataset = datasets["test"] + + # Get dataset size + dataset_size = test_dataset.get_dataset_size() + batch_size = test_dataset.get_batch_size() + + logger.info( + f"Dataset size: {dataset_size} batches, batch size: {batch_size}") + + # Calculate which batch and position in batch + batch_index = data_index // batch_size + position_in_batch = data_index % batch_size + + if batch_index >= dataset_size: + raise ValueError( + f"Data index {data_index} exceeds dataset size {dataset_size * batch_size}" + ) + + # Get the specific data point from the dataset + batch_count = 0 + for u, y, target in test_dataset: + if batch_count == batch_index: + # Extract the specific data point from the batch + u_single = u[position_in_batch].asnumpy() + y_single = y[position_in_batch].asnumpy() + target_single = target[position_in_batch].asnumpy() + + logger.info( + f"Extracted data point {data_index} from batch {batch_index}, position {position_in_batch}" + ) + logger.info( + f"u shape: {u_single.shape}, y shape: {y_single.shape}, target shape: {target_single.shape}" + ) + + return u_single, y_single, target_single + + batch_count += 1 + + raise ValueError(f"Could not find data point {data_index}") + + +def inference_on_dataset( + model: Prob_DeepONet, + dataset_path: str, + checkpoint_path: str, + output_dir: str = "inference_results", +) -> None: + """Perform inference on entire dataset + + Args: + model: DeepONet model + dataset_path: Path to dataset file + checkpoint_path: Path to model checkpoint + output_dir: Output directory for results + """ + # Load trained model + model = load_trained_model(model, checkpoint_path) + logger.info(f"Model loaded from: {checkpoint_path}") + + # Load dataset + logger.info(f"Loading dataset from: {dataset_path}") + datasets, metadata = load_and_preprocess_real_data( + {"data": {"data_path": dataset_path}} + ) + + # Create output directory + os.makedirs(output_dir, exist_ok=True) + + # Perform inference on test set + logger.info("Performing inference on test dataset...") + test_dataset = datasets["test"] + + all_predictions = [] + all_targets = [] + all_means = [] + all_stds = [] + + for u, y, target in test_dataset: + # Forward pass + mean_pred, log_std_pred = model(u, y) + std_pred = ms.ops.Exp()(log_std_pred) + + # Store results + all_predictions.append(mean_pred) + all_targets.append(target) + all_means.append(mean_pred) + all_stds.append(std_pred) + + # Concatenate all batches + if all_predictions: + predictions = ms.ops.Concat(axis=0)(all_predictions) + targets = ms.ops.Concat(axis=0)(all_targets) + means = ms.ops.Concat(axis=0)(all_means) + stds = ms.ops.Concat(axis=0)(all_stds) + + # Convert to numpy for saving + predictions_np = predictions.asnumpy() + targets_np = targets.asnumpy() + means_np = means.asnumpy() + stds_np = stds.asnumpy() + + # Save results + results = { + "predictions": predictions_np.tolist(), + "targets": targets_np.tolist(), + "means": means_np.tolist(), + "stds": stds_np.tolist(), + } + + results_path = os.path.join(output_dir, "inference_results.json") + with open(results_path, "w") as f: + json.dump(results, f, indent=2) + + logger.info(f"Inference results saved to: {results_path}") + logger.info(f"Predictions shape: {predictions_np.shape}") + logger.info(f"Targets shape: {targets_np.shape}") + + +def main(): + """Main inference function""" + parser = argparse.ArgumentParser(description="DeepONet-Grid-UQ Inference") + parser.add_argument( + "--config", + type=str, + default="configs/config.yaml", + help="Path to configuration file", + ) + parser.add_argument( + "--checkpoint", + type=str, + required=True, + help="Path to model checkpoint") + parser.add_argument( + "--data_path", + type=str, + default=None, + help="Path to data file for dataset inference", + ) + parser.add_argument( + "--output_dir", + type=str, + default="inference_results", + help="Output directory for results", + ) + parser.add_argument( + "--single_inference", + action="store_true", + help="Perform single data point inference", + ) + parser.add_argument( + "--data_index", + type=int, + default=0, + help="Index of data point for single inference (default: 0)", + ) + + args = parser.parse_args() + + # Load configuration + logger.info(f"Loading configuration from {args.config}") + config = load_config(args.config) + + # Load and preprocess data to get metadata + logger.info("Loading data to get metadata...") + datasets, metadata = load_and_preprocess_real_data(config) + + logger.info(f"Data metadata:") + logger.info(f" Input dimension: {metadata['input_dim']}") + logger.info(f" Output dimension: {metadata['output_dim']}") + logger.info(f" Total samples: {metadata['n_samples']}") + logger.info(f" Number of sensors: {metadata['n_sensors']}") + + # Create model + logger.info("Creating model...") + model = create_model(config, metadata) + + if args.single_inference: + # Single data point inference from dataset + if args.data_path is None: + logger.error("For single inference, --data_path must be provided") + return + + # Load trained model + model = load_trained_model(model, args.checkpoint) + logger.info(f"Model loaded from: {args.checkpoint}") + + # Load dataset and get specific data point + logger.info(f"Loading dataset from: {args.data_path}") + datasets, metadata = load_and_preprocess_real_data( + {"data": {"data_path": args.data_path}} + ) + + # Extract specific data point + u_data, y_data, target_data = get_single_data_from_dataset( + datasets, args.data_index + ) + + # Perform single inference + logger.info("Performing single inference...") + mean_pred, std_pred = single_inference(model, u_data, y_data) + + logger.info("Single inference results:") + logger.info(f" Data index: {args.data_index}") + logger.info(f" Target value: {target_data}") + logger.info(f" Mean prediction: {mean_pred}") + logger.info(f" Standard deviation: {std_pred}") + logger.info(f" Prediction error: {abs(mean_pred - target_data)}") + + # Save results + results = { + "data_index": args.data_index, + "target_value": target_data.tolist(), + "mean_prediction": mean_pred.tolist(), + "std_prediction": std_pred.tolist(), + "prediction_error": abs(mean_pred - target_data).tolist(), + "u_input": u_data.tolist(), + "y_input": y_data.tolist(), + } + + os.makedirs(args.output_dir, exist_ok=True) + results_path = os.path.join( + args.output_dir, f"single_inference_index_{args.data_index}.json" + ) + with open(results_path, "w") as f: + json.dump(results, f, indent=2) + + logger.info(f"Single inference results saved to: {results_path}") + + else: + # Dataset inference + if args.data_path is None: + logger.error("For dataset inference, --data_path must be provided") + return + + # Perform inference on dataset + inference_on_dataset( + model, + args.data_path, + args.checkpoint, + args.output_dir) + + logger.info("Dataset inference completed successfully!") + + +if __name__ == "__main__": + main() + # python inference.py --checkpoint outputs/best_model.ckpt \ + # --data_path data/test-data-voltage-m-33-mix.npz \ + # --output_dir inference_results diff --git a/MindEnergy/application/deeponet-grid/src/__init__.py b/MindEnergy/application/deeponet-grid/src/__init__.py index bb5ac7bce..45bb07076 100644 --- a/MindEnergy/application/deeponet-grid/src/__init__.py +++ b/MindEnergy/application/deeponet-grid/src/__init__.py @@ -1,17 +1,38 @@ -from .trainer import DeepONetTrainer +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== from .data import DataGenerator +from .metrics import ( + MetricsCalculator, + compute_calibration_error, + compute_mae, + compute_metrics, + compute_mse, + compute_r2_score, +) from .model import DeepONet, Prob_DeepONet -from .metrics import MetricsCalculator, compute_metrics, compute_r2_score, compute_mae, compute_mse, compute_calibration_error +from .trainer import DeepONetTrainer __all__ = [ - "DeepONetTrainer", - "DataGenerator", - "DeepONet", + "DeepONetTrainer", + "DataGenerator", + "DeepONet", "Prob_DeepONet", "MetricsCalculator", "compute_metrics", "compute_r2_score", "compute_mae", "compute_mse", - "compute_calibration_error" -] \ No newline at end of file + "compute_calibration_error", +] diff --git a/MindEnergy/application/deeponet-grid/src/data.py b/MindEnergy/application/deeponet-grid/src/data.py index 32997fe23..7ca0ca056 100644 --- a/MindEnergy/application/deeponet-grid/src/data.py +++ b/MindEnergy/application/deeponet-grid/src/data.py @@ -1,135 +1,79 @@ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import os +from typing import Any, Dict, Optional, Tuple + +import mindspore as ms +import mindspore.numpy as mnp import numpy as np import pandas as pd -from scipy import stats -import mindspore as ms from mindspore import context from mindspore.dataset import GeneratorDataset -import mindspore.numpy as mnp from mindspore.ops import operations as ops -from typing import Tuple, Optional, Dict, Any -import os -import yaml +from scipy import stats -def analyze_npz_data(data_path): - """Analyze npz data file and print statistical information - - Args: - data_path (str): Path to the npz file - """ - print(f"\n{'='*50}") - print(f"Analyzing data from: {data_path}") - print(f"{'='*50}") - - # Load data - data = np.load(data_path) - - # Print available keys - print("\nAvailable data fields:") - print("-" * 30) - for key in data.files: - print(f"- {key}") - - # Analyze each field - print("\nStatistical Analysis:") - print("-" * 50) - - # Create a list to store all statistics - stats_list = [] - - for key in data.files: - arr = data[key] - print(f"\nField: {key}") - print(f"Shape: {arr.shape}") - print(f"Data type: {arr.dtype}") - - # Basic statistics - stats_dict = { - 'Field': key, - 'Shape': arr.shape, - 'Mean': np.mean(arr), - 'Median': np.median(arr), - 'Std': np.std(arr), - 'Min': np.min(arr), - 'Max': np.max(arr), - '25%': np.percentile(arr, 25), - '75%': np.percentile(arr, 75), - 'Skewness': stats.skew(arr.flatten()), - 'Kurtosis': stats.kurtosis(arr.flatten()) - } - stats_list.append(stats_dict) - - # Print statistics - print(f"Mean: {stats_dict['Mean']:.6f}") - print(f"Median: {stats_dict['Median']:.6f}") - print(f"Standard Deviation: {stats_dict['Std']:.6f}") - print(f"Min: {stats_dict['Min']:.6f}") - print(f"Max: {stats_dict['Max']:.6f}") - print(f"25th percentile: {stats_dict['25%']:.6f}") - print(f"75th percentile: {stats_dict['75%']:.6f}") - print(f"Skewness: {stats_dict['Skewness']:.6f}") - print(f"Kurtosis: {stats_dict['Kurtosis']:.6f}") - - # Check for NaN and Inf values - nan_count = np.isnan(arr).sum() - inf_count = np.isinf(arr).sum() - if nan_count > 0 or inf_count > 0: - print(f"\nWarning: Found {nan_count} NaN values and {inf_count} Inf values") - - # Print value range distribution - print("\nValue Range Distribution:") - hist, bins = np.histogram(arr.flatten(), bins=10) - for i in range(len(hist)): - print(f"[{bins[i]:.2f}, {bins[i+1]:.2f}]: {hist[i]} values") - - return stats_list class DataGenerator: """Data generator for MindSpore training""" - def __init__(self, u: np.ndarray, y: np.ndarray, s: np.ndarray, dtype: str = 'float32'): + + def __init__( + self, u: np.ndarray, y: np.ndarray, s: np.ndarray, dtype: str = "float32" + ): self.u = u self.y = y self.s = s self.len = len(u) - self.dtype = ms.float32 if dtype == 'float32' else ms.float64 - + self.dtype = ms.float32 if dtype == "float32" else ms.float64 + def __getitem__(self, index): # Return data as MindSpore tensors with float32 dtype by default # Ensure proper shapes for DeepONet u_data = self.u[index] y_data = self.y[index] s_data = self.s[index] - - # Debug: print shapes before processing - # print(f"DEBUG: u_data.shape={u_data.shape}, y_data.shape={y_data.shape}, s_data.shape={s_data.shape}") - - # Ensure y_data is 1D for DeepONet + + # Ensure y_data is 1D for DeepONet if len(y_data.shape) != 1: raise ValueError(f"y_data must be 1D, got shape {y_data.shape}") - - # Ensure s_data is 1D + + # Ensure s_data is 1D if len(s_data.shape) != 1: raise ValueError(f"s_data must be 1D, got shape {s_data.shape}") - + # Convert to MindSpore tensors with specified dtype u_tensor = ms.Tensor(u_data, self.dtype) y_tensor = ms.Tensor(y_data, self.dtype) s_tensor = ms.Tensor(s_data, self.dtype) - + return u_tensor, y_tensor, s_tensor - + def __len__(self): return self.len -def generate_synthetic_data(n_samples: int = 1000, n_sensors: int = 33, - n_points: int = 1, seed: int = 1234) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + +def generate_synthetic_data( + n_samples: int = 1000, n_sensors: int = 33, n_points: int = 1, seed: int = 1234 +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """Generate synthetic data for testing - + Args: n_samples: Number of samples n_sensors: Number of sensors n_points: Number of evaluation points seed: Random seed - + Returns: Tuple of (u, y, s) where: - u: Input function values at sensor locations @@ -137,26 +81,27 @@ def generate_synthetic_data(n_samples: int = 1000, n_sensors: int = 33, - s: True solution values """ np.random.seed(seed) - + # Generate input functions (u) - random functions u = np.random.randn(n_samples, n_sensors) - + # Generate evaluation points (y) y = np.random.rand(n_samples, n_points) * 2.0 # Random points in [0, 2] - + # Generate true solutions (s) - simple example using sin function s = np.sin(u.mean(axis=1, keepdims=True) * y) - + # Prepare data for DeepONet (expand to single query points) expanded_u, expanded_y, expanded_s, _ = prepare_deeponet_data(u, y, s) return expanded_u, expanded_y, expanded_s + def load_real_data(data_path: str) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """Load real data from npz file - + Args: data_path: Path to the npz file - + Returns: Tuple of (u, y, s) where: - u: Input function values at sensor locations @@ -165,195 +110,196 @@ def load_real_data(data_path: str) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """ if not os.path.exists(data_path): raise FileNotFoundError(f"Data file not found: {data_path}") - + data = np.load(data_path) - + # Check for required fields - required_fields = ['u', 'y', 's'] + required_fields = ["u", "y", "s"] for field in required_fields: if field not in data.files: raise ValueError(f"Required field '{field}' not found in data file") - - return data['u'], data['y'], data['s'] -def normalize_data_mindspore(u: np.ndarray, y: np.ndarray, s: np.ndarray, - method: str = 'standard', dtype: str = 'float32') -> Tuple[np.ndarray, np.ndarray, np.ndarray, Dict]: - """Normalize data using MindSpore operations - + return data["u"], data["y"], data["s"] + + +def normalize_data( + u: np.ndarray, + y: np.ndarray, + s: np.ndarray, + method: str = "standard", + dtype: str = "float32", +) -> Tuple[np.ndarray, np.ndarray, np.ndarray, Dict]: + """Normalize data + Args: u: Input function values y: Evaluation points s: Solution values method: Normalization method ('standard', 'minmax', 'none') - + Returns: Tuple of (u_norm, y_norm, s_norm, scalers) """ scalers = {} - - if method == 'none': + + if method == "none": return u, y, s, scalers - - if dtype == 'float32': + + if dtype == "float32": dtype = ms.float32 - elif dtype == 'float64': + elif dtype == "float64": dtype = ms.float64 - - # Convert to MindSpore tensors for computation + + # Convert to tensors for computation u_tensor = ms.Tensor(u, dtype) y_tensor = ms.Tensor(y, dtype) s_tensor = ms.Tensor(s, dtype) - - # MindSpore operations + + # Operations reduce_mean = ops.ReduceMean(keep_dims=True) reduce_std = ops.ReduceStd(keep_dims=True) reduce_min = ops.ReduceMin(keep_dims=True) reduce_max = ops.ReduceMax(keep_dims=True) - - if method == 'standard': + + if method == "standard": # Standard normalization: (x - mean) / std u_mean = reduce_mean(u_tensor, 0) u_std = reduce_std(u_tensor) u_norm = (u_tensor - u_mean) / (u_std + 1e-8) - + y_mean = reduce_mean(y_tensor, 0) y_std = reduce_std(y_tensor) y_norm = (y_tensor - y_mean) / (y_std + 1e-8) - + s_mean = reduce_mean(s_tensor, 0) s_std = reduce_std(s_tensor) s_norm = (s_tensor - s_mean) / (s_std + 1e-8) - + # Store scalers for later use - scalers['u'] = {'mean': u_mean.asnumpy(), 'std': u_std.asnumpy()} - scalers['y'] = {'mean': y_mean.asnumpy(), 'std': y_std.asnumpy()} - scalers['s'] = {'mean': s_mean.asnumpy(), 'std': s_std.asnumpy()} - - elif method == 'minmax': + scalers["u"] = {"mean": u_mean.asnumpy(), "std": u_std.asnumpy()} + scalers["y"] = {"mean": y_mean.asnumpy(), "std": y_std.asnumpy()} + scalers["s"] = {"mean": s_mean.asnumpy(), "std": s_std.asnumpy()} + + elif method == "minmax": # Min-max normalization: (x - min) / (max - min) u_min = reduce_min(u_tensor, 0) u_max = reduce_max(u_tensor, 0) u_norm = (u_tensor - u_min) / (u_max - u_min + 1e-8) - + y_min = reduce_min(y_tensor, 0) y_max = reduce_max(y_tensor, 0) y_norm = (y_tensor - y_min) / (y_max - y_min + 1e-8) - + s_min = reduce_min(s_tensor, 0) s_max = reduce_max(s_tensor, 0) s_norm = (s_tensor - s_min) / (s_max - s_min + 1e-8) - + # Store scalers for later use - scalers['u'] = {'min': u_min.asnumpy(), 'max': u_max.asnumpy()} - scalers['y'] = {'min': y_min.asnumpy(), 'max': y_max.asnumpy()} - scalers['s'] = {'min': s_min.asnumpy(), 'max': s_max.asnumpy()} - + scalers["u"] = {"min": u_min.asnumpy(), "max": u_max.asnumpy()} + scalers["y"] = {"min": y_min.asnumpy(), "max": y_max.asnumpy()} + scalers["s"] = {"min": s_min.asnumpy(), "max": s_max.asnumpy()} + # Convert back to numpy u_norm = u_norm.asnumpy() y_norm = y_norm.asnumpy() s_norm = s_norm.asnumpy() - + return u_norm, y_norm, s_norm, scalers -def split_data(u: np.ndarray, y: np.ndarray, s: np.ndarray, - train_split: float = 0.8, val_split: float = 0.1, - test_split: float = 0.1, random_state: int = 42) -> Dict[str, Tuple]: + +def split_data( + u: np.ndarray, + y: np.ndarray, + s: np.ndarray, + train_ratio: float = 0.8, + val_split: float = 0.1, + test_split: float = 0.1, + random_state: int = 42, +) -> Dict[str, Tuple]: """Split data into train, validation, and test sets - + Args: u: Input function values y: Evaluation points s: Solution values - train_split: Training set fraction + train_ratio: Training set fraction val_split: Validation set fraction test_split: Test set fraction random_state: Random seed - + Returns: Dictionary containing train, validation, and test data """ - assert abs(train_split + val_split + test_split - 1.0) < 1e-6, "Splits must sum to 1.0" - + assert ( + abs(train_ratio + val_split + test_split - 1.0) < 1e-6 + ), "Splits must sum to 1.0" + # Set random seed np.random.seed(random_state) - + n_samples = len(u) indices = np.random.permutation(n_samples) - - n_train = int(train_split * n_samples) + + n_train = int(train_ratio * n_samples) n_val = int(val_split * n_samples) - + train_indices = indices[:n_train] - val_indices = indices[n_train:n_train + n_val] - test_indices = indices[n_train + n_val:] - + val_indices = indices[n_train : n_train + n_val] + test_indices = indices[n_train + n_val :] + data_splits = { - 'train': (u[train_indices], y[train_indices], s[train_indices]), - 'val': (u[val_indices], y[val_indices], s[val_indices]), - 'test': (u[test_indices], y[test_indices], s[test_indices]) + "train": (u[train_indices], y[train_indices], s[train_indices]), + "val": (u[val_indices], y[val_indices], s[val_indices]), + "test": (u[test_indices], y[test_indices], s[test_indices]), } - + return data_splits -def create_mindspore_datasets(data_splits: Dict[str, Tuple], batch_size: int = 32) -> Dict[str, GeneratorDataset]: - """Create MindSpore datasets from data splits - + +def create_datasets( + data_splits: Dict[str, Tuple], batch_size: int = 32 +) -> Dict[str, GeneratorDataset]: + """Create datasets from data splits + Args: data_splits: Dictionary containing train, val, test data batch_size: Batch size for training - + Returns: - Dictionary containing MindSpore datasets + Dictionary containing datasets """ datasets = {} - + for split_name, (u, y, s) in data_splits.items(): # Create data generator data_gen = DataGenerator(u, y, s) - - # Create MindSpore dataset with three columns: u, y, s - if split_name == 'train': + + # Create dataset with three columns: u, y, s + if split_name == "train": dataset = GeneratorDataset( - source=data_gen, - column_names=["u", "y", "s"], - shuffle=True + source=data_gen, column_names=["u", "y", "s"], shuffle=True ).batch(batch_size) else: dataset = GeneratorDataset( - source=data_gen, - column_names=["u", "y", "s"], - shuffle=False + source=data_gen, column_names=["u", "y", "s"], shuffle=False ).batch(batch_size) - + + # set batch size datasets[split_name] = dataset - + return datasets -def save_data_analysis(data_path: str, output_path: str = None): - """Save data analysis to file - - Args: - data_path: Path to data file - output_path: Path to save analysis results - """ - if output_path is None: - output_path = data_path.replace('.npz', '_analysis.csv') - - stats_list = analyze_npz_data(data_path) - df = pd.DataFrame(stats_list) - df.to_csv(output_path, index=False) - print(f"Analysis saved to: {output_path}") def prepare_deeponet_data(u, y, s, time_points=None): """ Prepare data for DeepONet training from user's data format - + Args: u: Input functions, shape (n_samples, n_sensors) y: Time points, shape (n_samples, n_points) - this will be converted to individual query points s: Target values, shape (n_samples, n_points) time_points: Optional time points array, if None will use y as time points - + Returns: u_expanded: Expanded input functions, shape (n_samples * n_points, n_sensors) y_expanded: Individual query points, shape (n_samples * n_points, 1) @@ -366,129 +312,183 @@ def prepare_deeponet_data(u, y, s, time_points=None): s_n_samples, s_n_points = s.shape if y_n_samples != s_n_samples or n_samples != y_n_samples: - raise ValueError(f"u, y and s must have the same number of samples, got {n_samples}, {y_n_samples} and {s_n_samples}") + raise ValueError( + f"u, y and s must have the same number of samples, got {n_samples}, {y_n_samples} and {s_n_samples}" + ) if y_n_points != s_n_points: - raise ValueError(f"y and s must have the same number of points, got {y_n_points} and {s_n_points}") + raise ValueError( + f"y and s must have the same number of points, got {y_n_points} and {s_n_points}" + ) n_points = y_n_points # expand time points to batch dimension # from (n_samples, n_sensors) and (n_samples, n_points) # to (n_samples * n_points, n_sensors) and (n_samples * n_points, 1) - + # repeat u_batch to match each time point - u_expanded = np.repeat(u, n_points, axis=0) # (n_samples * n_points, n_sensors) - + # (n_samples * n_points, n_sensors) + u_expanded = np.repeat(u, n_points, axis=0) + # create query points for each time point of each sample y_expanded = np.tile(y.reshape(-1, 1), (n_samples, 1)) # (n_samples * n_points, 1) s_expanded = np.tile(s.reshape(-1, 1), (n_samples, 1)) # (n_samples * n_points, 1) - - + + # # Expand data for DeepONet training + # u_expanded = [] + # y_expanded = [] + # s_expanded = [] + + # for i in range(n_samples): + # for j in range(n_points): + # u_expanded.append(u[i]) # Same input function for all time points + # y_expanded.append([y[i][j]]) # Single time point - 确保是2D [value] + # s_expanded.append([s[i, j]]) # Target value at this time point - 确保是2D + # [value] + + # u_expanded = np.array(u_expanded) + # y_expanded = np.array(y_expanded) + # s_expanded = np.array(s_expanded) + metadata = { - 'n_samples': n_samples, - 'n_sensors': n_sensors, - 'n_points': n_points, - 'original_shapes': { - 'u': u.shape, - 'y': y.shape, - 's': s.shape + "n_samples": n_samples, + "n_sensors": n_sensors, + "n_points": n_points, + "original_shapes": {"u": u.shape, "y": y.shape, "s": s.shape}, + "expanded_shapes": { + "u": u_expanded.shape, + "y": y_expanded.shape, + "s": s_expanded.shape, }, - 'expanded_shapes': { - 'u': u_expanded.shape, - 'y': y_expanded.shape, - 's': s_expanded.shape - } } return u_expanded, y_expanded, s_expanded, metadata + def trajectory_prediction(u: np.ndarray, time_points: np.ndarray, model) -> np.ndarray: """ Predict trajectory for a single sample using DeepONet model - + Args: u: Input function values for single sample, shape (n_sensors,) time_points: Time points to predict, shape (n_time_points,) model: Trained DeepONet model - + Returns: predictions: Predicted values at time points, shape (n_time_points, 1) """ - + # Ensure u is 2D for model input if len(u.shape) == 1: u = u.reshape(1, -1) # (1, n_sensors) - + # Convert to MindSpore tensor u_tensor = ms.Tensor(u, ms.float32) - + predictions = [] - + # Predict for each time point for t in time_points: # Create single query point y_t = ms.Tensor([[t]], ms.float32) # (1, 1) - + # Forward pass mean_pred, log_std_pred = model(u_tensor, y_t) - + # Get prediction value pred_val = float(mean_pred[0, 0]) predictions.append(pred_val) - + return np.array(predictions).reshape(-1, 1) + def batch_trajectory_prediction(model, u_batch, time_points): """ Batch prediction of trajectories - + Args: model: Trained DeepONet model u_batch: Input function batch, shape (batch_size, n_sensors) time_points: Time points array, shape (n_time_points,) - + Returns: mean_predictions: Predicted mean, shape (batch_size, n_time_points, 1) + std_predictions: Predicted standard deviation, shape (batch_size, n_time_points, 1) """ batch_size = u_batch.shape[0] n_time_points = len(time_points) - + # expand time points to batch dimension # from (batch_size, n_sensors) and (n_time_points,) # to (batch_size * n_time_points, n_sensors) and (batch_size * n_time_points, 1) - + # repeat u_batch to match each time point - u_expanded = np.repeat(u_batch, n_time_points, axis=0) # (batch_size * n_time_points, n_sensors) - + u_expanded = np.repeat( + u_batch, n_time_points, axis=0 + ) # (batch_size * n_time_points, n_sensors) + # create query points for each time point of each sample - y_expanded = np.tile(time_points.reshape(-1, 1), (batch_size, 1)) # (batch_size * n_time_points, 1) - + y_expanded = np.tile( + time_points.reshape(-1, 1), (batch_size, 1) + ) # (batch_size * n_time_points, 1) + # convert to MindSpore tensors u_tensor = ms.Tensor(u_expanded, ms.float32) y_tensor = ms.Tensor(y_expanded, ms.float32) - + # one inference to get all predictions - mean_pred, _ = model(u_tensor, y_tensor) - + mean_pred, log_std_pred = model(u_tensor, y_tensor) + # reshape results mean_predictions = mean_pred.reshape(batch_size, n_time_points, 1) - - return mean_predictions - - -if __name__ == "__main__": - import argparse - parser = argparse.ArgumentParser(description='Data loading and preprocessing utilities') - parser.add_argument('--data_path', type=str, required=True, - help='Path to the npz data file') - parser.add_argument('--analyze', action='store_true', - help='Analyze data and save results') - args = parser.parse_args() - - if args.analyze: - analyze_npz_data(args.data_path) - save_data_analysis(args.data_path) - else: - # Test data loading - u, y, s = load_real_data(args.data_path) - print(f"Loaded data: u shape {u.shape}, y shape {y.shape}, s shape {s.shape}") \ No newline at end of file + std_predictions = ms.ops.exp(log_std_pred).reshape(batch_size, n_time_points, 1) + + return mean_predictions, std_predictions + + +def load_and_preprocess_real_data(config: dict): + # Find training data + data_path = config["data"]["data_path"] + + # Load raw data + u, y, s = load_real_data(data_path) + + # Prepare data for DeepONet (convert multi-time-point to single query + # points) + u_expanded, y_expanded, s_expanded, prep_metadata = prepare_deeponet_data(u, y, s) + + # Normalize data + # logger.info("Normalizing data...") + # u_norm, y_norm, s_norm, scalers = normalize_data(u_expanded, y_expanded, s_expanded, method='standard') + + # Split data + data_splits = split_data( + u_expanded, + y_expanded, + s_expanded, + train_ratio=config["data"]["train_ratio"], + val_split=config["data"]["val_split"], + test_split=config["data"]["test_split"], + ) + + # Create MindSpore datasets + datasets = create_datasets(data_splits, batch_size=config["training"]["batch_size"]) + + # Prepare metadata + metadata = { + "input_dim": u_expanded.shape[-1], + "output_dim": s_expanded.shape[-1], + "n_samples": len(u_expanded), + "n_sensors": u.shape[-1], # Original sensor count + "data_shapes": { + "u": u_expanded.shape, + "y": y_expanded.shape, + "s": s_expanded.shape, + }, + } + + # Add preparation metadata + if prep_metadata: + metadata.update(prep_metadata) + + return datasets, metadata diff --git a/MindEnergy/application/deeponet-grid/src/metrics.py b/MindEnergy/application/deeponet-grid/src/metrics.py index 377983e4a..e93a92acb 100644 --- a/MindEnergy/application/deeponet-grid/src/metrics.py +++ b/MindEnergy/application/deeponet-grid/src/metrics.py @@ -1,59 +1,80 @@ -#!/usr/bin/env python3 +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== """ Evaluation metrics for DeepONet-Grid-UQ """ +from typing import Any, Dict, List, Optional, Tuple + import mindspore as ms -import mindspore.ops as ops import mindspore.numpy as mnp +import mindspore.ops as ops import numpy as np -from typing import List, Tuple, Dict, Any, Optional + class MetricsCalculator: """Metrics calculator for DeepONet evaluation""" - + def __init__(self): self.norm = ops.LpNorm(axis=-1, keep_dims=False) self.norm_l1 = ops.LpNorm(axis=-1, keep_dims=False, p=1) self.reduce_mean = ops.ReduceMean() self.abs = ops.Abs() self.exp = ops.Exp() - + def l2_relative_error(self, y_true: ms.Tensor, y_pred: ms.Tensor) -> float: diff = (y_true - y_pred).reshape(-1) true = y_true.reshape(-1) numerator = ops.norm(diff, ord=2) denominator = ops.norm(true, ord=2) value = numerator / denominator - if hasattr(value, 'asnumpy'): + if hasattr(value, "asnumpy"): value = value.asnumpy() - if hasattr(value, 'item'): + if hasattr(value, "item"): value = value.item() return float(value) - + def l1_relative_error(self, y_true: ms.Tensor, y_pred: ms.Tensor) -> float: diff = (y_true - y_pred).reshape(-1) true = y_true.reshape(-1) numerator = ops.norm(diff, ord=1) denominator = ops.norm(true, ord=1) value = numerator / denominator - if hasattr(value, 'asnumpy'): + if hasattr(value, "asnumpy"): value = value.asnumpy() - if hasattr(value, 'item'): + if hasattr(value, "item"): value = value.item() return float(value) - - def fraction_in_CI(self, s: ms.Tensor, s_mean: ms.Tensor, s_std: ms.Tensor, - xi: float = 2.0, verbose: bool = False) -> float: + + def fraction_in_CI( + self, + s: ms.Tensor, + s_mean: ms.Tensor, + s_std: ms.Tensor, + xi: float = 2.0, + verbose: bool = False, + ) -> float: """Compute fraction of true trajectory in predicted confidence interval - + Args: s: True values s_mean: Predicted mean s_std: Predicted standard deviation xi: Confidence interval multiplier (default: 2.0 for 95% CI) verbose: Whether to print results - + Returns: Fraction of points within confidence interval """ @@ -61,91 +82,106 @@ class MetricsCalculator: s = s.reshape(-1) s_mean = s_mean.reshape(-1) s_std = s_std.reshape(-1) - + # Check if points are within confidence interval within_CI = self.abs(s - s_mean) <= xi * s_std ratio = float(self.reduce_mean(within_CI.astype(ms.float32))) - + if verbose: print(f"% of the true traj. within the error bars is {100 * ratio:.3f}") - + return ratio - - def trajectory_rel_error(self, s_true: ms.Tensor, s_pred: ms.Tensor, - verbose: bool = False) -> Tuple[float, float]: + + def trajectory_rel_error( + self, s_true: ms.Tensor, s_pred: ms.Tensor, verbose: bool = False + ) -> Tuple[float, float]: """Compute trajectory relative errors - + Args: s_true: True trajectory s_pred: Predicted trajectory verbose: Whether to print results - + Returns: Tuple of (L1_error, L2_error) """ s_true_flat = s_true.reshape(-1) s_pred_flat = s_pred.reshape(-1) - + l1_error = self.l1_relative_error(s_true_flat, s_pred_flat) l2_error = self.l2_relative_error(s_true_flat, s_pred_flat) - + if verbose: print(f"The L1 relative error is {l1_error:.5f}") print(f"The L2 relative error is {l2_error:.5f}") - + return l1_error, l2_error -def compute_metrics(s_true: List[ms.Tensor], s_pred: List[ms.Tensor], - metrics: List[str], verbose: bool = False) -> List[List[float]]: + +def compute_metrics( + s_true: List[ms.Tensor], + s_pred: List[ms.Tensor], + metrics: List[str], + verbose: bool = False, +) -> List[List[float]]: """Compute metrics for multiple trajectories - + Args: s_true: List of true trajectories s_pred: List of predicted trajectories metrics: List of metric names ('l1', 'l2') verbose: Whether to print results - + Returns: List of [max, min, mean] for each metric """ calculator = MetricsCalculator() out = [] - + for metric_name in metrics: temp = [] for k in range(len(s_true)): - if metric_name.lower() == 'l1': + if metric_name.lower() == "l1": error = calculator.l1_relative_error(s_true[k], s_pred[k]) - elif metric_name.lower() == 'l2': + elif metric_name.lower() == "l2": error = calculator.l2_relative_error(s_true[k], s_pred[k]) else: raise ValueError(f"Unsupported metric: {metric_name}") temp.append(error) - + # Convert to numpy for statistics temp_np = np.array(temp) - out.append([ - np.round(100 * np.max(temp_np), decimals=5), - np.round(100 * np.min(temp_np), decimals=5), - np.round(100 * np.mean(temp_np), decimals=5), - ]) - + out.append( + [ + np.round(100 * np.max(temp_np), decimals=5), + np.round(100 * np.min(temp_np), decimals=5), + np.round(100 * np.mean(temp_np), decimals=5), + ] + ) + if verbose: try: - print(f"l1-relative errors: max={out[0][0]:.3f}, min={out[0][1]:.3f}, mean={out[0][2]:.3f}") - print(f"l2-relative errors: max={out[1][0]:.3f}, min={out[1][1]:.3f}, mean={out[1][2]:.3f}") - except: + print( + f"l1-relative errors: max={out[0][0]:.3f}, min={out[0][1]:.3f}, mean={out[0][2]:.3f}" + ) + print( + f"l2-relative errors: max={out[1][0]:.3f}, min={out[1][1]:.3f}, mean={out[1][2]:.3f}" + ) + except BaseException: print("not the correct metrics") - + return out -def update_metrics_history(history: Dict[str, List[float]], state: List[float]) -> Dict[str, List[float]]: + +def update_metrics_history( + history: Dict[str, List[float]], state: List[float] +) -> Dict[str, List[float]]: """Update metrics history - + Args: history: Current history dictionary state: New state [max, min, mean] - + Returns: Updated history """ @@ -155,90 +191,95 @@ def update_metrics_history(history: Dict[str, List[float]], state: List[float]) history["min"] = [] if "mean" not in history: history["mean"] = [] - + history["max"].append(state[0]) history["min"].append(state[1]) history["mean"].append(state[2]) - + return history -def compute_calibration_error(s_true: ms.Tensor, s_mean: ms.Tensor, s_std: ms.Tensor, - n_bins: int = 10) -> float: + +def compute_calibration_error( + s_true: ms.Tensor, s_mean: ms.Tensor, s_std: ms.Tensor, n_bins: int = 10 +) -> float: """Compute calibration error for uncertainty quantification - + Args: s_true: True values s_mean: Predicted mean s_std: Predicted standard deviation n_bins: Number of bins for calibration - + Returns: Calibration error """ # Normalize residuals residuals = (s_true - s_mean) / s_std - + # Compute empirical CDF sorted_residuals = ops.Sort()(residuals.reshape(-1))[0] n_points = sorted_residuals.shape[0] - + # Create bins bin_edges = mnp.linspace(0, 1, n_bins + 1) bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2 - + # Compute empirical probabilities empirical_probs = [] for i in range(n_bins): start_idx = int(bin_edges[i] * n_points) end_idx = int(bin_edges[i + 1] * n_points) empirical_probs.append((end_idx - start_idx) / n_points) - + empirical_probs = mnp.array(empirical_probs) - + # Compute theoretical probabilities (uniform for well-calibrated model) theoretical_probs = mnp.ones(n_bins) / n_bins - + # Compute calibration error calibration_error = ops.ReduceSum()((empirical_probs - theoretical_probs) ** 2) - + return float(calibration_error) + def compute_r2_score(y_true: ms.Tensor, y_pred: ms.Tensor) -> float: """Compute R² score - + Args: y_true: True values y_pred: Predicted values - + Returns: R² score """ ss_res = ops.ReduceSum()((y_true - y_pred) ** 2) ss_tot = ops.ReduceSum()((y_true - ops.ReduceMean()(y_true)) ** 2) - + r2 = 1 - ss_res / ss_tot return float(r2) + def compute_mae(y_true: ms.Tensor, y_pred: ms.Tensor) -> float: """Compute Mean Absolute Error - + Args: y_true: True values y_pred: Predicted values - + Returns: MAE value """ mae = ops.ReduceMean()(ops.Abs()(y_true - y_pred)) return float(mae) + def compute_mse(y_true: ms.Tensor, y_pred: ms.Tensor) -> float: """Compute Mean Squared Error - + Args: y_true: True values y_pred: Predicted values - + Returns: MSE value """ diff --git a/MindEnergy/application/deeponet-grid/src/model.py b/MindEnergy/application/deeponet-grid/src/model.py index fe742f628..87af63ffc 100644 --- a/MindEnergy/application/deeponet-grid/src/model.py +++ b/MindEnergy/application/deeponet-grid/src/model.py @@ -1,9 +1,24 @@ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +from typing import Any + import mindspore as ms -from mindspore import nn, mint, Parameter, ops import mindspore.numpy as np -from mindspore.common.initializer import initializer, XavierNormal, Zero +from mindspore import Parameter, mint, nn, ops +from mindspore.common.initializer import XavierNormal, Zero, initializer -from typing import Any # MLP class MLP(nn.Cell): @@ -11,43 +26,52 @@ class MLP(nn.Cell): super(MLP, self).__init__() layers = [] for k in range(len(layer_size) - 2): - layers.append(nn.Dense(layer_size[k], layer_size[k+1], has_bias=True)) + layers.append(nn.Dense(layer_size[k], layer_size[k + 1], has_bias=True)) layers.append(get_activation(activation)) layers.append(nn.Dense(layer_size[-2], layer_size[-1], has_bias=True)) self.net = nn.SequentialCell(layers) self.net.apply(self._init_weights) - + def _init_weights(self, m: Any) -> None: if isinstance(m, nn.Dense): - m.weight.set_data(initializer(XavierNormal(), m.weight.shape, m.weight.dtype)) + m.weight.set_data( + initializer(XavierNormal(), m.weight.shape, m.weight.dtype) + ) m.bias.set_data(initializer(Zero(), m.bias.shape, m.bias.dtype)) - - def construct(self, x: ms.Tensor) -> ms.Tensor: + + def construct(self, x: ms.Tensor): return self.net(x) - + + # modified MLP class modified_MLP(nn.Cell): def __init__(self, layer_size: list, activation: str) -> None: super(modified_MLP, self).__init__() layers = [] for k in range(len(layer_size) - 1): - layers.append(nn.Dense(layer_size[k], layer_size[k+1], has_bias=True)) + layers.append(nn.Dense(layer_size[k], layer_size[k + 1], has_bias=True)) self.net = nn.SequentialCell(layers) - - self.U = nn.SequentialCell([nn.Dense(layer_size[0], layer_size[1], has_bias=True)]) - self.V = nn.SequentialCell([nn.Dense(layer_size[0], layer_size[1], has_bias=True)]) + + self.U = nn.SequentialCell( + [nn.Dense(layer_size[0], layer_size[1], has_bias=True)] + ) + self.V = nn.SequentialCell( + [nn.Dense(layer_size[0], layer_size[1], has_bias=True)] + ) self.activation = get_activation(activation) - + self.net.apply(self._init_weights) self.U.apply(self._init_weights) self.V.apply(self._init_weights) - + def _init_weights(self, m: Any) -> None: if isinstance(m, nn.Dense): - m.weight.set_data(initializer(XavierNormal(), m.weight.shape, m.weight.dtype)) - m.bias.set_data(initializer(Zero(), m.bias.shape, m.bias.dtype)) - - def construct(self, x: ms.Tensor) -> ms.Tensor: + m.weight.set_data( + initializer(XavierNormal(), m.weight.shape, m.weight.dtype) + ) + m.bias.set_data(initializer(Zero(), m.bias.shape, m.bias.dtype)) + + def construct(self, x: ms.Tensor): u = self.activation(self.U(x)) v = self.activation(self.V(x)) for k in range(len(self.net) - 1): @@ -57,30 +81,32 @@ class modified_MLP(nn.Cell): y = self.net[-1](x) return y + # get activation function from str def get_activation(identifier: str) -> Any: """get activation function.""" - return{ - "elu": nn.ELU(), - "relu": nn.ReLU(), - "selu": nn.SeLU(), - "sigmoid": nn.Sigmoid(), - "leaky": nn.LeakyReLU(), - "tanh": nn.Tanh(), - "sin": sin_act(), - # "softplus": nn.Softplus(), - "Rrelu": nn.RReLU(), - "gelu": nn.GELU(), - "silu": nn.SiLU(), - "Mish": nn.Mish(), + return { + "elu": nn.ELU(), + "relu": nn.ReLU(), + "selu": nn.SeLU(), + "sigmoid": nn.Sigmoid(), + "leaky": nn.LeakyReLU(), + "tanh": nn.Tanh(), + "sin": sin_act(), + # "softplus": nn.Softplus(), + "Rrelu": nn.RReLU(), + "gelu": nn.GELU(), + "silu": nn.SiLU(), + "Mish": nn.Mish(), }[identifier] + # sin activation function class sin_act(nn.Cell): def __init__(self): super(sin_act, self).__init__() - def construct(self, x: ms.Tensor) -> ms.Tensor: + def construct(self, x: ms.Tensor): return mint.sin(x) @@ -88,7 +114,8 @@ class DeepONet(nn.Cell): """ Base DeepONet class that serves as an interface for different DeepONet implementations. """ - def __init__(self, branch: dict, trunk: dict, use_bias: bool=True) -> None: + + def __init__(self, branch: dict, trunk: dict, use_bias: bool = True) -> None: super(DeepONet, self).__init__() # Branch if branch["type"] == "MLP": @@ -96,31 +123,37 @@ class DeepONet(nn.Cell): elif branch["type"] == "modified": self.branch = modified_MLP(branch["layer_size"][:-2], branch["activation"]) else: - raise ValueError(f"Unsupported branch type: {branch['type']}. Supported: 'MLP', 'modified'.") + raise ValueError( + f"Unsupported branch type: {branch['type']}. Supported: 'MLP', 'modified'." + ) # Trunk if trunk["type"] == "MLP": self.trunk = MLP(trunk["layer_size"][:-2], trunk["activation"]) elif trunk["type"] == "modified": self.trunk = modified_MLP(trunk["layer_size"][:-2], trunk["activation"]) else: - raise ValueError(f"Unsupported trunk type: {trunk['type']}. Supported: 'MLP', 'modified'.") + raise ValueError( + f"Unsupported trunk type: {trunk['type']}. Supported: 'MLP', 'modified'." + ) self.use_bias = use_bias def _init_weights(self, m): """Initialize weights for dense layers.""" if isinstance(m, nn.Dense): - m.weight.set_data(initializer(XavierNormal(), m.weight.shape, m.weight.dtype)) + m.weight.set_data( + initializer(XavierNormal(), m.weight.shape, m.weight.dtype) + ) m.bias.set_data(initializer(Zero(), m.bias.shape, m.bias.dtype)) def construct(self, xu, xy): """ Forward pass interface. To be implemented by subclasses. - + Args: xu: Branch input tensor xy: Trunk input tensor - + Returns: Model output (to be defined by subclasses) """ @@ -128,41 +161,69 @@ class DeepONet(nn.Cell): class Prob_DeepONet(DeepONet): - def __init__(self, branch: dict, trunk: dict, use_bias: bool=True) -> None: + def __init__(self, branch: dict, trunk: dict, use_bias: bool = True) -> None: super(Prob_DeepONet, self).__init__(branch, trunk, use_bias) - + # Add probabilistic components if use_bias: - self.bias_mu = Parameter(ms.Tensor(np.randn(1), ms.float32), requires_grad=True) - self.bias_std = Parameter(ms.Tensor(np.randn(1), ms.float32), requires_grad=True) - - self.branch_mu = nn.SequentialCell([ - get_activation(branch["activation"]), - nn.Dense(branch["layer_size"][-3], branch["layer_size"][-2], has_bias=True), - get_activation(branch["activation"]), - nn.Dense(branch["layer_size"][-2], branch["layer_size"][-1], has_bias=True) - ]) - - self.branch_std = nn.SequentialCell([ - get_activation(branch["activation"]), - nn.Dense(branch["layer_size"][-3], branch["layer_size"][-2], has_bias=True), - get_activation(branch["activation"]), - nn.Dense(branch["layer_size"][-2], branch["layer_size"][-1], has_bias=True) - ]) - - self.trunk_mu = nn.SequentialCell([ - get_activation(trunk["activation"]), - nn.Dense(trunk["layer_size"][-3], trunk["layer_size"][-2], has_bias=True), - get_activation(trunk["activation"]), - nn.Dense(trunk["layer_size"][-2], trunk["layer_size"][-1], has_bias=True) - ]) - - self.trunk_std = nn.SequentialCell([ - get_activation(trunk["activation"]), - nn.Dense(trunk["layer_size"][-3], trunk["layer_size"][-2], has_bias=True), - get_activation(trunk["activation"]), - nn.Dense(trunk["layer_size"][-2], trunk["layer_size"][-1], has_bias=True) - ]) + self.bias_mu = Parameter( + ms.Tensor(np.randn(1), ms.float32), requires_grad=True + ) + self.bias_std = Parameter( + ms.Tensor(np.randn(1), ms.float32), requires_grad=True + ) + + self.branch_mu = nn.SequentialCell( + [ + get_activation(branch["activation"]), + nn.Dense( + branch["layer_size"][-3], branch["layer_size"][-2], has_bias=True + ), + get_activation(branch["activation"]), + nn.Dense( + branch["layer_size"][-2], branch["layer_size"][-1], has_bias=True + ), + ] + ) + + self.branch_std = nn.SequentialCell( + [ + get_activation(branch["activation"]), + nn.Dense( + branch["layer_size"][-3], branch["layer_size"][-2], has_bias=True + ), + get_activation(branch["activation"]), + nn.Dense( + branch["layer_size"][-2], branch["layer_size"][-1], has_bias=True + ), + ] + ) + + self.trunk_mu = nn.SequentialCell( + [ + get_activation(trunk["activation"]), + nn.Dense( + trunk["layer_size"][-3], trunk["layer_size"][-2], has_bias=True + ), + get_activation(trunk["activation"]), + nn.Dense( + trunk["layer_size"][-2], trunk["layer_size"][-1], has_bias=True + ), + ] + ) + + self.trunk_std = nn.SequentialCell( + [ + get_activation(trunk["activation"]), + nn.Dense( + trunk["layer_size"][-3], trunk["layer_size"][-2], has_bias=True + ), + get_activation(trunk["activation"]), + nn.Dense( + trunk["layer_size"][-2], trunk["layer_size"][-1], has_bias=True + ), + ] + ) self.branch_mu.apply(self._init_weights) self.branch_std.apply(self._init_weights) @@ -171,10 +232,12 @@ class Prob_DeepONet(DeepONet): def _init_weights(self, m): if isinstance(m, nn.Dense): - m.weight.set_data(initializer(XavierNormal(), m.weight.shape, m.weight.dtype)) + m.weight.set_data( + initializer(XavierNormal(), m.weight.shape, m.weight.dtype) + ) m.bias.set_data(initializer(Zero(), m.bias.shape, m.bias.dtype)) - def construct(self, xu, xy) -> list: + def construct(self, xu, xy): u, y = xu, xy b = self.branch(u) t = self.trunk(y) @@ -189,36 +252,10 @@ class Prob_DeepONet(DeepONet): # Use ReduceSum operation for proper reduction reduce_sum = ops.ReduceSum(keep_dims=True) mu = reduce_sum(b_mu * t_mu, 1) # Reduce along feature dimension - log_std = reduce_sum(b_std * t_std, 1) # Reduce along feature dimension - + # Reduce along feature dimension + log_std = reduce_sum(b_std * t_std, 1) + if self.use_bias: mu += self.bias_mu log_std += self.bias_std return (mu, log_std) - - -# test codes for Prob_DeepONet -if __name__ == "__main__": - branch = { - "type": "modified", - "layer_size": [33, 200, 200, 200, 100], - "activation": "sin" - } - trunk = { - "type": "modified", - "layer_size": [100, 200, 200, 200, 100], - "activation": "sin" - } - use_bias = True - - model = Prob_DeepONet(branch, trunk, use_bias) - # xu = ms.Tensor(np.ones((10, 33)), ms.float32) - # xy = ms.Tensor(np.ones((10, 100)), ms.float32) - xu = ms.Tensor(np.randn(10, 33), ms.float32) - xy = ms.Tensor(np.randn(10, 100), ms.float32) - mu, log_std = model(xu, xy) - print(mu) - print(log_std) - - - \ No newline at end of file diff --git a/MindEnergy/application/deeponet-grid/src/trainer.py b/MindEnergy/application/deeponet-grid/src/trainer.py index fc81cc13f..99448c067 100644 --- a/MindEnergy/application/deeponet-grid/src/trainer.py +++ b/MindEnergy/application/deeponet-grid/src/trainer.py @@ -1,57 +1,82 @@ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +import os +import time +from typing import Any, Dict, List, Optional, Tuple + import mindspore as ms import mindspore.nn as nn +import mindspore.numpy as mnp import mindspore.ops as ops -from mindspore import context, Parameter +import numpy as np +import yaml +from mindspore import Parameter, context +from mindspore.common.initializer import XavierNormal, Zero, initializer from mindspore.dataset import GeneratorDataset -import mindspore.numpy as mnp -from mindspore.train.callback import Callback, LossMonitor, TimeMonitor from mindspore.train import Model -from mindspore.train.loss_scale_manager import DynamicLossScaleManager from mindspore.train.amp import auto_mixed_precision -from mindspore.common.initializer import initializer, XavierNormal, Zero -import numpy as np -from typing import Any, List, Tuple, Optional, Dict -import os -import yaml -import time +from mindspore.train.callback import Callback, LossMonitor, TimeMonitor +from mindspore.train.loss_scale_manager import DynamicLossScaleManager from tqdm.auto import trange -import logging - # Import metrics -from .metrics import compute_metrics, update_metrics_history, compute_calibration_error, compute_r2_score, compute_mae, compute_mse +from .metrics import ( + compute_calibration_error, + compute_mae, + compute_metrics, + compute_mse, + compute_r2_score, + update_metrics_history, +) # Set up logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) + class CustomCallback(Callback): """Custom callback for training monitoring""" + def __init__(self, print_every: int = 10, save_every: int = 100): super(CustomCallback, self).__init__() self.print_every = print_every self.save_every = save_every self.step = 0 self.losses = [] - + def step_end(self, run_context): """Called at the end of each step""" self.step += 1 cb_params = run_context.original_args() loss = cb_params.net_outputs - + if isinstance(loss, (list, tuple)): loss = loss[0] - + self.losses.append(float(loss)) - + if self.step % self.print_every == 0: logger.info(f"Step {self.step}, Loss: {float(loss):.6f}") + class ProbabilisticLoss(nn.Cell): """Negative log likelihood loss for probabilistic DeepONet Equivalent to torch.distributions.Normal(mean_pred, torch.exp(log_std_pred)).log_prob(target).mean() """ + def __init__(self): super(ProbabilisticLoss, self).__init__() self.log = ops.Log() @@ -59,253 +84,194 @@ class ProbabilisticLoss(nn.Cell): self.square = ops.Square() self.reduce_mean = ops.ReduceMean() self.log_2pi = np.log(2 * np.pi) - + def construct(self, mean_pred, log_std_pred, target): """Compute negative log likelihood loss using normal distribution - + Args: mean_pred: Predicted mean log_std_pred: Predicted log standard deviation target: Target values - + Returns: Negative mean log probability """ # Convert log_std to std std_pred = self.exp(log_std_pred) - + # Compute log probability of normal distribution # log_prob = -0.5 * ((x - mu) / sigma)^2 - log(sigma) - 0.5 * log(2*pi) - log_prob = -0.5 * self.square((target - mean_pred) / std_pred) - log_std_pred - 0.5 * self.log_2pi - - # Return negative mean log probability (equivalent to -dist.log_prob(target).mean()) + log_prob = ( + -0.5 * self.square((target - mean_pred) / std_pred) + - log_std_pred + - 0.5 * self.log_2pi + ) + + # Return negative mean log probability (equivalent to + # -dist.log_prob(target).mean()) return -self.reduce_mean(log_prob) + class MSELoss(nn.Cell): """Mean squared error loss""" + def __init__(self): super(MSELoss, self).__init__() self.mse = nn.MSELoss() - + def construct(self, mean_pred, log_std_pred, target): """Compute MSE loss (ignores uncertainty predictions) - + Args: mean_pred: Predicted mean log_std_pred: Predicted log standard deviation (ignored) target: Target values - + Returns: MSE loss value """ return self.mse(mean_pred, target) + class DeepONetTrainer: """Trainer class for DeepONet with uncertainty quantification""" - def __init__(self, - model: nn.Cell, - config: Dict[str, Any], - save_dir: str = "outputs"): - + + def __init__( + self, model: nn.Cell, config: Dict[str, Any], save_dir: str = "outputs" + ): + self.model = model self.config = config self.save_dir = save_dir - self.training_config = config['training'] - + self.training_config = config["training"] + # Create save directory os.makedirs(save_dir, exist_ok=True) - + # Initialize optimizer - self.optimizer = self._create_optimizer() - - # Initialize loss function - self.loss_fn = self._create_loss_function() - - # Initialize learning rate scheduler - self.scheduler = self._create_scheduler() - - # Initialize metrics - self.metrics = { - 'train_loss': [], - 'val_loss': [], - 'best_loss': float('inf'), - 'patience_counter': 0 - } - - # Set up device context - self._setup_device() - - def _setup_device(self): - """Set up device context""" - device_config = self.config['device'] - context.set_context( - mode=getattr(context, device_config['mode']), - device_target=device_config['target'] - ) - logger.info(f"Device set to: {device_config['target']}, Mode: {device_config['mode']}") - - def _create_optimizer(self) -> nn.Optimizer: - """Create optimizer based on configuration""" - optimizer_type = self.training_config.get('optimizer', 'adam') - learning_rate = float(self.training_config['learning_rate']) # Ensure it's a float - weight_decay = self.training_config.get('weight_decay', 0.0) - - if optimizer_type.lower() == 'adam': - return nn.Adam( - self.model.trainable_params(), - learning_rate=learning_rate, - weight_decay=weight_decay - ) - elif optimizer_type.lower() == 'sgd': - return nn.SGD( - self.model.trainable_params(), - learning_rate=learning_rate, - weight_decay=weight_decay - ) - elif optimizer_type.lower() == 'adamw': - return nn.AdamWeightDecay( + optimizer_type = self.training_config.get("optimizer", "adam") + if self.training_config.get("scheduler", None) is "cosine": + min_lr = self.training_config.get("min_lr", 1e-7) + max_lr = self.training_config.get("max_lr", 1e-3) + total_step = self.training_config.get("total_step", 10000) + step_per_epoch = self.training_config.get("step_per_epoch", 1000) + decay_epoch = self.training_config.get("decay_epoch", 1000) + learning_rate = nn.cosine_decay_lr( + min_lr, max_lr, total_step, step_per_epoch, decay_epoch + ) # Ensure it's a float + else: + learning_rate = float( + self.training_config["learning_rate"] + ) # Ensure it's a float + weight_decay = self.training_config.get("weight_decay", 0.0) + + if optimizer_type.lower() == "adam": + self.optimizer = nn.Adam( self.model.trainable_params(), learning_rate=learning_rate, - weight_decay=weight_decay + weight_decay=weight_decay, ) else: raise ValueError(f"Unsupported optimizer: {optimizer_type}") - - def _create_loss_function(self) -> nn.Cell: - """Create loss function based on configuration""" - loss_type = self.training_config.get('loss_type', 'nll') - - if loss_type.lower() == 'nll': - return ProbabilisticLoss() - elif loss_type.lower() == 'mse': - return MSELoss() + + # Initialize loss function + loss_type = self.training_config.get("loss_type", "nll") + + if loss_type.lower() == "nll": + self.loss_fn = ProbabilisticLoss() + elif loss_type.lower() == "mse": + self.loss_fn = MSELoss() else: raise ValueError(f"Unsupported loss type: {loss_type}") - - def _create_scheduler(self) -> Optional[Any]: - """Create learning rate scheduler""" - scheduler_type = self.training_config.get('scheduler', None) - - if scheduler_type is None: - return None - - if scheduler_type == 'reduce_lr_on_plateau': - # Custom implementation for reduce LR on plateau - return ReduceLROnPlateau( - self.optimizer, - mode='min', - patience=self.training_config.get('patience', 10), - factor=self.training_config.get('factor', 0.8), - verbose=self.training_config.get('verbose', True) - ) - elif scheduler_type == 'cosine': - return nn.CosineAnnealingLR( - self.optimizer, - T_max=self.training_config['epochs'], - eta_min=self.training_config.get('min_lr', 1e-7) - ) - elif scheduler_type == 'step': - return nn.StepLR( - self.optimizer, - step_size=self.training_config.get('step_size', 100), - gamma=self.training_config.get('factor', 0.8) - ) - else: - logger.warning(f"Unsupported scheduler: {scheduler_type}") - return None - - def _reduce_lr_on_plateau(self, val_loss: float): - """Reduce learning rate if validation loss plateaus""" - if val_loss < self.metrics['best_loss']: - self.metrics['best_loss'] = val_loss - self.metrics['patience_counter'] = 0 - else: - self.metrics['patience_counter'] += 1 - - if self.metrics['patience_counter'] >= self.training_config['patience']: - # Reduce learning rate - current_lr = float(self.optimizer.learning_rate) # Ensure it's a float - factor = float(self.training_config['factor']) # Ensure it's a float - min_lr = float(self.training_config.get('min_lr', 1e-7)) # Ensure it's a float - new_lr = max(current_lr * factor, min_lr) - - if new_lr < current_lr: - self.optimizer.learning_rate.set_data(ms.Tensor(new_lr, ms.float32)) - logger.info(f"Reducing learning rate to {new_lr:.2e}") - self.metrics['patience_counter'] = 0 - - def train_step(self, u: ms.Tensor, y: ms.Tensor, - target: ms.Tensor) -> Tuple[ms.Tensor, ms.Tensor, ms.Tensor]: + + # Initialize metrics + self.metrics = { + "train_loss": [], + "val_loss": [], + "best_loss": float("inf"), + "patience_counter": 0, + } + + def train_step( + self, u: ms.Tensor, y: ms.Tensor, target: ms.Tensor + ) -> Tuple[ms.Tensor, ms.Tensor, ms.Tensor]: """Single training step - + Args: u: Input function values y: Evaluation points target: Target values - + Returns: Tuple of (loss, mean_pred, log_std_pred) """ + def forward_fn(): mean_pred, log_std_pred = self.model(u, y) loss = self.loss_fn(mean_pred, log_std_pred, target) return loss, mean_pred, log_std_pred - - grad_fn = ops.value_and_grad(forward_fn, None, self.optimizer.parameters, - has_aux=True) - + + grad_fn = ops.value_and_grad( + forward_fn, None, self.optimizer.parameters, has_aux=True + ) + (loss, mean_pred, log_std_pred), grads = grad_fn() self.optimizer(grads) - + return loss, mean_pred, log_std_pred - + def validate(self, val_dataset: GeneratorDataset) -> float: """Validate model on validation dataset - + Args: val_dataset: Validation dataset - + Returns: Average validation loss """ self.model.set_train(False) total_loss = 0.0 num_batches = 0 - + for u, y, target in val_dataset: mean_pred, log_std_pred = self.model(u, y) loss = self.loss_fn(mean_pred, log_std_pred, target) total_loss += float(loss) num_batches += 1 - + self.model.set_train(True) - return total_loss / num_batches if num_batches > 0 else float('inf') - - def train(self, - train_dataset: GeneratorDataset, - val_dataset: Optional[GeneratorDataset] = None) -> Dict[str, List[float]]: + return total_loss / num_batches if num_batches > 0 else float("inf") + + def train( + self, + train_dataset: GeneratorDataset, + val_dataset: Optional[GeneratorDataset] = None, + ) -> Dict[str, List[float]]: """Train the model - + Args: train_dataset: Training dataset val_dataset: Validation dataset (optional) - + Returns: Training history """ import time - epochs = self.training_config['epochs'] - print_every = self.training_config.get('print_every', 10) - eval_every = self.training_config.get('eval_every', 100) # 100 steps - verbose = self.training_config.get('verbose', True) - + + epochs = self.training_config["epochs"] + print_every = self.training_config.get("print_every", 10) + eval_every = self.training_config.get("eval_every", 100) # 100 steps + verbose = self.training_config.get("verbose", True) + # Log model parameter count total_params = sum(p.size for p in self.model.get_parameters()) trainable_params = sum(p.size for p in self.model.trainable_params()) logging.info(f"Total model parameters: {total_params}") logging.info(f"Trainable parameters: {trainable_params}") # Log training data size - train_data_size = train_dataset.get_dataset_size() * train_dataset.get_batch_size() + train_data_size = ( + train_dataset.get_dataset_size() * train_dataset.get_batch_size() + ) logging.info(f"Training data size (number of samples): {train_data_size}") logging.info(f"Training batch size : {train_dataset.get_batch_size()}") @@ -313,20 +279,20 @@ class DeepONetTrainer: print("Model parameter details:") for p in self.model.get_parameters(): print(f"{p.name}: shape={p.shape}, size={p.size}") - + if verbose: print(f"\n***** Probabilistic Training for {epochs} epochs *****\n") - + # Initialize best values and logger best = {} - best["prob loss"] = float('inf') - + best["prob loss"] = float("inf") + logger_hist = {} logger_hist["prob loss"] = [] logger_hist["val loss"] = [] - + global_step = 0 - + for epoch in range(epochs): self.model.set_train() epoch_loss = 0 @@ -338,26 +304,33 @@ class DeepONetTrainer: epoch_loss += float(loss) batch_count += 1 global_step += 1 - # show loss + # show loss if global_step % print_every == 0: - # Negative log likelihood loss can be negative, so we print its negation for clarity. - msg = (f"Epoch {epoch+1}, Step {global_step}, Batch {batch_count}, " - f"Loss: {float(loss):.6f}, " - f"Step time: {step_time:.3f}s") + # Negative log likelihood loss can be negative, so we print + # its negation for clarity. + msg = ( + f"Epoch {epoch+1}, Step {global_step}, Batch {batch_count}, " + f"Loss: {float(loss):.6f}, " + f"Step time: {step_time:.3f}s" + ) # print(msg) logging.info(msg) - # evaluate + # evaluate if val_dataset is not None and global_step % eval_every == 0: val_loss = self.validate(val_dataset) logger_hist["val loss"].append((global_step, val_loss)) # Negative log likelihood loss can be negative - msg = (f"[Eval] Epoch {epoch+1}, Step {global_step}, Val-Loss: {float(val_loss):.6f} ") + msg = f"[Eval] Epoch {epoch+1}, Step {global_step}, Val-Loss: {float(val_loss):.6f} " # print(msg) logging.info(msg) # save best ckpt if val_loss < best["prob loss"]: best["prob loss"] = val_loss - self.save_model('best_model.ckpt') + self.save_model("best_model.ckpt") + + # record current learning rate + current_lr = float(self.optimizer.learning_rate) + logger.info(f"Current learning rate: {current_lr:.2e}") # Compute average epoch loss try: avg_epoch_loss = epoch_loss / batch_count @@ -368,24 +341,24 @@ class DeepONetTrainer: # show after each epoch if verbose: # Negative log likelihood loss can be negative - print(f'Epoch {epoch+1}/{epochs}:') - print(f' Train-Loss: {float(avg_epoch_loss):.6f} ') + print(f"Epoch {epoch+1}/{epochs}:") + print(f" Train-Loss: {float(avg_epoch_loss):.6f} ") print(f' Best-Loss: {float(best["prob loss"]):.6f} ') return logger_hist - + def save_model(self, filename: str): """Save model checkpoint - + Args: filename: Name of the checkpoint file """ save_path = os.path.join(self.save_dir, filename) ms.save_checkpoint(self.model, save_path) logger.info(f"Model saved to: {save_path}") - + def load_model(self, filename: str): """Load model checkpoint - + Args: filename: Name of the checkpoint file """ @@ -395,14 +368,14 @@ class DeepONetTrainer: logger.info(f"Model loaded from: {load_path}") else: logger.warning(f"Checkpoint not found: {load_path}") - + def predict(self, u: ms.Tensor, y: ms.Tensor) -> Tuple[ms.Tensor, ms.Tensor]: """Make predictions - + Args: u: Input function values y: Evaluation points - + Returns: Tuple of (mean_pred, log_std_pred) """ @@ -410,35 +383,35 @@ class DeepONetTrainer: mean_pred, log_std_pred = self.model(u, y) self.model.set_train(True) return mean_pred, log_std_pred - + def evaluate(self, test_dataset: GeneratorDataset) -> Dict[str, float]: """Evaluate model on test dataset - + Args: test_dataset: Test dataset - + Returns: Dictionary of evaluation metrics """ self.model.set_train(False) - + # Collect predictions and targets all_predictions = [] all_targets = [] all_means = [] all_stds = [] - + for u, y, target in test_dataset: # Forward pass mean_pred, log_std_pred = self.model(u, y) std_pred = ops.Exp()(log_std_pred) - + # Store results all_predictions.append(mean_pred) all_targets.append(target) all_means.append(mean_pred) all_stds.append(std_pred) - + # Concatenate all batches if all_predictions: predictions = ops.Concat(axis=0)(all_predictions) @@ -447,164 +420,87 @@ class DeepONetTrainer: stds = ops.Concat(axis=0)(all_stds) else: return {} - + # Compute basic metrics metrics = {} - metrics['mse'] = compute_mse(targets, predictions) - metrics['mae'] = compute_mae(targets, predictions) - metrics['r2'] = compute_r2_score(targets, predictions) - + metrics["mse"] = compute_mse(targets, predictions) + metrics["mae"] = compute_mae(targets, predictions) + metrics["r2"] = compute_r2_score(targets, predictions) + # Compute relative errors from .metrics import MetricsCalculator + calculator = MetricsCalculator() - metrics['l1_relative_error'] = calculator.l1_relative_error(targets, predictions) - metrics['l2_relative_error'] = calculator.l2_relative_error(targets, predictions) - + metrics["l1_relative_error"] = calculator.l1_relative_error( + targets, predictions + ) + metrics["l2_relative_error"] = calculator.l2_relative_error( + targets, predictions + ) + # Compute uncertainty quantification metrics - metrics['calibration_error'] = compute_calibration_error(targets, means, stds) - metrics['fraction_in_CI'] = calculator.fraction_in_CI(targets, means, stds, xi=2.0) - + metrics["calibration_error"] = compute_calibration_error(targets, means, stds) + metrics["fraction_in_CI"] = calculator.fraction_in_CI( + targets, means, stds, xi=2.0 + ) + # Compute trajectory errors if data is structured as trajectories try: l1_traj, l2_traj = calculator.trajectory_rel_error(targets, predictions) - metrics['trajectory_l1_error'] = l1_traj - metrics['trajectory_l2_error'] = l2_traj - except: + metrics["trajectory_l1_error"] = l1_traj + metrics["trajectory_l2_error"] = l2_traj + except BaseException: pass - + return metrics - - def compute_trajectory_metrics(self, s_test: List[ms.Tensor], - mean_predictions: List[ms.Tensor], - std_predictions: List[ms.Tensor], - verbose: bool = False) -> Dict[str, Any]: + def compute_trajectory_metrics( + self, + s_test: List[ms.Tensor], + mean_predictions: List[ms.Tensor], + std_predictions: List[ms.Tensor], + verbose: bool = False, + ) -> Dict[str, Any]: """Compute metrics for trajectory predictions - + Args: s_test: List of true solutions mean_predictions: List of predicted means std_predictions: List of predicted standard deviations verbose: Whether to print results - + Returns: Dictionary of trajectory metrics """ # Compute L1 and L2 relative errors - metrics_state = compute_metrics(s_test, mean_predictions, ['l1', 'l2'], verbose=verbose) - + metrics_state = compute_metrics( + s_test, mean_predictions, ["l1", "l2"], verbose=verbose + ) + # Compute fraction in confidence interval for each trajectory from .metrics import MetricsCalculator + calculator = MetricsCalculator() - + ci_fractions = [] for i in range(len(s_test)): - ci_frac = calculator.fraction_in_CI(s_test[i], mean_predictions[i], std_predictions[i]) + ci_frac = calculator.fraction_in_CI( + s_test[i], mean_predictions[i], std_predictions[i] + ) ci_fractions.append(ci_frac) - + avg_ci_fraction = np.mean(ci_fractions) - + return { - 'l1_metrics': metrics_state[0], # [max, min, mean] - 'l2_metrics': metrics_state[1], # [max, min, mean] - 'avg_ci_fraction': avg_ci_fraction, - 'ci_fractions': ci_fractions + "l1_metrics": metrics_state[0], # [max, min, mean] + "l2_metrics": metrics_state[1], # [max, min, mean] + "avg_ci_fraction": avg_ci_fraction, + "ci_fractions": ci_fractions, } -class ReduceLROnPlateau: - """Reduce learning rate when a metric has stopped improving""" - - def __init__(self, optimizer, mode='min', factor=0.1, patience=10, verbose=False, - min_lr=0, eps=1e-8): - self.optimizer = optimizer - self.mode = mode - self.factor = float(factor) # Ensure it's a float - self.patience = int(patience) # Ensure it's an int - self.verbose = verbose - self.min_lr = float(min_lr) # Ensure it's a float - self.eps = float(eps) # Ensure it's a float - - self.best = None - self.num_bad_epochs = 0 - self.mode_worse = float('inf') if mode == 'min' else float('-inf') - - def step(self, metrics): - """Update learning rate based on metrics""" - if self.best is None: - self.best = float(metrics) # Ensure it's a float - elif self._is_better(metrics, self.best): - self.best = float(metrics) # Ensure it's a float - self.num_bad_epochs = 0 - else: - self.num_bad_epochs += 1 - - if self.num_bad_epochs >= self.patience: - self._reduce_lr() - self.num_bad_epochs = 0 - - def _is_better(self, current, best): - """Check if current metrics are better than best""" - current = float(current) # Ensure it's a float - best = float(best) # Ensure it's a float - if self.mode == 'min': - return current < best - self.eps - else: - return current > best + self.eps - - def _reduce_lr(self): - """Reduce learning rate for all parameter groups""" - for param_group in self.optimizer.param_groups: - old_lr = float(param_group['lr']) # Ensure it's a float - new_lr = max(old_lr * self.factor, self.min_lr) - param_group['lr'] = new_lr - - if self.verbose and old_lr != new_lr: - logger.info(f'Reducing learning rate from {old_lr:.6f} to {new_lr:.6f}') - -def create_trainer(model: nn.Cell, config: Dict[str, Any], save_dir: str = "outputs") -> DeepONetTrainer: - """Create trainer instance - - Args: - model: DeepONet model - config: Configuration dictionary - save_dir: Directory to save outputs - - Returns: - Trainer instance - """ - return DeepONetTrainer(model, config, save_dir) -if __name__ == "__main__": - # Test trainer - from model import Prob_DeepONet - from data import generate_synthetic_data, create_mindspore_datasets, split_data - - # Load config - with open('configs/config.yaml', 'r') as f: - config = yaml.safe_load(f) - - # Generate test data - u, y, s = generate_synthetic_data(n_samples=100, n_sensors=50, n_points=1) - data_splits = split_data(u, y, s, train_split=0.8, val_split=0.1, test_split=0.1) - datasets = create_mindspore_datasets(data_splits, batch_size=16) - - # Create model - branch_config = config['model']['branch'] - trunk_config = config['model']['trunk'] - - # Update layer sizes based on data - branch_config['layer_size'][0] = u.shape[-1] # Input dimension - trunk_config['layer_size'][0] = y.shape[-1] # Input dimension - - model = Prob_DeepONet(branch_config, trunk_config, config['model']['use_bias']) - - # Create trainer - trainer = create_trainer(model, config) - - # Train model - history = trainer.train(datasets['train'], datasets['val']) - - # Evaluate model - metrics = trainer.evaluate(datasets['test']) - - print("Training completed successfully!") +def create_trainer( + model: nn.Cell, config: Dict[str, Any], save_dir: str = "outputs" +) -> DeepONetTrainer: + """Create trainer instance""" + return DeepONetTrainer(model, config, save_dir) diff --git a/MindEnergy/application/deeponet-grid/src/utils.py b/MindEnergy/application/deeponet-grid/src/utils.py index 5cfb08973..2a25e465a 100644 --- a/MindEnergy/application/deeponet-grid/src/utils.py +++ b/MindEnergy/application/deeponet-grid/src/utils.py @@ -1,23 +1,39 @@ -#!/usr/bin/env python3 +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== """ Utility functions for DeepONet-Grid-UQ -DeepONet-Grid-UQ工具函数 """ +import json +import os import re +from typing import Dict, List, Optional, Tuple + import matplotlib.pyplot as plt +import mindspore as ms import numpy as np -from typing import List, Tuple, Dict, Optional -import json -import os +import yaml + def parse_loss_log(log_file_path: str) -> Tuple[List[float], List[int], List[int]]: """ Parse loss log file and extract loss values, epochs, and steps - + Args: log_file_path: Path to the log file - + Returns: Tuple of (losses, epochs, steps) where: - losses: List of loss values @@ -27,37 +43,39 @@ def parse_loss_log(log_file_path: str) -> Tuple[List[float], List[int], List[int losses = [] epochs = [] steps = [] - + if not os.path.exists(log_file_path): print(f"Log file not found: {log_file_path}") return losses, epochs, steps - + # Regular expression to match the log format - # INFO:root:Epoch 1, Step 100, Batch 100, Loss: -0.524214, Step time: 0.040s - pattern = r'INFO:root:Epoch (\d+), Step (\d+), Batch \d+, Loss: ([-\d.]+), Step time: \d+\.\d+s' - - with open(log_file_path, 'r', encoding='utf-8') as f: + # INFO:root:Epoch 1, Step 100, Batch 100, Loss: -0.524214, Step time: + # 0.040s + pattern = r"INFO:root:Epoch (\d+), Step (\d+), Batch \d+, Loss: ([-\d.]+), Step time: \d+\.\d+s" + + with open(log_file_path, "r", encoding="utf-8") as f: for line in f: match = re.search(pattern, line) if match: epoch = int(match.group(1)) step = int(match.group(2)) loss = float(match.group(3)) - + epochs.append(epoch) steps.append(step) losses.append(loss) - + print(f"Parsed {len(losses)} loss entries from log file") return losses, epochs, steps + def parse_val_loss_log(log_file_path: str) -> Tuple[List[float], List[int], List[int]]: """ Parse validation loss log file and extract validation loss values - + Args: log_file_path: Path to the log file - + Returns: Tuple of (val_losses, epochs, steps) where: - val_losses: List of validation loss values @@ -67,37 +85,41 @@ def parse_val_loss_log(log_file_path: str) -> Tuple[List[float], List[int], List val_losses = [] epochs = [] steps = [] - + if not os.path.exists(log_file_path): print(f"Log file not found: {log_file_path}") return val_losses, epochs, steps - + # Regular expression to match validation loss log format - # INFO:root:[Eval] Epoch 1, Step 100, Val-Loss: -0.705899 - pattern = r'INFO:root:\[Eval\] Epoch (\d+), Step (\d+), Val-Loss: ([-\d.]+) ' - - with open(log_file_path, 'r', encoding='utf-8') as f: + # INFO:root:[Eval] Epoch 1, Step 100, Val-Loss: -0.705899 (negated for + # display) + pattern = r"INFO:root:\[Eval\] Epoch (\d+), Step (\d+), Val-Loss: ([-\d.]+) \(negated for display\)" + + with open(log_file_path, "r", encoding="utf-8") as f: for line in f: match = re.search(pattern, line) if match: epoch = int(match.group(1)) step = int(match.group(2)) val_loss = float(match.group(3)) - + epochs.append(epoch) steps.append(step) val_losses.append(val_loss) - + print(f"Parsed {len(val_losses)} validation loss entries from log file") return val_losses, epochs, steps -def plot_loss_curves(log_file_path: str, - save_path: Optional[str] = None, - show_plot: bool = True, - figsize: Tuple[int, int] = (12, 8)) -> None: + +def plot_loss_curves( + log_file_path: str, + save_path: Optional[str] = None, + show_plot: bool = True, + figsize: Tuple[int, int] = (12, 8), +) -> None: """ Plot training and validation loss curves from log file - + Args: log_file_path: Path to the log file save_path: Path to save the plot (optional) @@ -107,27 +129,29 @@ def plot_loss_curves(log_file_path: str, # Parse loss data losses, epochs, steps = parse_loss_log(log_file_path) val_losses, val_epochs, val_steps = parse_val_loss_log(log_file_path) - + if not losses: print("No loss data found in log file") return - + # Create figure fig, (ax1, ax2) = plt.subplots(2, 1, figsize=figsize) - + # Plot training loss - ax1.plot(steps, losses, 'b-', label='Training Loss', linewidth=1, alpha=0.8) - ax1.set_xlabel('Training Steps') - ax1.set_ylabel('Loss') - ax1.set_title('Training Loss Curve') + ax1.plot(steps, losses, "b-", label="Training Loss", linewidth=1, alpha=0.8) + ax1.set_xlabel("Training Steps") + ax1.set_ylabel("Loss (negated for display)") + ax1.set_title("Training Loss Curve") ax1.grid(True, alpha=0.3) ax1.legend() - + # Plot validation loss if available if val_losses: - ax1.plot(val_steps, val_losses, 'r-', label='Validation Loss', linewidth=1, alpha=0.8) + ax1.plot( + val_steps, val_losses, "r-", label="Validation Loss", linewidth=1, alpha=0.8 + ) ax1.legend() - + # Plot loss vs epochs unique_epochs = list(set(epochs)) epoch_losses = [] @@ -135,34 +159,37 @@ def plot_loss_curves(log_file_path: str, epoch_indices = [i for i, e in enumerate(epochs) if e == epoch] epoch_loss = np.mean([losses[i] for i in epoch_indices]) epoch_losses.append(epoch_loss) - - ax2.plot(unique_epochs, epoch_losses, 'g-', label='Average Epoch Loss', linewidth=2) - ax2.set_xlabel('Epochs') - ax2.set_ylabel('Average Loss') - ax2.set_title('Average Loss per Epoch') + + ax2.plot(unique_epochs, epoch_losses, "g-", label="Average Epoch Loss", linewidth=2) + ax2.set_xlabel("Epochs") + ax2.set_ylabel("Average Loss (negated for display)") + ax2.set_title("Average Loss per Epoch") ax2.grid(True, alpha=0.3) ax2.legend() - + plt.tight_layout() - + # Save plot if specified if save_path: - plt.savefig(save_path, dpi=300, bbox_inches='tight') + plt.savefig(save_path, dpi=300, bbox_inches="tight") print(f"Loss curve saved to: {save_path}") - + # Show plot if requested if show_plot: plt.show() - + plt.close() -def plot_loss_statistics(log_file_path: str, - save_path: Optional[str] = None, - show_plot: bool = True, - figsize: Tuple[int, int] = (15, 10)) -> None: + +def plot_loss_statistics( + log_file_path: str, + save_path: Optional[str] = None, + show_plot: bool = True, + figsize: Tuple[int, int] = (15, 10), +) -> None: """ Plot detailed loss statistics including histograms and moving averages - + Args: log_file_path: Path to the log file save_path: Path to save the plot (optional) @@ -172,42 +199,44 @@ def plot_loss_statistics(log_file_path: str, # Parse loss data losses, epochs, steps = parse_loss_log(log_file_path) val_losses, val_epochs, val_steps = parse_val_loss_log(log_file_path) - + if not losses: print("No loss data found in log file") return - + # Create figure with subplots fig = plt.figure(figsize=figsize) - + # 1. Training loss over steps ax1 = plt.subplot(2, 3, 1) - ax1.plot(steps, losses, 'b-', alpha=0.7, linewidth=0.8) - ax1.set_xlabel('Training Steps') - ax1.set_ylabel('Loss') - ax1.set_title('Training Loss') + ax1.plot(steps, losses, "b-", alpha=0.7, linewidth=0.8) + ax1.set_xlabel("Training Steps") + ax1.set_ylabel("Loss (negated for display)") + ax1.set_title("Training Loss") ax1.grid(True, alpha=0.3) - + # 2. Moving average of training loss ax2 = plt.subplot(2, 3, 2) window_size = min(50, len(losses) // 10) # Adaptive window size if window_size > 1: - moving_avg = np.convolve(losses, np.ones(window_size)/window_size, mode='valid') - steps_avg = steps[window_size-1:] - ax2.plot(steps_avg, moving_avg, 'r-', linewidth=1.5) - ax2.set_xlabel('Training Steps') - ax2.set_ylabel('Moving Average Loss') - ax2.set_title(f'Moving Average (window={window_size})') + moving_avg = np.convolve( + losses, np.ones(window_size) / window_size, mode="valid" + ) + steps_avg = steps[window_size - 1 :] + ax2.plot(steps_avg, moving_avg, "r-", linewidth=1.5) + ax2.set_xlabel("Training Steps") + ax2.set_ylabel("Moving Average Loss") + ax2.set_title(f"Moving Average (window={window_size})") ax2.grid(True, alpha=0.3) - + # 3. Loss histogram ax3 = plt.subplot(2, 3, 3) - ax3.hist(losses, bins=30, alpha=0.7, color='blue', edgecolor='black') - ax3.set_xlabel('Loss Value') - ax3.set_ylabel('Frequency') - ax3.set_title('Loss Distribution') + ax3.hist(losses, bins=30, alpha=0.7, color="blue", edgecolor="black") + ax3.set_xlabel("Loss Value") + ax3.set_ylabel("Frequency") + ax3.set_title("Loss Distribution") ax3.grid(True, alpha=0.3) - + # 4. Loss vs epochs ax4 = plt.subplot(2, 3, 4) unique_epochs = list(set(epochs)) @@ -216,29 +245,38 @@ def plot_loss_statistics(log_file_path: str, epoch_indices = [i for i, e in enumerate(epochs) if e == epoch] epoch_loss = np.mean([losses[i] for i in epoch_indices]) epoch_losses.append(epoch_loss) - - ax4.plot(unique_epochs, epoch_losses, 'g-', linewidth=2, marker='o') - ax4.set_xlabel('Epochs') - ax4.set_ylabel('Average Loss') - ax4.set_title('Average Loss per Epoch') + + ax4.plot(unique_epochs, epoch_losses, "g-", linewidth=2, marker="o") + ax4.set_xlabel("Epochs") + ax4.set_ylabel("Average Loss") + ax4.set_title("Average Loss per Epoch") ax4.grid(True, alpha=0.3) - + # 5. Validation loss (if available) ax5 = plt.subplot(2, 3, 5) if val_losses: - ax5.plot(val_steps, val_losses, 'orange', linewidth=1.5, label='Validation Loss') - ax5.set_xlabel('Training Steps') - ax5.set_ylabel('Validation Loss') - ax5.set_title('Validation Loss') + ax5.plot( + val_steps, val_losses, "orange", linewidth=1.5, label="Validation Loss" + ) + ax5.set_xlabel("Training Steps") + ax5.set_ylabel("Validation Loss") + ax5.set_title("Validation Loss") ax5.grid(True, alpha=0.3) ax5.legend() else: - ax5.text(0.5, 0.5, 'No validation loss data', ha='center', va='center', transform=ax5.transAxes) - ax5.set_title('Validation Loss (No Data)') - + ax5.text( + 0.5, + 0.5, + "No validation loss data", + ha="center", + va="center", + transform=ax5.transAxes, + ) + ax5.set_title("Validation Loss (No Data)") + # 6. Loss statistics ax6 = plt.subplot(2, 3, 6) - ax6.axis('off') + ax6.axis("off") stats_text = f""" Loss Statistics: ---------------- @@ -260,115 +298,102 @@ Max Val Loss: {max(val_losses):.6f} Mean Val Loss: {np.mean(val_losses):.6f} Final Val Loss: {val_losses[-1]:.6f} """ - - ax6.text(0.05, 0.95, stats_text, transform=ax6.transAxes, - fontsize=10, verticalalignment='top', fontfamily='monospace') - + + ax6.text( + 0.05, + 0.95, + stats_text, + transform=ax6.transAxes, + fontsize=10, + verticalalignment="top", + fontfamily="monospace", + ) + plt.tight_layout() - + # Save plot if specified if save_path: - plt.savefig(save_path, dpi=300, bbox_inches='tight') + plt.savefig(save_path, dpi=300, bbox_inches="tight") print(f"Loss statistics saved to: {save_path}") - + # Show plot if requested if show_plot: plt.show() - + plt.close() + def load_training_history(history_file_path: str) -> Dict: """ Load training history from JSON file - + Args: history_file_path: Path to the training history JSON file - + Returns: Dictionary containing training history """ if not os.path.exists(history_file_path): print(f"History file not found: {history_file_path}") return {} - - with open(history_file_path, 'r') as f: + + with open(history_file_path, "r") as f: history = json.load(f) - + return history -def plot_training_history(history_file_path: str, - save_path: Optional[str] = None, - show_plot: bool = True) -> None: + +def plot_training_history( + history_file_path: str, save_path: Optional[str] = None, show_plot: bool = True +) -> None: """ Plot training history from JSON file - + Args: history_file_path: Path to the training history JSON file save_path: Path to save the plot (optional) show_plot: Whether to display the plot """ history = load_training_history(history_file_path) - + if not history: print("No training history data found") return - - fig, axes = plt.subplots(1, len(history), figsize=(5*len(history), 4)) + + fig, axes = plt.subplots(1, len(history), figsize=(5 * len(history), 4)) if len(history) == 1: axes = [axes] - + for i, (key, values) in enumerate(history.items()): - axes[i].plot(values, 'b-', linewidth=1.5) - axes[i].set_xlabel('Epochs') - axes[i].set_ylabel(key.replace('_', ' ').title()) + axes[i].plot(values, "b-", linewidth=1.5) + axes[i].set_xlabel("Epochs") + axes[i].set_ylabel(key.replace("_", " ").title()) axes[i].set_title(f'{key.replace("_", " ").title()} vs Epochs') axes[i].grid(True, alpha=0.3) - + plt.tight_layout() - + # Save plot if specified if save_path: - plt.savefig(save_path, dpi=300, bbox_inches='tight') + plt.savefig(save_path, dpi=300, bbox_inches="tight") print(f"Training history plot saved to: {save_path}") - + # Show plot if requested if show_plot: plt.show() - + plt.close() -def main(): - """Example usage of utility functions""" - print("DeepONet-Grid-UQ Utility Functions") - print("=" * 40) - - # Example usage - log_file = "outputs/training.log" - - if os.path.exists(log_file): - print(f"Found log file: {log_file}") - - # Parse and print basic statistics - losses, epochs, steps = parse_loss_log(log_file) - if losses: - print(f"Loss statistics:") - print(f" Total entries: {len(losses)}") - print(f" Min loss: {min(losses):.6f}") - print(f" Max loss: {max(losses):.6f}") - print(f" Mean loss: {np.mean(losses):.6f}") - print(f" Final loss: {losses[-1]:.6f}") - - # Plot loss curves - plot_loss_curves(log_file, save_path="loss_curves.png") - plot_loss_statistics(log_file, save_path="loss_statistics.png") - - # Check for training history - history_file = "outputs/training_history.json" - if os.path.exists(history_file): - print(f"Found training history: {history_file}") - plot_training_history(history_file, save_path="training_history.png") - - print("Utility functions completed!") - -if __name__ == "__main__": - main() \ No newline at end of file + +def load_config(config_path: str) -> dict: + with open(config_path, "r", encoding="utf-8") as f: + config = yaml.safe_load(f) + return config + + +def load_trained_model(model, checkpoint_path: str): + if not os.path.exists(checkpoint_path): + raise FileNotFoundError(f"Checkpoint not found: {checkpoint_path}") + + ms.load_checkpoint(checkpoint_path, model) + return model diff --git a/MindEnergy/application/deeponet-grid/test_system.py b/MindEnergy/application/deeponet-grid/test_system.py deleted file mode 100644 index c359074eb..000000000 --- a/MindEnergy/application/deeponet-grid/test_system.py +++ /dev/null @@ -1,544 +0,0 @@ -#!/usr/bin/env python3 -""" -System test script for DeepONet-Grid-UQ -""" - -import os -import sys -import yaml -import numpy as np -import mindspore as ms -from mindspore import context - -# Add src to path -sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) - -from src.model import Prob_DeepONet -from src.data import generate_synthetic_data, normalize_data_mindspore, split_data, create_mindspore_datasets, load_real_data, prepare_deeponet_data, trajectory_prediction, batch_trajectory_prediction -from src.trainer import create_trainer -from src.metrics import compute_metrics, update_metrics_history, MetricsCalculator - -def test_config_loading(): - """Test configuration loading""" - print("Testing configuration loading...") - - config_path = 'configs/config.yaml' - if os.path.exists(config_path): - with open(config_path, 'r') as f: - config = yaml.safe_load(f) - - print(f" Configuration loaded successfully") - print(f" Model config: {config.get('model', 'Not found')}") - print(f" Training config: {config.get('training', 'Not found')}") - print("Correct: Configuration loading test passed") - else: - print(f"✗ Configuration file not found: {config_path}") - return False - - return True - -def test_data_generation(): - """Test data generation functionality""" - print("Testing data generation...") - - # Generate synthetic data - u, y, s = generate_synthetic_data(n_samples=100, n_sensors=50, n_points=1, seed=42) - - print(f" Generated data shapes: u={u.shape}, y={y.shape}, s={s.shape}") - - # Test data splitting - data_splits = split_data(u, y, s, train_split=0.8, val_split=0.1, test_split=0.1) - print(f" Data splits: {list(data_splits.keys())}") - - # Test MindSpore dataset creation - datasets = create_mindspore_datasets(data_splits, batch_size=16) - print(f" Created datasets: {list(datasets.keys())}") - - print("Correct: Data generation test passed") - return datasets - -def test_data_normalization(): - # use test_data_generation to generate data - - # Generate synthetic data - u, y, s = generate_synthetic_data(n_samples=100, n_sensors=50, n_points=1, seed=42) - - print(f" Generated data shapes: u={u.shape}, y={y.shape}, s={s.shape}") - - # normalize data - - # try different normalization methods - u_norm, y_norm, s_norm, scalers = normalize_data_mindspore(u, y, s, method='standard') - print(f" Normalized data shapes: u={u_norm.shape}, y={y_norm.shape}, s={s_norm.shape}") - - u_norm, y_norm, s_norm, scalers = normalize_data_mindspore(u, y, s, method='minmax') - print(f" Normalized data shapes: u={u_norm.shape}, y={y_norm.shape}, s={s_norm.shape}") - - print("Correct: Data normalization test passed") - -def test_load_real_data(): - """Test real data loading functionality""" - print("Testing real data loading...") - - # Check for common data file locations - possible_data_paths = [ - "/Users/congwang/Documents/codes/M_deeponetgrid/deeponet_ms/data/grid_real/data/trustworthydatasetv1/test-data-voltage-m-33-mix.npz", - "/Users/congwang/Documents/codes/M_deeponetgrid/deeponet_ms/data/grid_real/data/trustworthydatasetv1/train-data-voltage-m-33-Q-100-mix.npz" - ] - - data_found = False - for data_path in possible_data_paths: - if os.path.exists(data_path): - print(f" Found data file: {data_path}") - try: - # Load data - u, y, s = load_real_data(data_path) - print(f" Loaded data shapes: u={u.shape}, y={y.shape}, s={s.shape}") - - # Test data properties - print(f" Data types: u={u.dtype}, y={y.dtype}, s={s.dtype}") - print(f" Data ranges: u=[{u.min():.3f}, {u.max():.3f}], y=[{y.min():.3f}, {y.max():.3f}], s=[{s.min():.3f}, {s.max():.3f}]") - - # Test data splitting - data_splits = split_data(u, y, s, train_split=0.8, val_split=0.1, test_split=0.1) - print(f" Data splits: {list(data_splits.keys())}") - - # Test MindSpore dataset creation - datasets = create_mindspore_datasets(data_splits, batch_size=16) - print(f" Created datasets: {list(datasets.keys())}") - - data_found = True - print("Correct: Real data loading test passed") - # return datasets - - except Exception as e: - print(f" Error loading {data_path}: {e}") - continue - - if not data_found: - print(" No data files found in common locations") - print(" Creating synthetic data for testing...") - - # Create synthetic data as fallback - u, y, s = generate_synthetic_data(n_samples=100, n_sensors=50, n_points=1, seed=42) - data_splits = split_data(u, y, s, train_split=0.8, val_split=0.1, test_split=0.1) - datasets = create_mindspore_datasets(data_splits, batch_size=16) - - print("Correct: Real data loading test passed (using synthetic data)") - return datasets - -def test_trajectory_prediction(): - """Test trajectory prediction functionality""" - print("Testing trajectory prediction...") - - # Load real data - data_path = "/Users/congwang/Documents/codes/M_deeponetgrid/deeponet_ms/data/grid_real/data/trustworthydatasetv1/test-data-voltage-m-33-mix.npz" - u, y, s = load_real_data(data_path) - - # Test trajectory_prediction function - print(" Testing trajectory_prediction function...") - - # Create a simple model for testing (if available) - try: - from src.model import Prob_DeepONet - - # Create a simple model - branch_config = { - "type": "modified", - "layer_size": [33, 100, 100, 50], # Smaller for testing - "activation": "sin" - } - - trunk_config = { - "type": "modified", - "layer_size": [1, 100, 100, 50], # Smaller for testing - "activation": "sin" - } - - model = Prob_DeepONet(branch_config, trunk_config, use_bias=True) - - # Test single sample trajectory prediction - u_single = u[0] # First sample, shape (33,) - y_single = y[0] # First sample, shape (100,) - - predictions = trajectory_prediction(u_single, y_single, model) - print(f" Single sample prediction shape: {predictions.shape}") - print(f" Predictions: {predictions.flatten()}") - - # Test batch trajectory prediction - u_batch = u[:3] # First 3 samples, shape (3, 33) - y_batch = y[:3] # First 3 samples, shape (3, 100) - batch_predictions = batch_trajectory_prediction(u_batch, y_batch, model) - print(f" Batch prediction shape: {batch_predictions.shape}") - - print(" Correct: Trajectory prediction functions work correctly") - - except ImportError: - print(" Warning: Model not available, skipping trajectory prediction test") - except Exception as e: - print(f" Warning: Trajectory prediction test failed: {e}") - - print("Correct: Trajectory prediction test passed") - -def test_model_creation(): - """Test model creation functionality""" - print("Testing model creation...") - - # Model configuration based on real data (following original author's formula) - # depth = 3 means 3 hidden layers: [input] + [width] * depth + [output] - branch_config = { - "type": "modified", - "layer_size": [33, 200, 200, 200, 100], # [m] + [width] * depth + [n_basis] - "activation": "sin" - } - - trunk_config = { - "type": "modified", - "layer_size": [1, 200, 200, 200, 100], # [dim] + [width] * depth + [n_basis] - "activation": "sin" - } - - # Create model - model = Prob_DeepONet(branch_config, trunk_config, use_bias=True) - print(f" Model created successfully") - - # Test forward pass - single query point - print("\n Testing single query point prediction:") - u = ms.Tensor(np.random.randn(10, 33), ms.float32) # 10 samples, 33 sensors - y = ms.Tensor(np.random.randn(10, 1), ms.float32) # 10 samples, 1 query point each - - mean_pred, log_std_pred = model(u, y) - print(f" Input shapes: u={u.shape}, y={y.shape}") - print(f" Output shapes: mean={mean_pred.shape}, log_std={log_std_pred.shape}") - print(f" Correct: DeepONet outputs scalar values for each query point") - - # Test forward pass - demonstrate the limitation - print("\n Testing multiple query points (this will fail as expected):") - u = ms.Tensor(np.random.randn(10, 33), ms.float32) # 10 samples, 33 sensors - y = ms.Tensor(np.random.randn(10, 100), ms.float32) # 10 samples, 100 query points each - - try: - mean_pred, log_std_pred = model(u, y) - print(f" Warning: Unexpected success! This should have failed.") - except Exception as e: - print(f" Correct: Expected error: {str(e)[:100]}...") - print(f" Correct: This demonstrates that DeepONet expects single query points") - - # Demonstrate how to get full trajectory - print("\n Demonstrating full trajectory prediction:") - print(f" To get 100 time points, you need 100 forward passes:") - print(f" - For each time point t in [0, 1, 2, ..., 99]:") - print(f" y_t = [[t]] # Single query point") - print(f" pred_t = model(u, y_t) # Forward pass") - print(f" - Collect all pred_t to get trajectory") - - # Show a working example - print("\n Working example - predict 5 time points:") - u_single = ms.Tensor(np.random.randn(1, 33), ms.float32) # 1 sample, 33 sensors - time_points = [0.0, 0.5, 1.0, 1.5, 2.0] - - predictions = [] - for t in time_points: - y_t = ms.Tensor([[t]], ms.float32) # Single query point - mean_pred, log_std_pred = model(u_single, y_t) - pred_val = float(mean_pred[0, 0]) - predictions.append(pred_val) - print(f" Time {t}: prediction = {pred_val:.4f}") - - print(f" Correct: Successfully predicted trajectory: {len(predictions)} points") - - print("Correct: Model creation test passed") - return model - -def test_trainer_creation(): - """Test trainer creation functionality""" - print("Testing trainer creation...") - - # Load config - config_path = 'configs/config.yaml' - if os.path.exists(config_path): - with open(config_path, 'r') as f: - config = yaml.safe_load(f) - else: - # Create minimal config for testing based on original notebook - config = { - 'model': { - 'm': 50, - 'dim': 1, - 'width': 200, - 'depth': 3, - 'n_basis': 100, - 'branch_type': 'modified', - 'trunk_type': 'modified', - 'activation': 'sin', - 'use_bias': True - }, - 'training': { - 'learning_rate': 0.00005, # 5e-5 - 'batch_size': 16, - 'epochs': 10, - 'print_every': 5, - 'eval_every': 5, - 'patience': 10, - 'factor': 0.8, - 'verbose': True, - 'loss_type': 'nll', - 'optimizer': 'adam', - 'weight_decay': 0.0 - }, - 'device': { - 'target': 'CPU', - 'mode': 'PYNATIVE_MODE' - }, - 'output': { - 'save_dir': 'test_outputs', - 'save_best': True - } - } - - # Create model - model = test_model_creation() - - # Create trainer - trainer = create_trainer(model, config, save_dir='test_outputs') - print(f" Trainer created successfully") - - print("Correct: Trainer creation test passed") - return trainer, config - -def test_training_workflow(): - """Test complete training workflow""" - print("Testing training workflow...") - - # Set up device context - context.set_context(mode=context.PYNATIVE_MODE, device_target="CPU") - - # Load data using approach from test_real_data_loading but with only 20 samples - print(" Loading data for training test...") - - # Check for common data file locations - data_paths = [ - "/Users/congwang/Documents/codes/M_deeponetgrid/deeponet_ms/data/grid_real/data/trustworthydatasetv1/train-data-voltage-m-33-Q-100-mix.npz" - ] - - data_found = False - for data_path in data_paths: - if os.path.exists(data_path): - print(f" Found data file: {data_path}") - try: - # Load data - u, y, s = load_real_data(data_path) - print(f" Loaded data shapes: u={u.shape}, y={y.shape}, s={s.shape}") - - # Take only 10 samples for testing - u = u[:10] - y = y[:10] - s = s[:10] - print(f" Using 20 samples: u={u.shape}, y={y.shape}, s={s.shape}") - - # Prepare data for DeepONet (convert multi-time-point to single query points) - print(" Preparing data for DeepONet...") - u_expanded, y_expanded, s_expanded, metadata = prepare_deeponet_data(u, y, s) - print(f" Prepared data: u={u_expanded.shape}, y={y_expanded.shape}, s={s_expanded.shape}") - - # data normalization - # u_expanded, y_expanded, s_expanded, scalers = normalize_data_mindspore(u_expanded, y_expanded, s_expanded, method='standard') - - # Test data splitting - data_splits = split_data(u_expanded, y_expanded, s_expanded, train_split=0.8, val_split=0.1, test_split=0.1) - print(f" Data splits: {list(data_splits.keys())}") - - # Test MindSpore dataset creation - datasets = create_mindspore_datasets(data_splits, batch_size=8) # Smaller batch size for 20 samples - print(f" Created datasets: {list(datasets.keys())}") - - data_found = True - break - - except Exception as e: - print(f" Error loading {data_path}: {e}") - continue - - if not data_found: - print(" No data files found in common locations") - print(" Creating synthetic data for testing...") - - # Create synthetic data as fallback with only 20 samples - u, y, s = generate_synthetic_data(n_samples=20, n_sensors=33, n_points=1, seed=42) - data_splits = split_data(u, y, s, train_split=0.8, val_split=0.1, test_split=0.1) - datasets = create_mindspore_datasets(data_splits, batch_size=8) - - # Create trainer using the same approach as test_trainer_creation() - print(" Creating trainer...") - - # Load config - config_path = 'configs/config.yaml' - if os.path.exists(config_path): - with open(config_path, 'r') as f: - config = yaml.safe_load(f) - else: - # Create minimal config for testing based on original notebook - config = { - 'model': { - 'm': 33, # Match the data dimension - 'dim': 1, - 'width': 200, - 'depth': 3, - 'n_basis': 100, - 'branch_type': 'modified', - 'trunk_type': 'modified', - 'activation': 'sin', - 'use_bias': True - }, - 'training': { - 'learning_rate': 0.00005, # 5e-5 - 'batch_size': 8, # Smaller batch size for testing - 'epochs': 5, # Fewer epochs for testing - 'print_every': 1, - 'eval_every': 1, - 'patience': 10, - 'factor': 0.8, - 'verbose': True, - 'loss_type': 'nll', - 'optimizer': 'adam', - 'weight_decay': 0.0 - }, - 'device': { - 'target': 'CPU', - 'mode': 'PYNATIVE_MODE' - }, - 'output': { - 'save_dir': 'test_outputs', - 'save_best': True - } - } - - # Create model - branch_config = { - "type": "modified", - "layer_size": [33, 200, 200, 200, 100], # [m] + [width] * depth + [n_basis] - "activation": "sin" - } - - trunk_config = { - "type": "modified", - "layer_size": [1, 200, 200, 200, 100], # [dim] + [width] * depth + [n_basis] - "activation": "sin" - } - - model = Prob_DeepONet(branch_config, trunk_config, use_bias=True) - - # Create trainer - trainer = create_trainer(model, config, save_dir='test_outputs') - print(f" Trainer created successfully") - - # Run a few training steps - print(" Running training steps...") - try: - # Get one batch from training dataset - for u, y, target in datasets['train']: - print(f"u shape: {u.shape}, y shape: {y.shape}, target shape: {target.shape}") - loss, mean_pred, log_std_pred = trainer.train_step(u, y, target) - print(f" Training step completed, loss: {float(loss):.6f}") - print(f" Prediction shapes: mean={mean_pred.shape}, log_std={log_std_pred.shape}") - print(f" Target shape: {target.shape}") - break - - # Test validation - val_loss = trainer.validate(datasets['val']) - print(f" Validation loss: {val_loss:.6f}") - - print("Correct: Training workflow test passed") - - except Exception as e: - print(f"✗ Training workflow test failed: {e}") - import traceback - traceback.print_exc() - return False - - return True - -def test_metrics(): - """Test metrics functionality""" - print("Testing metrics...") - - # Create test data - y_true = ms.Tensor(np.random.randn(100, 1), ms.float32) - y_pred = ms.Tensor(np.random.randn(100, 1), ms.float32) - s_std = ms.Tensor(np.abs(np.random.randn(100, 1)), ms.float32) - - # Test MetricsCalculator - calculator = MetricsCalculator() - - # Test L1 and L2 relative errors - l1_error = calculator.l1_relative_error(y_true, y_pred) - l2_error = calculator.l2_relative_error(y_true, y_pred) - print(f" L1 relative error: {l1_error:.6f}") - print(f" L2 relative error: {l2_error:.6f}") - - # Test fraction in confidence interval - ci_fraction = calculator.fraction_in_CI(y_true, y_pred, s_std, xi=2.0) - print(f" Fraction in CI: {ci_fraction:.6f}") - - # Test trajectory relative error - l1_traj, l2_traj = calculator.trajectory_rel_error(y_true, y_pred) - print(f" Trajectory L1: {l1_traj:.6f}, L2: {l2_traj:.6f}") - - # Test compute_metrics for multiple trajectories - s_true_list = [y_true, y_true * 0.5] - s_pred_list = [y_pred, y_pred * 0.5] - metrics_result = compute_metrics(s_true_list, s_pred_list, ['l1', 'l2'], verbose=True) - print(f" Metrics result: {metrics_result}") - - # Test update_metrics_history - history = {} - history = update_metrics_history(history, metrics_result[0]) - print(f" Updated history: {history}") - - print("Correct: Metrics test passed") - return True - -def main(): - """Run all tests""" - print("=" * 50) - print("DeepONet-Grid-UQ System Test") - print("=" * 50) - - tests = [ - ("Configuration Loading", test_config_loading), # passed - ("Data Generation", test_data_generation), # passed - # TODO: test data normalization - # ("Data Normalization", test_data_normalization), - ("Load Real Data", test_load_real_data), # passed - ("Trajectory Prediction", test_trajectory_prediction), # passed - ("Model Creation", test_model_creation), # passed - ("Trainer Creation", test_trainer_creation), # passed - ("Training Workflow", test_training_workflow), # passed - ("Metrics", test_metrics), # passed - ] - - passed = 0 - total = len(tests) - - for test_name, test_func in tests: - print(f"\n{test_name}:") - print("-" * 30) - try: - result = test_func() - if result is not False: - passed += 1 - except Exception as e: - print(f"✗ {test_name} failed with error: {e}") - - print("\n" + "=" * 50) - print(f"Test Results: {passed}/{total} tests passed") - - if passed == total: - print("All tests passed! System is ready to use.") - else: - print("Warning: Some tests failed. Please check the errors above.") - - print("=" * 50) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/MindEnergy/application/deeponet-grid/train.py b/MindEnergy/application/deeponet-grid/train.py index 0ab2c50b9..2708ed5f7 100644 --- a/MindEnergy/application/deeponet-grid/train.py +++ b/MindEnergy/application/deeponet-grid/train.py @@ -1,184 +1,104 @@ -#!/usr/bin/env python3 +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== """ Main training script for DeepONet-Grid-UQ """ - -import os -import sys -import yaml import argparse +import json import logging +import os +import sys from pathlib import Path import mindspore as ms +import yaml from mindspore import context -# Add src to path -sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) - +from src.data import load_and_preprocess_real_data from src.model import Prob_DeepONet -from src.data import load_real_data, prepare_deeponet_data, split_data, create_mindspore_datasets from src.trainer import create_trainer +from src.utils import load_config + +# Add src to path +sys.path.append(os.path.join(os.path.dirname(__file__), "src")) # Set up logging logging.basicConfig( level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", handlers=[ - logging.FileHandler('training.log'), - logging.StreamHandler(sys.stdout) - ] + logging.FileHandler("training.log"), + logging.StreamHandler( + sys.stdout)], ) logger = logging.getLogger(__name__) -def load_config(config_path: str) -> dict: - """Load configuration from YAML file - - Args: - config_path: Path to configuration file - - Returns: - Configuration dictionary - """ - with open(config_path, 'r', encoding='utf-8') as f: - config = yaml.safe_load(f) - return config +context.set_context(mode=context.PYNATIVE_MODE, device_target="CPU") +logger.info(f"Mode set to: {context.get_context('mode')}") +logger.info(f"Device set to: {context.get_context('device_target')}") -def setup_device(config: dict): - """Set up MindSpore device context - - Args: - config: Configuration dictionary - """ - device_config = config['device'] - - # Set context - context.set_context( - mode=getattr(context, device_config['mode']), - device_target=device_config['target'] - ) - - logger.info(f"Device set to: {device_config['target']}") - logger.info(f"Mode set to: {device_config['mode']}") - -def find_training_data(): - """Find training data file""" - possible_data_paths = [ - "data/train-data-voltage-m-33-Q-100-mix.npz", - ] - - for data_path in possible_data_paths: - if os.path.exists(data_path): - logger.info(f"Found training data: {data_path}") - return data_path - - raise FileNotFoundError("No training data file found. Please check the data paths.") - -def load_and_preprocess_real_data(config: dict): - """Load and preprocess real training data - - Args: - config: Configuration dictionary - - Returns: - Tuple of (datasets, metadata) - """ - # Find training data - data_path = find_training_data() if config['data']['data_path'] is None else config['data']['data_path'] - - # Load raw data - logger.info(f"Loading data from: {data_path}") - u, y, s = load_real_data(data_path) - logger.info(f"Original data shapes: u={u.shape}, y={y.shape}, s={y.shape}") - - # Prepare data for DeepONet (convert multi-time-point to single query points) - logger.info("Preparing data for DeepONet...") - u_expanded, y_expanded, s_expanded, prep_metadata = prepare_deeponet_data(u, y, s) - logger.info(f"Prepared data shapes: u={u_expanded.shape}, y={y_expanded.shape}, s={s_expanded.shape}") - - # Normalize data - # logger.info("Normalizing data...") - # u_norm, y_norm, s_norm, scalers = normalize_data_mindspore(u_expanded, y_expanded, s_expanded, method='standard') - - # Split data - logger.info("Splitting data...") - data_splits = split_data( - u_expanded, y_expanded, s_expanded, - train_split=config['data']['train_split'], - val_split=config['data']['val_split'], - test_split=config['data']['test_split'] - ) - - # Create MindSpore datasets - logger.info("Creating MindSpore datasets...") - datasets = create_mindspore_datasets( - data_splits, - batch_size=config['training']['batch_size'] - ) - - # Prepare metadata - metadata = { - 'input_dim': u_expanded.shape[-1], - 'output_dim': s_expanded.shape[-1], - 'n_samples': len(u_expanded), - 'n_sensors': u.shape[-1], # Original sensor count - 'data_shapes': { - 'u': u_expanded.shape, - 'y': y_expanded.shape, - 's': s_expanded.shape - } - } - - # Add preparation metadata - if prep_metadata: - metadata.update(prep_metadata) - - return datasets, metadata def create_model(config: dict, metadata: dict) -> Prob_DeepONet: """Create DeepONet model based on configuration and data metadata - + Args: config: Configuration dictionary metadata: Data metadata containing input/output dimensions - + Returns: DeepONet model instance """ - model_config = config['model'] - - # Get network parameters from metadata if available, otherwise use config defaults - m = metadata.get('n_sensors', model_config.get('m', 33)) # Number of sensors - dim = model_config.get('dim', 1) # Input dimension for trunk (always 1 for DeepONet) - width = model_config.get('width', 200) # Network width - depth = model_config.get('depth', 3) # Network depth (number of hidden layers) - n_basis = model_config.get('n_basis', 100) # Number of basis functions - + model_config = config["model"] + + # Get network parameters from metadata if available, otherwise use config + # defaults + m = metadata.get( + "n_sensors", model_config.get( + "m", 33)) # Number of sensors + # Input dimension for trunk (always 1 for DeepONet) + dim = model_config.get("dim", 1) + width = model_config.get("width", 200) # Network width + # Network depth (number of hidden layers) + depth = model_config.get("depth", 3) + n_basis = model_config.get("n_basis", 100) # Number of basis functions + # Get network types - branch_type = model_config.get('branch_type', 'modified') - trunk_type = model_config.get('trunk_type', 'modified') - activation = model_config.get('activation', 'sin') - + branch_type = model_config.get("branch_type", "modified") + trunk_type = model_config.get("trunk_type", "modified") + activation = model_config.get("activation", "sin") + # Compute layer sizes automatically (following original author's formula) # [input] + [width] * depth + [output] # depth = 3 means 3 hidden layers branch_layer_size = [m] + [width] * depth + [n_basis] trunk_layer_size = [dim] + [width] * depth + [n_basis] - + # Create branch configuration branch_config = { - 'type': branch_type, - 'layer_size': branch_layer_size, - 'activation': activation + "type": branch_type, + "layer_size": branch_layer_size, + "activation": activation, } - + # Create trunk configuration trunk_config = { - 'type': trunk_type, - 'layer_size': trunk_layer_size, - 'activation': activation + "type": trunk_type, + "layer_size": trunk_layer_size, + "activation": activation, } - + logger.info(f"Branch network: {branch_layer_size}") logger.info(f"Trunk network: {trunk_layer_size}") logger.info(f"Branch type: {branch_type}, Trunk type: {trunk_type}") @@ -186,139 +106,153 @@ def create_model(config: dict, metadata: dict) -> Prob_DeepONet: logger.info(f"Depth: {depth} (number of hidden layers)") logger.info(f"Input sensors (m): {m}") logger.info(f"Trunk input dimension (dim): {dim}") - + # Create model model = Prob_DeepONet( branch=branch_config, trunk=trunk_config, - use_bias=model_config['use_bias'] - ) - + use_bias=model_config["use_bias"]) + logger.info("Model created successfully") return model + def train(): """Main training function""" - parser = argparse.ArgumentParser(description='Train DeepONet-Grid-UQ model') - parser.add_argument('--config', type=str, default='configs/config.yaml', - help='Path to configuration file') - parser.add_argument('--data_path', type=str, default=None, - help='Path to data file (overrides config)') - parser.add_argument('--output_dir', type=str, default=None, - help='Output directory (overrides config)') - parser.add_argument('--epochs', type=int, default=10, - help='Number of epochs (default: 10)') - parser.add_argument('--batch_size', type=int, default=None, - help='Batch size (overrides config)') - parser.add_argument('--learning_rate', type=float, default=None, - help='Learning rate (overrides config)') - parser.add_argument('--resume', type=str, default=None, - help='Resume training from checkpoint') - parser.add_argument('--test_only', action='store_true', - help='Only run evaluation on test set') - + parser = argparse.ArgumentParser( + description="Train DeepONet-Grid-UQ model") + parser.add_argument( + "--config", + type=str, + default="configs/config.yaml", + help="Path to configuration file", + ) + parser.add_argument( + "--data_path", + type=str, + default=None, + help="Path to data file (overrides config)", + ) + parser.add_argument( + "--output_dir", + type=str, + default=None, + help="Output directory (overrides config)", + ) + parser.add_argument( + "--epochs", type=int, default=10, help="Number of epochs (default: 10)" + ) + parser.add_argument( + "--batch_size", + type=int, + default=None, + help="Batch size (overrides config)") + parser.add_argument( + "--learning_rate", + type=float, + default=None, + help="Learning rate (overrides config)", + ) + parser.add_argument( + "--resume", + type=str, + default=None, + help="Resume training from checkpoint") + parser.add_argument( + "--test_only", + action="store_true", + help="Only run evaluation on test set") + args = parser.parse_args() - + # Load configuration logger.info(f"Loading configuration from {args.config}") config = load_config(args.config) - + # Override config with command line arguments if args.data_path: - config['data']['data_path'] = args.data_path - config['data']['use_synthetic'] = False - + config["data"]["data_path"] = args.data_path + config["data"]["use_synthetic"] = False + if args.output_dir: - config['output']['save_dir'] = args.output_dir - + config["output"]["save_dir"] = args.output_dir + # Set epochs to 10 as requested - config['training']['epochs'] = args.epochs - + config["training"]["epochs"] = args.epochs + if args.batch_size: - config['training']['batch_size'] = args.batch_size - + config["training"]["batch_size"] = args.batch_size + if args.learning_rate: - config['training']['learning_rate'] = args.learning_rate - - # Set up device - setup_device(config) - + config["training"]["learning_rate"] = args.learning_rate + # Load and preprocess real data logger.info("Loading and preprocessing real training data...") datasets, metadata = load_and_preprocess_real_data(config) - + logger.info(f"Data loaded successfully:") logger.info(f" Input dimension: {metadata['input_dim']}") logger.info(f" Output dimension: {metadata['output_dim']}") logger.info(f" Total samples: {metadata['n_samples']}") logger.info(f" Number of sensors: {metadata['n_sensors']}") - + # Create model logger.info("Creating model...") model = create_model(config, metadata) - + # Create trainer logger.info("Creating trainer...") trainer = create_trainer( - model=model, - config=config, - save_dir=config['output']['save_dir'] + model=model, config=config, save_dir=config["output"]["save_dir"] ) - + # Load checkpoint if resuming if args.resume: logger.info(f"Resuming from checkpoint: {args.resume}") trainer.load_model(args.resume) - - if args.test_only: - # Only run evaluation - logger.info("Running evaluation only...") - metrics = trainer.evaluate(datasets['test']) - - # Save evaluation results - import json - results_path = os.path.join(config['output']['save_dir'], 'test_results.json') - with open(results_path, 'w') as f: - json.dump(metrics, f, indent=2) - logger.info(f"Test results saved to: {results_path}") - - else: - # print config before training - logger.info(f"Training config: {config}") - - # Train model - logger.info(f"Starting training for {config['training']['epochs']} epochs...") - history = trainer.train( - train_dataset=datasets['train'], - val_dataset=datasets['val'] - ) - - # Save training history - import json - history_path = os.path.join(config['output']['save_dir'], 'training_history.json') - with open(history_path, 'w') as f: - json.dump(history, f, indent=2) - logger.info(f"Training history saved to: {history_path}") - - # Evaluate on test set - logger.info("Evaluating on test set...") - test_metrics = trainer.evaluate(datasets['test']) - - # Save test results - test_results_path = os.path.join(config['output']['save_dir'], 'test_results.json') - with open(test_results_path, 'w') as f: - json.dump(test_metrics, f, indent=2) - logger.info(f"Test results saved to: {test_results_path}") - - # Save final model - trainer.save_model('final_model.ckpt') - - logger.info("Training completed successfully!") - - # Print final results - logger.info("Final Results:") - for metric, value in test_metrics.items(): - logger.info(f" {metric.upper()}: {value:.6f}") + + # print config before training + logger.info(f"Training config: {config}") + + # Train model + logger.info( + f"Starting training for {config['training']['epochs']} epochs...") + history = trainer.train( + train_dataset=datasets["train"], val_dataset=datasets["val"] + ) + + # Save training history + import json + + history_path = os.path.join( + config["output"]["save_dir"], + "training_history.json") + with open(history_path, "w") as f: + json.dump(history, f, indent=2) + logger.info(f"Training history saved to: {history_path}") + + # Evaluate on test set + logger.info("Evaluating on test set...") + test_metrics = trainer.evaluate(datasets["test"]) + + # Save test results + test_results_path = os.path.join( + config["output"]["save_dir"], + "test_results.json") + with open(test_results_path, "w") as f: + json.dump(test_metrics, f, indent=2) + logger.info(f"Test results saved to: {test_results_path}") + + # Save final model + trainer.save_model("final_model.ckpt") + + logger.info("Training completed successfully!") + + # Print final results + logger.info("Final Results:") + for metric, value in test_metrics.items(): + logger.info(f" {metric.upper()}: {value:.6f}") + if __name__ == "__main__": - train() \ No newline at end of file + train() -- Gitee From b67202547c164d9c81cc88537d615c80aaec40ca Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Thu, 17 Jul 2025 11:26:58 +0800 Subject: [PATCH 07/25] Fix comments. --- .../application/deeponet-grid/README.md | 65 ++++++-------- .../application/deeponet-grid/README_en.md | 86 ++----------------- 2 files changed, 33 insertions(+), 118 deletions(-) diff --git a/MindEnergy/application/deeponet-grid/README.md b/MindEnergy/application/deeponet-grid/README.md index 0ef0f7188..62ef42097 100644 --- a/MindEnergy/application/deeponet-grid/README.md +++ b/MindEnergy/application/deeponet-grid/README.md @@ -2,31 +2,6 @@ 用于电力系统故障后进行预测的DeepONet-Grid网络 -## 案例特性概述 - -### 需求来源及价值概述 - -本工作构建了一个高效的网络DeepONet-Grid,用于对故障后的电力系统进行动态安全分析,该网络 - - (i) 接收故障前和故障期间收集的轨迹作为输入,并且 - (ii) 输出预测的故障后轨迹。 - -此外,本网络还通过不确定性量化(Uncertainty Quantification)为其方法赋予了在效率与可靠/可信预测之间取得平衡的能力。 - -原始论文:[DeepONet-grid-UQ: A trustworthy deep operator framework for predicting the power grid's post-fault trajectories](!https://www.sciencedirect.com/science/article/abs/pii/S0925231223002503) - -原始代码仓:[Github Link](!https://github.com/cmoyacal/DeepONet-Grid-UQ) - - - -### 研究背景与动机 - -电力系统作为关键基础设施,其稳定性和可靠性对现代社会至关重要。然而,电网经常面临罕见但严重的故障和扰动,这些事件可能导致系统不稳定,甚至引发大规模停电。 - -传统的动态安全分析需要求解复杂的非线性微分代数方程组,计算成本极高,难以实现实时分析。随着电网的转型,电力公司迫切需要能够进行近实时的动态安全评估。 - -现有的机器学习方法主要关注二分类问题(稳定/不稳定),缺乏对故障后轨迹的定量预测能力。系统运营商和规划者需要了解故障后各种状态变量的轨迹,以评估电压或频率是否会违反预定义限制并触发负荷切除等保护措施。 - ## 项目结构 ``` @@ -36,13 +11,14 @@ deeponet-grid/ ├── src/ │ ├── model.py # DeepONet模型定义 │ ├── data.py # 数据加载和预处理 -│ ├── utils.py # 功能函数,比如打印loss曲线 +│ ├── utils.py # 一些有用的函数 │ ├── trainer.py # 训练器实现 │ └── metrics.py # 评估指标 -├── train.py # 主训练脚本 +├── train.py # 训练脚本 +├── inference.py # 推理脚本 ├── requirements.txt # 依赖包列表 ├── README.md # 本文件 -├── Prob_DeepONet_Grid.ipynb # 使用Prob_DeepONet的样例notebook +├── README_en.md # 英文版本 └── outputs/ # 输出目录(自动创建) ``` @@ -94,16 +70,25 @@ python train.py \ --learning_rate 1e-4 ``` -### 4. 从检查点恢复训练 +### 3. 从检查点恢复训练 ```bash python train.py --resume outputs/best_model.ckpt ``` -### 5. 仅运行评估 +### 4. 仅运行评估 ```bash -python train.py --test_only --resume outputs/best_model.ckpt +# 指定数据推理 +python inference.py --checkpoint outputs/best_model.ckpt \ + --data_path data/test-data.npz \ + --single_inference \ + --data_index 0 + +# 数据集推理 +python inference.py --checkpoint outputs/best_model.ckpt \ + --data_path data/test-data.npz \ + --output_dir inference_results ``` ## 数据格式 @@ -111,8 +96,8 @@ python train.py --test_only --resume outputs/best_model.ckpt 数据文件应为 `.npz` 格式,包含以下字段: - `u`: 输入函数值,形状为 `(n_samples, n_sensors)` -- `y`: 评估点,形状为 `(n_samples, n_points, 1)` , 在数据处理阶段会自动转为n_samples * n_points, 1) -- `s`: 真实解值,形状为 `(n_samples, n_points, 1)`, 在数据处理阶段会自动转为n_samples * n_points, 1) +- `y`: 评估点,形状为 `(n_samples, n_points, n_dim)` +- `s`: 真实解值,形状为 `(n_samples, n_points, n_output)` 示例: ```python @@ -174,6 +159,13 @@ G(u)(y) = Σᵢ bᵢ(u) tᵢ(y) - **R²**: 决定系数 - **Calibration Error**: 校准误差 +## 故障排除 + +### 常见问题 + +1. **内存不足**: 减小 `batch_size` +2. **训练缓慢**: 使用GPU或Ascend设备 +3. **收敛困难**: 调整学习率或使用学习率调度器 ### 调试模式 @@ -205,10 +197,3 @@ def load_custom_data(data_path): # 自定义数据加载逻辑 return u, y, s ``` - -# - -## 训练损失函数图 - - -![loss](images/loss_curves.png) \ No newline at end of file diff --git a/MindEnergy/application/deeponet-grid/README_en.md b/MindEnergy/application/deeponet-grid/README_en.md index 943191992..c0394b903 100644 --- a/MindEnergy/application/deeponet-grid/README_en.md +++ b/MindEnergy/application/deeponet-grid/README_en.md @@ -14,11 +14,11 @@ deeponet-grid/ │ ├── utils.py # Utility functions │ ├── trainer.py # Trainer implementation │ └── metrics.py # Evaluation metrics -├── train.py # Main training script +├── train.py # Training script ├── inference.py # Inference script -├── test_system.py # System test script ├── requirements.txt # Dependency list -├── README_en.md # This file +├── README_en.md # This file +├── README.md # Chinese version └── outputs/ # Output directory (auto-created) ``` @@ -68,19 +68,13 @@ python train.py \ --batch_size 64 --learning_rate 1e-4 ``` -### 3sume Training from Checkpoint +### 3. Resume Training from Checkpoint ```bash python train.py --resume outputs/best_model.ckpt ``` -###4un Evaluation Only - -```bash -python train.py --test_only --resume outputs/best_model.ckpt -``` - -###5Inference +### 4. Run Evaluation Only ```bash # Single data point inference @@ -95,6 +89,8 @@ python inference.py --checkpoint outputs/best_model.ckpt \ --output_dir inference_results ``` + + ## Data Format Data files should be in `.npz` format with the following fields: @@ -116,7 +112,7 @@ u = np.random.randn(n_samples, n_sensors) y = np.random.rand(n_samples, n_points, 1) * 2.0 s = np.sin(u.mean(axis=1, keepdims=True) * y.squeeze(-1)) s = s.reshape(n_samples, n_points, 1 Save data -np.savez(data.npz, u=u, y=y, s=s) +np.savez('data.npz', u=u, y=y, s=s) ``` ## Model Architecture @@ -163,10 +159,6 @@ After training, the following files are generated in the output directory: ## Troubleshooting -### Common Issues -1nsufficient Memory**: Reduce `batch_size` -2**Slow Training**: Use GPU or Ascend devices -3. **Convergence Difficulties**: Adjust learning rate or use learning rate scheduler ### Debug Mode @@ -176,21 +168,6 @@ export MINDSPORE_LOG_LEVEL=DEBUG python train.py ``` -## System Testing - -Run comprehensive system tests: -```bash -python test_system.py -``` - -This will test: -- Configuration loading -- Data generation and loading -- Model creation -- Training workflow -- Inference functionality -- Metrics calculation - ## Extension Features ### Custom Loss Functions @@ -213,50 +190,3 @@ def load_custom_data(data_path): # Custom data loading logic return u, y, s ``` - -## Key Features - -### 1. Adaptive Learning Rate Scheduling -The project supports learning rate scheduling strategies: - -- **CosineAnnealingLR**: Cosine annealing scheduling - -Configuration example: -```yaml -training: - scheduler: "cosine" # Scheduler type - patience: 100 # Patience value - factor: 08 # Decay factor - min_lr: 1e-7 # Minimum learning rate -``` - -### 2. Probabilistic Uncertainty Quantification -- Outputs mean and standard deviation -- Supports uncertainty quantification -- Uses negative log likelihood loss - -### 3. Data Processing -- Automatic multi-time-point data processing -- Data normalization -- Train/validation/test splitting - -### 4. Training Monitoring -- Print loss every 10s -- Validate every100ps -- Automatically save best model - -### 5. Trajectory Prediction -- Supports single sample and batch trajectory prediction -- Uncertainty quantification -- Confidence interval calculation - -## Comparison with Original PyTorch Version - -| Feature | PyTorch Version | MindSpore Version | -|---------|-----------------|-------------------| -| Framework | PyTorch | MindSpore | -| Device Support | GPU/CPU | CPU/GPU/Ascend | -| Model Structure | Same | Same | -| Loss Function | NLL | NLL | -| Data Processing | Manual expansion | Automatic expansion | -| Training Monitoring | Basic | Enhanced (step-level monitoring) | -- Gitee From cbcd8c74d61042d56d139329d661ce55bd65f25c Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Thu, 17 Jul 2025 13:10:47 +0800 Subject: [PATCH 08/25] Fix comments --- .../application/deeponet-grid/inference.py | 145 ++---------------- .../application/deeponet-grid/src/data.py | 53 ++++--- .../application/deeponet-grid/src/metrics.py | 3 +- .../application/deeponet-grid/src/model.py | 21 ++- .../application/deeponet-grid/src/trainer.py | 63 ++++---- .../application/deeponet-grid/src/utils.py | 43 ++++-- MindEnergy/application/deeponet-grid/train.py | 3 + 7 files changed, 126 insertions(+), 205 deletions(-) diff --git a/MindEnergy/application/deeponet-grid/inference.py b/MindEnergy/application/deeponet-grid/inference.py index 9bb5dbfa9..086736b01 100644 --- a/MindEnergy/application/deeponet-grid/inference.py +++ b/MindEnergy/application/deeponet-grid/inference.py @@ -237,7 +237,16 @@ def inference_on_dataset( # Load dataset logger.info(f"Loading dataset from: {dataset_path}") datasets, metadata = load_and_preprocess_real_data( - {"data": {"data_path": dataset_path}} + { + "data": { + "data_path": dataset_path, + "use_synthetic": False, + "train_ratio": 0.0, + "val_ratio": 0.0, + "test_ratio": 1.0, + }, + "training": {"batch_size": 1024}, + } ) # Create output directory @@ -293,134 +302,6 @@ def inference_on_dataset( logger.info(f"Targets shape: {targets_np.shape}") -def main(): - """Main inference function""" - parser = argparse.ArgumentParser(description="DeepONet-Grid-UQ Inference") - parser.add_argument( - "--config", - type=str, - default="configs/config.yaml", - help="Path to configuration file", - ) - parser.add_argument( - "--checkpoint", - type=str, - required=True, - help="Path to model checkpoint") - parser.add_argument( - "--data_path", - type=str, - default=None, - help="Path to data file for dataset inference", - ) - parser.add_argument( - "--output_dir", - type=str, - default="inference_results", - help="Output directory for results", - ) - parser.add_argument( - "--single_inference", - action="store_true", - help="Perform single data point inference", - ) - parser.add_argument( - "--data_index", - type=int, - default=0, - help="Index of data point for single inference (default: 0)", - ) - - args = parser.parse_args() - - # Load configuration - logger.info(f"Loading configuration from {args.config}") - config = load_config(args.config) - - # Load and preprocess data to get metadata - logger.info("Loading data to get metadata...") - datasets, metadata = load_and_preprocess_real_data(config) - - logger.info(f"Data metadata:") - logger.info(f" Input dimension: {metadata['input_dim']}") - logger.info(f" Output dimension: {metadata['output_dim']}") - logger.info(f" Total samples: {metadata['n_samples']}") - logger.info(f" Number of sensors: {metadata['n_sensors']}") - - # Create model - logger.info("Creating model...") - model = create_model(config, metadata) - - if args.single_inference: - # Single data point inference from dataset - if args.data_path is None: - logger.error("For single inference, --data_path must be provided") - return - - # Load trained model - model = load_trained_model(model, args.checkpoint) - logger.info(f"Model loaded from: {args.checkpoint}") - - # Load dataset and get specific data point - logger.info(f"Loading dataset from: {args.data_path}") - datasets, metadata = load_and_preprocess_real_data( - {"data": {"data_path": args.data_path}} - ) - - # Extract specific data point - u_data, y_data, target_data = get_single_data_from_dataset( - datasets, args.data_index - ) - - # Perform single inference - logger.info("Performing single inference...") - mean_pred, std_pred = single_inference(model, u_data, y_data) - - logger.info("Single inference results:") - logger.info(f" Data index: {args.data_index}") - logger.info(f" Target value: {target_data}") - logger.info(f" Mean prediction: {mean_pred}") - logger.info(f" Standard deviation: {std_pred}") - logger.info(f" Prediction error: {abs(mean_pred - target_data)}") - - # Save results - results = { - "data_index": args.data_index, - "target_value": target_data.tolist(), - "mean_prediction": mean_pred.tolist(), - "std_prediction": std_pred.tolist(), - "prediction_error": abs(mean_pred - target_data).tolist(), - "u_input": u_data.tolist(), - "y_input": y_data.tolist(), - } - - os.makedirs(args.output_dir, exist_ok=True) - results_path = os.path.join( - args.output_dir, f"single_inference_index_{args.data_index}.json" - ) - with open(results_path, "w") as f: - json.dump(results, f, indent=2) - - logger.info(f"Single inference results saved to: {results_path}") - - else: - # Dataset inference - if args.data_path is None: - logger.error("For dataset inference, --data_path must be provided") - return - - # Perform inference on dataset - inference_on_dataset( - model, - args.data_path, - args.checkpoint, - args.output_dir) - - logger.info("Dataset inference completed successfully!") - - -if __name__ == "__main__": - main() - # python inference.py --checkpoint outputs/best_model.ckpt \ - # --data_path data/test-data-voltage-m-33-mix.npz \ - # --output_dir inference_results +# python inference.py --checkpoint outputs/best_model.ckpt \ +# --data_path data/test-data-voltage-m-33-mix.npz \ +# --output_dir inference_results diff --git a/MindEnergy/application/deeponet-grid/src/data.py b/MindEnergy/application/deeponet-grid/src/data.py index 7ca0ca056..f81e886f7 100644 --- a/MindEnergy/application/deeponet-grid/src/data.py +++ b/MindEnergy/application/deeponet-grid/src/data.py @@ -29,8 +29,11 @@ class DataGenerator: """Data generator for MindSpore training""" def __init__( - self, u: np.ndarray, y: np.ndarray, s: np.ndarray, dtype: str = "float32" - ): + self, + u: np.ndarray, + y: np.ndarray, + s: np.ndarray, + dtype: str = "float32"): self.u = u self.y = y self.s = s @@ -96,7 +99,8 @@ def generate_synthetic_data( return expanded_u, expanded_y, expanded_s -def load_real_data(data_path: str) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: +def load_real_data( + data_path: str) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """Load real data from npz file Args: @@ -117,7 +121,8 @@ def load_real_data(data_path: str) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: required_fields = ["u", "y", "s"] for field in required_fields: if field not in data.files: - raise ValueError(f"Required field '{field}' not found in data file") + raise ValueError( + f"Required field '{field}' not found in data file") return data["u"], data["y"], data["s"] @@ -212,8 +217,8 @@ def split_data( y: np.ndarray, s: np.ndarray, train_ratio: float = 0.8, - val_split: float = 0.1, - test_split: float = 0.1, + val_ratio: float = 0.1, + test_ratio: float = 0.1, random_state: int = 42, ) -> Dict[str, Tuple]: """Split data into train, validation, and test sets @@ -223,15 +228,15 @@ def split_data( y: Evaluation points s: Solution values train_ratio: Training set fraction - val_split: Validation set fraction - test_split: Test set fraction + val_ratio: Validation set fraction + test_ratio: Test set fraction random_state: Random seed Returns: Dictionary containing train, validation, and test data """ assert ( - abs(train_ratio + val_split + test_split - 1.0) < 1e-6 + abs(train_ratio + val_ratio + test_ratio - 1.0) < 1e-6 ), "Splits must sum to 1.0" # Set random seed @@ -241,11 +246,11 @@ def split_data( indices = np.random.permutation(n_samples) n_train = int(train_ratio * n_samples) - n_val = int(val_split * n_samples) + n_val = int(val_ratio * n_samples) train_indices = indices[:n_train] - val_indices = indices[n_train : n_train + n_val] - test_indices = indices[n_train + n_val :] + val_indices = indices[n_train: n_train + n_val] + test_indices = indices[n_train + n_val:] data_splits = { "train": (u[train_indices], y[train_indices], s[train_indices]), @@ -332,8 +337,10 @@ def prepare_deeponet_data(u, y, s, time_points=None): u_expanded = np.repeat(u, n_points, axis=0) # create query points for each time point of each sample - y_expanded = np.tile(y.reshape(-1, 1), (n_samples, 1)) # (n_samples * n_points, 1) - s_expanded = np.tile(s.reshape(-1, 1), (n_samples, 1)) # (n_samples * n_points, 1) + y_expanded = np.tile(y.reshape(-1, 1), (n_samples, 1) + ) # (n_samples * n_points, 1) + s_expanded = np.tile(s.reshape(-1, 1), (n_samples, 1) + ) # (n_samples * n_points, 1) # # Expand data for DeepONet training # u_expanded = [] @@ -365,7 +372,10 @@ def prepare_deeponet_data(u, y, s, time_points=None): return u_expanded, y_expanded, s_expanded, metadata -def trajectory_prediction(u: np.ndarray, time_points: np.ndarray, model) -> np.ndarray: +def trajectory_prediction( + u: np.ndarray, + time_points: np.ndarray, + model) -> np.ndarray: """ Predict trajectory for a single sample using DeepONet model @@ -441,7 +451,8 @@ def batch_trajectory_prediction(model, u_batch, time_points): # reshape results mean_predictions = mean_pred.reshape(batch_size, n_time_points, 1) - std_predictions = ms.ops.exp(log_std_pred).reshape(batch_size, n_time_points, 1) + std_predictions = ms.ops.exp(log_std_pred).reshape( + batch_size, n_time_points, 1) return mean_predictions, std_predictions @@ -455,7 +466,8 @@ def load_and_preprocess_real_data(config: dict): # Prepare data for DeepONet (convert multi-time-point to single query # points) - u_expanded, y_expanded, s_expanded, prep_metadata = prepare_deeponet_data(u, y, s) + u_expanded, y_expanded, s_expanded, prep_metadata = prepare_deeponet_data( + u, y, s) # Normalize data # logger.info("Normalizing data...") @@ -467,12 +479,13 @@ def load_and_preprocess_real_data(config: dict): y_expanded, s_expanded, train_ratio=config["data"]["train_ratio"], - val_split=config["data"]["val_split"], - test_split=config["data"]["test_split"], + val_ratio=config["data"]["val_ratio"], + test_ratio=config["data"]["test_ratio"], ) # Create MindSpore datasets - datasets = create_datasets(data_splits, batch_size=config["training"]["batch_size"]) + datasets = create_datasets( + data_splits, batch_size=config["training"]["batch_size"]) # Prepare metadata metadata = { diff --git a/MindEnergy/application/deeponet-grid/src/metrics.py b/MindEnergy/application/deeponet-grid/src/metrics.py index e93a92acb..a65b57b49 100644 --- a/MindEnergy/application/deeponet-grid/src/metrics.py +++ b/MindEnergy/application/deeponet-grid/src/metrics.py @@ -88,7 +88,8 @@ class MetricsCalculator: ratio = float(self.reduce_mean(within_CI.astype(ms.float32))) if verbose: - print(f"% of the true traj. within the error bars is {100 * ratio:.3f}") + print( + f"% of the true traj. within the error bars is {100 * ratio:.3f}") return ratio diff --git a/MindEnergy/application/deeponet-grid/src/model.py b/MindEnergy/application/deeponet-grid/src/model.py index 87af63ffc..d92819bd9 100644 --- a/MindEnergy/application/deeponet-grid/src/model.py +++ b/MindEnergy/application/deeponet-grid/src/model.py @@ -26,7 +26,8 @@ class MLP(nn.Cell): super(MLP, self).__init__() layers = [] for k in range(len(layer_size) - 2): - layers.append(nn.Dense(layer_size[k], layer_size[k + 1], has_bias=True)) + layers.append( + nn.Dense(layer_size[k], layer_size[k + 1], has_bias=True)) layers.append(get_activation(activation)) layers.append(nn.Dense(layer_size[-2], layer_size[-1], has_bias=True)) self.net = nn.SequentialCell(layers) @@ -49,7 +50,8 @@ class modified_MLP(nn.Cell): super(modified_MLP, self).__init__() layers = [] for k in range(len(layer_size) - 1): - layers.append(nn.Dense(layer_size[k], layer_size[k + 1], has_bias=True)) + layers.append( + nn.Dense(layer_size[k], layer_size[k + 1], has_bias=True)) self.net = nn.SequentialCell(layers) self.U = nn.SequentialCell( @@ -115,13 +117,15 @@ class DeepONet(nn.Cell): Base DeepONet class that serves as an interface for different DeepONet implementations. """ - def __init__(self, branch: dict, trunk: dict, use_bias: bool = True) -> None: + def __init__(self, branch: dict, trunk: dict, + use_bias: bool = True) -> None: super(DeepONet, self).__init__() # Branch if branch["type"] == "MLP": self.branch = MLP(branch["layer_size"][:-2], branch["activation"]) elif branch["type"] == "modified": - self.branch = modified_MLP(branch["layer_size"][:-2], branch["activation"]) + self.branch = modified_MLP( + branch["layer_size"][:-2], branch["activation"]) else: raise ValueError( f"Unsupported branch type: {branch['type']}. Supported: 'MLP', 'modified'." @@ -130,7 +134,8 @@ class DeepONet(nn.Cell): if trunk["type"] == "MLP": self.trunk = MLP(trunk["layer_size"][:-2], trunk["activation"]) elif trunk["type"] == "modified": - self.trunk = modified_MLP(trunk["layer_size"][:-2], trunk["activation"]) + self.trunk = modified_MLP( + trunk["layer_size"][:-2], trunk["activation"]) else: raise ValueError( f"Unsupported trunk type: {trunk['type']}. Supported: 'MLP', 'modified'." @@ -157,11 +162,13 @@ class DeepONet(nn.Cell): Returns: Model output (to be defined by subclasses) """ - raise NotImplementedError("construct method must be implemented by subclasses") + raise NotImplementedError( + "construct method must be implemented by subclasses") class Prob_DeepONet(DeepONet): - def __init__(self, branch: dict, trunk: dict, use_bias: bool = True) -> None: + def __init__(self, branch: dict, trunk: dict, + use_bias: bool = True) -> None: super(Prob_DeepONet, self).__init__(branch, trunk, use_bias) # Add probabilistic components diff --git a/MindEnergy/application/deeponet-grid/src/trainer.py b/MindEnergy/application/deeponet-grid/src/trainer.py index 99448c067..da48b0d5f 100644 --- a/MindEnergy/application/deeponet-grid/src/trainer.py +++ b/MindEnergy/application/deeponet-grid/src/trainer.py @@ -33,14 +33,8 @@ from mindspore.train.loss_scale_manager import DynamicLossScaleManager from tqdm.auto import trange # Import metrics -from .metrics import ( - compute_calibration_error, - compute_mae, - compute_metrics, - compute_mse, - compute_r2_score, - update_metrics_history, -) +from .metrics import (compute_calibration_error, compute_mae, compute_metrics, + compute_mse, compute_r2_score, update_metrics_history) # Set up logging logging.basicConfig(level=logging.INFO) @@ -145,17 +139,15 @@ class DeepONetTrainer: self.save_dir = save_dir self.training_config = config["training"] - # Create save directory - os.makedirs(save_dir, exist_ok=True) - # Initialize optimizer optimizer_type = self.training_config.get("optimizer", "adam") - if self.training_config.get("scheduler", None) is "cosine": - min_lr = self.training_config.get("min_lr", 1e-7) - max_lr = self.training_config.get("max_lr", 1e-3) - total_step = self.training_config.get("total_step", 10000) - step_per_epoch = self.training_config.get("step_per_epoch", 1000) - decay_epoch = self.training_config.get("decay_epoch", 1000) + if self.training_config.get("scheduler", None) == "cosine": + min_lr = float(self.training_config.get("min_lr", 1e-7)) + max_lr = float(self.training_config.get("max_lr", 1e-3)) + total_step = int(self.training_config.get("total_step", 10000)) + step_per_epoch = int( + self.training_config.get("step_per_epoch", 1000)) + decay_epoch = int(self.training_config.get("decay_epoch", 1000)) learning_rate = nn.cosine_decay_lr( min_lr, max_lr, total_step, step_per_epoch, decay_epoch ) # Ensure it's a float @@ -163,7 +155,7 @@ class DeepONetTrainer: learning_rate = float( self.training_config["learning_rate"] ) # Ensure it's a float - weight_decay = self.training_config.get("weight_decay", 0.0) + weight_decay = float(self.training_config.get("weight_decay", 0.0)) if optimizer_type.lower() == "adam": self.optimizer = nn.Adam( @@ -272,16 +264,18 @@ class DeepONetTrainer: train_data_size = ( train_dataset.get_dataset_size() * train_dataset.get_batch_size() ) - logging.info(f"Training data size (number of samples): {train_data_size}") + logging.info( + f"Training data size (number of samples): {train_data_size}") logging.info(f"Training batch size : {train_dataset.get_batch_size()}") # Print all parameter names, shapes, and sizes for debugging - print("Model parameter details:") + logger.info("Model parameter details:") for p in self.model.get_parameters(): - print(f"{p.name}: shape={p.shape}, size={p.size}") + logger.info(f"{p.name}: shape={p.shape}, size={p.size}") if verbose: - print(f"\n***** Probabilistic Training for {epochs} epochs *****\n") + logger.info( + f"\n***** Probabilistic Training for {epochs} epochs *****\n") # Initialize best values and logger best = {} @@ -311,9 +305,7 @@ class DeepONetTrainer: msg = ( f"Epoch {epoch+1}, Step {global_step}, Batch {batch_count}, " f"Loss: {float(loss):.6f}, " - f"Step time: {step_time:.3f}s" - ) - # print(msg) + f"Step time: {step_time:.3f}s") logging.info(msg) # evaluate if val_dataset is not None and global_step % eval_every == 0: @@ -321,7 +313,6 @@ class DeepONetTrainer: logger_hist["val loss"].append((global_step, val_loss)) # Negative log likelihood loss can be negative msg = f"[Eval] Epoch {epoch+1}, Step {global_step}, Val-Loss: {float(val_loss):.6f} " - # print(msg) logging.info(msg) # save best ckpt if val_loss < best["prob loss"]: @@ -329,21 +320,22 @@ class DeepONetTrainer: self.save_model("best_model.ckpt") # record current learning rate - current_lr = float(self.optimizer.learning_rate) + current_lr = self.optimizer.learning_rate logger.info(f"Current learning rate: {current_lr:.2e}") # Compute average epoch loss try: avg_epoch_loss = epoch_loss / batch_count except ZeroDivisionError as e: - print(f"error: {e}, batch size larger than number of training examples") + logger.error( + f"error: {e}, batch size larger than number of training examples") continue logger_hist["prob loss"].append(avg_epoch_loss) # show after each epoch if verbose: # Negative log likelihood loss can be negative - print(f"Epoch {epoch+1}/{epochs}:") - print(f" Train-Loss: {float(avg_epoch_loss):.6f} ") - print(f' Best-Loss: {float(best["prob loss"]):.6f} ') + logger.info(f"Epoch {epoch+1}/{epochs}:") + logger.info(f" Train-Loss: {float(avg_epoch_loss):.6f} ") + logger.info(f' Best-Loss: {float(best["prob loss"]):.6f} ') return logger_hist def save_model(self, filename: str): @@ -369,7 +361,8 @@ class DeepONetTrainer: else: logger.warning(f"Checkpoint not found: {load_path}") - def predict(self, u: ms.Tensor, y: ms.Tensor) -> Tuple[ms.Tensor, ms.Tensor]: + def predict(self, u: ms.Tensor, + y: ms.Tensor) -> Tuple[ms.Tensor, ms.Tensor]: """Make predictions Args: @@ -439,14 +432,16 @@ class DeepONetTrainer: ) # Compute uncertainty quantification metrics - metrics["calibration_error"] = compute_calibration_error(targets, means, stds) + metrics["calibration_error"] = compute_calibration_error( + targets, means, stds) metrics["fraction_in_CI"] = calculator.fraction_in_CI( targets, means, stds, xi=2.0 ) # Compute trajectory errors if data is structured as trajectories try: - l1_traj, l2_traj = calculator.trajectory_rel_error(targets, predictions) + l1_traj, l2_traj = calculator.trajectory_rel_error( + targets, predictions) metrics["trajectory_l1_error"] = l1_traj metrics["trajectory_l2_error"] = l2_traj except BaseException: diff --git a/MindEnergy/application/deeponet-grid/src/utils.py b/MindEnergy/application/deeponet-grid/src/utils.py index 2a25e465a..de329aab9 100644 --- a/MindEnergy/application/deeponet-grid/src/utils.py +++ b/MindEnergy/application/deeponet-grid/src/utils.py @@ -27,7 +27,8 @@ import numpy as np import yaml -def parse_loss_log(log_file_path: str) -> Tuple[List[float], List[int], List[int]]: +def parse_loss_log( + log_file_path: str) -> Tuple[List[float], List[int], List[int]]: """ Parse loss log file and extract loss values, epochs, and steps @@ -69,7 +70,8 @@ def parse_loss_log(log_file_path: str) -> Tuple[List[float], List[int], List[int return losses, epochs, steps -def parse_val_loss_log(log_file_path: str) -> Tuple[List[float], List[int], List[int]]: +def parse_val_loss_log( + log_file_path: str) -> Tuple[List[float], List[int], List[int]]: """ Parse validation loss log file and extract validation loss values @@ -138,7 +140,13 @@ def plot_loss_curves( fig, (ax1, ax2) = plt.subplots(2, 1, figsize=figsize) # Plot training loss - ax1.plot(steps, losses, "b-", label="Training Loss", linewidth=1, alpha=0.8) + ax1.plot( + steps, + losses, + "b-", + label="Training Loss", + linewidth=1, + alpha=0.8) ax1.set_xlabel("Training Steps") ax1.set_ylabel("Loss (negated for display)") ax1.set_title("Training Loss Curve") @@ -148,8 +156,12 @@ def plot_loss_curves( # Plot validation loss if available if val_losses: ax1.plot( - val_steps, val_losses, "r-", label="Validation Loss", linewidth=1, alpha=0.8 - ) + val_steps, + val_losses, + "r-", + label="Validation Loss", + linewidth=1, + alpha=0.8) ax1.legend() # Plot loss vs epochs @@ -160,7 +172,12 @@ def plot_loss_curves( epoch_loss = np.mean([losses[i] for i in epoch_indices]) epoch_losses.append(epoch_loss) - ax2.plot(unique_epochs, epoch_losses, "g-", label="Average Epoch Loss", linewidth=2) + ax2.plot( + unique_epochs, + epoch_losses, + "g-", + label="Average Epoch Loss", + linewidth=2) ax2.set_xlabel("Epochs") ax2.set_ylabel("Average Loss (negated for display)") ax2.set_title("Average Loss per Epoch") @@ -222,7 +239,7 @@ def plot_loss_statistics( moving_avg = np.convolve( losses, np.ones(window_size) / window_size, mode="valid" ) - steps_avg = steps[window_size - 1 :] + steps_avg = steps[window_size - 1:] ax2.plot(steps_avg, moving_avg, "r-", linewidth=1.5) ax2.set_xlabel("Training Steps") ax2.set_ylabel("Moving Average Loss") @@ -256,8 +273,11 @@ def plot_loss_statistics( ax5 = plt.subplot(2, 3, 5) if val_losses: ax5.plot( - val_steps, val_losses, "orange", linewidth=1.5, label="Validation Loss" - ) + val_steps, + val_losses, + "orange", + linewidth=1.5, + label="Validation Loss") ax5.set_xlabel("Training Steps") ax5.set_ylabel("Validation Loss") ax5.set_title("Validation Loss") @@ -344,8 +364,9 @@ def load_training_history(history_file_path: str) -> Dict: def plot_training_history( - history_file_path: str, save_path: Optional[str] = None, show_plot: bool = True -) -> None: + history_file_path: str, + save_path: Optional[str] = None, + show_plot: bool = True) -> None: """ Plot training history from JSON file diff --git a/MindEnergy/application/deeponet-grid/train.py b/MindEnergy/application/deeponet-grid/train.py index 2708ed5f7..d67a81139 100644 --- a/MindEnergy/application/deeponet-grid/train.py +++ b/MindEnergy/application/deeponet-grid/train.py @@ -177,6 +177,9 @@ def train(): if args.output_dir: config["output"]["save_dir"] = args.output_dir + # Create save directory + os.makedirs(config["output"]["save_dir"], exist_ok=True) + logger.info(f"Save directory created: {config['output']['save_dir']}") # Set epochs to 10 as requested config["training"]["epochs"] = args.epochs -- Gitee From 7d5e7684488f258a6e832575486503e3486f984f Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Thu, 17 Jul 2025 14:35:14 +0800 Subject: [PATCH 09/25] Edit inference pipeline. --- .../application/deeponet-grid/README.md | 11 +- .../application/deeponet-grid/README_en.md | 2 +- .../application/deeponet-grid/inference.py | 189 ++++++++++-------- 3 files changed, 107 insertions(+), 95 deletions(-) diff --git a/MindEnergy/application/deeponet-grid/README.md b/MindEnergy/application/deeponet-grid/README.md index 62ef42097..32e4cde7f 100644 --- a/MindEnergy/application/deeponet-grid/README.md +++ b/MindEnergy/application/deeponet-grid/README.md @@ -81,13 +81,13 @@ python train.py --resume outputs/best_model.ckpt ```bash # 指定数据推理 python inference.py --checkpoint outputs/best_model.ckpt \ - --data_path data/test-data.npz \ - --single_inference \ + --data_path data/test-data-voltage-m-33-mix.npz \ + --trajectory_prediction \ --data_index 0 # 数据集推理 python inference.py --checkpoint outputs/best_model.ckpt \ - --data_path data/test-data.npz \ + --data_path data/test-data-voltage-m-33-mix.npz \ --output_dir inference_results ``` @@ -161,11 +161,6 @@ G(u)(y) = Σᵢ bᵢ(u) tᵢ(y) ## 故障排除 -### 常见问题 - -1. **内存不足**: 减小 `batch_size` -2. **训练缓慢**: 使用GPU或Ascend设备 -3. **收敛困难**: 调整学习率或使用学习率调度器 ### 调试模式 diff --git a/MindEnergy/application/deeponet-grid/README_en.md b/MindEnergy/application/deeponet-grid/README_en.md index c0394b903..888ebee07 100644 --- a/MindEnergy/application/deeponet-grid/README_en.md +++ b/MindEnergy/application/deeponet-grid/README_en.md @@ -80,7 +80,7 @@ python train.py --resume outputs/best_model.ckpt # Single data point inference python inference.py --checkpoint outputs/best_model.ckpt \ --data_path data/test-data.npz \ - --single_inference \ + --trajectory_prediction \ --data_index 0 # Dataset inference diff --git a/MindEnergy/application/deeponet-grid/inference.py b/MindEnergy/application/deeponet-grid/inference.py index 086736b01..0cf12ac03 100644 --- a/MindEnergy/application/deeponet-grid/inference.py +++ b/MindEnergy/application/deeponet-grid/inference.py @@ -15,7 +15,12 @@ import numpy as np import yaml from mindspore import context -from src.data import load_and_preprocess_real_data +from src.data import ( + load_real_data, + load_and_preprocess_real_data, + trajectory_prediction, + batch_trajectory_prediction, +) from src.model import Prob_DeepONet from src.utils import load_config, load_trained_model @@ -40,7 +45,7 @@ logger.info(f"Mode set to: {context.get_context('mode')}") logger.info(f"Device set to: {context.get_context('device_target')}") -def create_model(config: dict, metadata: dict) -> Prob_DeepONet: +def create_model(config: dict, n_sensors=33) -> Prob_DeepONet: """Create DeepONet model based on configuration and data metadata Args: @@ -54,9 +59,7 @@ def create_model(config: dict, metadata: dict) -> Prob_DeepONet: # Get network parameters from metadata if available, otherwise use config # defaults - m = metadata.get( - "n_sensors", model_config.get( - "m", 33)) # Number of sensors + m = n_sensors # Number of sensors # Input dimension for trunk (always 1 for DeepONet) dim = model_config.get("dim", 1) width = model_config.get("width", 200) # Network width @@ -105,30 +108,6 @@ def create_model(config: dict, metadata: dict) -> Prob_DeepONet: return model -def single_inference( - model: Prob_DeepONet, u: np.ndarray, y: np.ndarray -) -> Tuple[np.ndarray, np.ndarray]: - - # Convert to MindSpore tensors - u_tensor = ms.Tensor(u, ms.float32) - y_tensor = ms.Tensor(y, ms.float32) - - # Set model to evaluation mode - model.set_train(False) - - # Forward pass - mean_pred, log_std_pred = model(u_tensor, y_tensor) - - # Convert log_std to std - std_pred = ms.ops.Exp()(log_std_pred) - - # Convert back to numpy - mean_np = mean_pred.asnumpy() - std_np = std_pred.asnumpy() - - return mean_np, std_np - - def batch_inference( model: Prob_DeepONet, u: np.ndarray, y: np.ndarray ) -> Tuple[np.ndarray, np.ndarray]: @@ -162,60 +141,6 @@ def batch_inference( return mean_np, std_np -def get_single_data_from_dataset( - datasets: dict, data_index: int = 0 -) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: - """Extract single data point from dataset - - Args: - datasets: Dictionary containing train/val/test datasets - data_index: Index of the data point to extract (default: 0) - - Returns: - Tuple of (u, y, target) for the specified data point - """ - # Use test dataset for inference - test_dataset = datasets["test"] - - # Get dataset size - dataset_size = test_dataset.get_dataset_size() - batch_size = test_dataset.get_batch_size() - - logger.info( - f"Dataset size: {dataset_size} batches, batch size: {batch_size}") - - # Calculate which batch and position in batch - batch_index = data_index // batch_size - position_in_batch = data_index % batch_size - - if batch_index >= dataset_size: - raise ValueError( - f"Data index {data_index} exceeds dataset size {dataset_size * batch_size}" - ) - - # Get the specific data point from the dataset - batch_count = 0 - for u, y, target in test_dataset: - if batch_count == batch_index: - # Extract the specific data point from the batch - u_single = u[position_in_batch].asnumpy() - y_single = y[position_in_batch].asnumpy() - target_single = target[position_in_batch].asnumpy() - - logger.info( - f"Extracted data point {data_index} from batch {batch_index}, position {position_in_batch}" - ) - logger.info( - f"u shape: {u_single.shape}, y shape: {y_single.shape}, target shape: {target_single.shape}" - ) - - return u_single, y_single, target_single - - batch_count += 1 - - raise ValueError(f"Could not find data point {data_index}") - - def inference_on_dataset( model: Prob_DeepONet, dataset_path: str, @@ -302,6 +227,98 @@ def inference_on_dataset( logger.info(f"Targets shape: {targets_np.shape}") -# python inference.py --checkpoint outputs/best_model.ckpt \ -# --data_path data/test-data-voltage-m-33-mix.npz \ -# --output_dir inference_results +def inference(): + """Main inference function""" + parser = argparse.ArgumentParser(description="DeepONet-Grid-UQ Inference") + parser.add_argument( + "--config", + type=str, + default="configs/config.yaml", + help="Path to configuration file", + ) + parser.add_argument( + "--checkpoint", + type=str, + required=True, + help="Path to model checkpoint") + parser.add_argument( + "--data_path", + type=str, + default=None, + help="Path to data file for dataset inference", + ) + parser.add_argument( + "--output_dir", + type=str, + default="inference_results", + help="Output directory for results", + ) + parser.add_argument( + "--trajectory_prediction", + action="store_true", + help="Perform single data point (with multiple time points) inference", + ) + parser.add_argument( + "--data_index", + type=int, + default=0, + help="Index of data point for single inference (default: 0)", + ) + + args = parser.parse_args() + + # Load configuration + logger.info(f"Loading configuration from {args.config}") + config = load_config(args.config) + + # Create model + logger.info("Creating model...") + model = create_model(config, n_sensors=33) + + if args.trajectory_prediction: + # Single data point inference from dataset + # Load trained model + model = load_trained_model(model, args.checkpoint) + logger.info(f"Model loaded from: {args.checkpoint}") + + if args.data_path is None: + logger.error("For single inference, --data_path must be provided") + return + + # Load dataset and get specific data point + logger.info(f"Loading dataset from: {args.data_path}") + u, y, s = load_real_data(args.data_path) + + # Test single sample trajectory prediction + u_single = u[args.data_index] + y_single = y[args.data_index] + + predictions = trajectory_prediction(u_single, y_single, model) + print(predictions) + logger.info(f" Single sample prediction shape: {predictions.shape}") + logger.info(f" Predictions: {predictions.flatten()}") + + # batch trajectory prediction + # u_batch = u[:args.data_index] # First 3 samples, shape (3, 33) + # y_batch = y[:args.data_index] # First 3 samples, shape (3, 100) + # batch_predictions = batch_trajectory_prediction( + # u_batch, y_batch, model) + + else: + # Dataset inference + if args.data_path is None: + logger.error("For dataset inference, --data_path must be provided") + return + + # Perform inference on dataset + inference_on_dataset( + model, + args.data_path, + args.checkpoint, + args.output_dir) + + logger.info("Dataset inference completed successfully!") + + +if __name__ == "__main__": + inference() -- Gitee From 246d27fdc7592700cf89ea23e7e625bc8ec984b6 Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Thu, 17 Jul 2025 15:08:54 +0800 Subject: [PATCH 10/25] Fix normalizaiton --- .../application/deeponet-grid/inference.py | 19 ++++++++++++++++-- .../application/deeponet-grid/src/data.py | 20 +++++++++---------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/MindEnergy/application/deeponet-grid/inference.py b/MindEnergy/application/deeponet-grid/inference.py index 0cf12ac03..8c260e805 100644 --- a/MindEnergy/application/deeponet-grid/inference.py +++ b/MindEnergy/application/deeponet-grid/inference.py @@ -22,6 +22,7 @@ from src.data import ( batch_trajectory_prediction, ) from src.model import Prob_DeepONet +from src.metrics import MetricsCalculator from src.utils import load_config, load_trained_model # Add src to path @@ -240,11 +241,12 @@ def inference(): "--checkpoint", type=str, required=True, + default="outputs/best_model.ckpt", help="Path to model checkpoint") parser.add_argument( "--data_path", type=str, - default=None, + default="data/test-data-voltage-m-33-mix.npz", help="Path to data file for dataset inference", ) parser.add_argument( @@ -297,7 +299,20 @@ def inference(): print(predictions) logger.info(f" Single sample prediction shape: {predictions.shape}") logger.info(f" Predictions: {predictions.flatten()}") - + logger.info(f" Target: {s[args.data_index].flatten()}") + + calculator = MetricsCalculator() + y_true = ms.Tensor(s[args.data_index], ms.float32) + y_pred = ms.Tensor(predictions, ms.float32) + # Test L1 and L2 relative errors + l1_error = calculator.l1_relative_error(y_true, y_pred) + l2_error = calculator.l2_relative_error(y_true, y_pred) + print(f" L1 relative error: {l1_error:.6f}") + print(f" L2 relative error: {l2_error:.6f}") + + # Test trajectory relative error + l1_traj, l2_traj = calculator.trajectory_rel_error(y_true, y_pred) + print(f" Trajectory L1: {l1_traj:.6f}, L2: {l2_traj:.6f}") # batch trajectory prediction # u_batch = u[:args.data_index] # First 3 samples, shape (3, 33) # y_batch = y[:args.data_index] # First 3 samples, shape (3, 100) diff --git a/MindEnergy/application/deeponet-grid/src/data.py b/MindEnergy/application/deeponet-grid/src/data.py index f81e886f7..a11da6a04 100644 --- a/MindEnergy/application/deeponet-grid/src/data.py +++ b/MindEnergy/application/deeponet-grid/src/data.py @@ -169,21 +169,20 @@ def normalize_data( if method == "standard": # Standard normalization: (x - mean) / std u_mean = reduce_mean(u_tensor, 0) - u_std = reduce_std(u_tensor) - u_norm = (u_tensor - u_mean) / (u_std + 1e-8) - + u_std = reduce_std(u_tensor)[0] + u_norm = (u_tensor - u_mean) / (u_std[0] + 1e-8) y_mean = reduce_mean(y_tensor, 0) y_std = reduce_std(y_tensor) - y_norm = (y_tensor - y_mean) / (y_std + 1e-8) - + y_norm = (y_tensor - y_mean) / (y_std[0] + 1e-8) s_mean = reduce_mean(s_tensor, 0) s_std = reduce_std(s_tensor) - s_norm = (s_tensor - s_mean) / (s_std + 1e-8) - + s_norm = (s_tensor - s_mean) / (s_std[0] + 1e-8) # Store scalers for later use - scalers["u"] = {"mean": u_mean.asnumpy(), "std": u_std.asnumpy()} - scalers["y"] = {"mean": y_mean.asnumpy(), "std": y_std.asnumpy()} - scalers["s"] = {"mean": s_mean.asnumpy(), "std": s_std.asnumpy()} + scalers["u"] = {"mean": u_mean.asnumpy(), "std[0]": u_std[0].asnumpy()} + scalers["y"] = {"mean": y_mean.asnumpy(), "std[0]": y_std[0].asnumpy()} + scalers["s"] = {"mean": s_mean.asnumpy(), "std[0]": s_std[0].asnumpy()} + + print(2) elif method == "minmax": # Min-max normalization: (x - min) / (max - min) @@ -203,6 +202,7 @@ def normalize_data( scalers["u"] = {"min": u_min.asnumpy(), "max": u_max.asnumpy()} scalers["y"] = {"min": y_min.asnumpy(), "max": y_max.asnumpy()} scalers["s"] = {"min": s_min.asnumpy(), "max": s_max.asnumpy()} + print(3) # Convert back to numpy u_norm = u_norm.asnumpy() -- Gitee From 1e62f5981ffc77f9a64cc2dfa4b56150e4af215f Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Thu, 17 Jul 2025 17:39:47 +0800 Subject: [PATCH 11/25] Fix comments. --- .../application/deeponet-grid/inference.py | 14 +++--- .../application/deeponet-grid/src/data.py | 45 +++++++++++++------ .../application/deeponet-grid/src/trainer.py | 14 +----- MindEnergy/application/deeponet-grid/train.py | 17 ++++++- 4 files changed, 56 insertions(+), 34 deletions(-) diff --git a/MindEnergy/application/deeponet-grid/inference.py b/MindEnergy/application/deeponet-grid/inference.py index 8c260e805..0d9811473 100644 --- a/MindEnergy/application/deeponet-grid/inference.py +++ b/MindEnergy/application/deeponet-grid/inference.py @@ -240,7 +240,6 @@ def inference(): parser.add_argument( "--checkpoint", type=str, - required=True, default="outputs/best_model.ckpt", help="Path to model checkpoint") parser.add_argument( @@ -296,8 +295,8 @@ def inference(): y_single = y[args.data_index] predictions = trajectory_prediction(u_single, y_single, model) - print(predictions) - logger.info(f" Single sample prediction shape: {predictions.shape}") + logger.info( + f" Single sample prediction shape: {predictions.shape} on data index {args.data_index}") logger.info(f" Predictions: {predictions.flatten()}") logger.info(f" Target: {s[args.data_index].flatten()}") @@ -307,13 +306,14 @@ def inference(): # Test L1 and L2 relative errors l1_error = calculator.l1_relative_error(y_true, y_pred) l2_error = calculator.l2_relative_error(y_true, y_pred) - print(f" L1 relative error: {l1_error:.6f}") - print(f" L2 relative error: {l2_error:.6f}") + logger.info(f" L1 relative error: {l1_error:.6f}") + logger.info(f" L2 relative error: {l2_error:.6f}") # Test trajectory relative error l1_traj, l2_traj = calculator.trajectory_rel_error(y_true, y_pred) - print(f" Trajectory L1: {l1_traj:.6f}, L2: {l2_traj:.6f}") - # batch trajectory prediction + logger.info(f" Trajectory L1: {l1_traj:.6f}, L2: {l2_traj:.6f}") + + # batch trajectory prediction, if you want to inference [:data_index] # u_batch = u[:args.data_index] # First 3 samples, shape (3, 33) # y_batch = y[:args.data_index] # First 3 samples, shape (3, 100) # batch_predictions = batch_trajectory_prediction( diff --git a/MindEnergy/application/deeponet-grid/src/data.py b/MindEnergy/application/deeponet-grid/src/data.py index a11da6a04..fd7dde347 100644 --- a/MindEnergy/application/deeponet-grid/src/data.py +++ b/MindEnergy/application/deeponet-grid/src/data.py @@ -182,8 +182,6 @@ def normalize_data( scalers["y"] = {"mean": y_mean.asnumpy(), "std[0]": y_std[0].asnumpy()} scalers["s"] = {"mean": s_mean.asnumpy(), "std[0]": s_std[0].asnumpy()} - print(2) - elif method == "minmax": # Min-max normalization: (x - min) / (max - min) u_min = reduce_min(u_tensor, 0) @@ -202,7 +200,6 @@ def normalize_data( scalers["u"] = {"min": u_min.asnumpy(), "max": u_max.asnumpy()} scalers["y"] = {"min": y_min.asnumpy(), "max": y_max.asnumpy()} scalers["s"] = {"min": s_min.asnumpy(), "max": s_max.asnumpy()} - print(3) # Convert back to numpy u_norm = u_norm.asnumpy() @@ -391,23 +388,43 @@ def trajectory_prediction( # Ensure u is 2D for model input if len(u.shape) == 1: u = u.reshape(1, -1) # (1, n_sensors) + n_time_points = len(time_points) # Convert to MindSpore tensor - u_tensor = ms.Tensor(u, ms.float32) - predictions = [] - # Predict for each time point - for t in time_points: - # Create single query point - y_t = ms.Tensor([[t]], ms.float32) # (1, 1) + # Predict for each time point using batch + + # create query points for each time point of the sample + u_expanded = np.repeat( + u, n_time_points, axis=0 + ) + y_expanded = np.tile( + time_points.reshape(-1, 1), (1, 1) + ) # (batch_size * n_time_points, 1) + + # convert to MindSpore tensors + u_tensor = ms.Tensor(u_expanded, ms.float32) + y_tensor = ms.Tensor(y_expanded, ms.float32) + + # one inference to get all predictions + mean_pred, log_std_pred = model(u_tensor, y_tensor) + + # reshape results + predictions = mean_pred.reshape(1, n_time_points, 1) + std_predictions = ms.ops.exp(log_std_pred).reshape( + 1, n_time_points, 1) + + # for t in time_points: + # # Create single query point + # y_t = ms.Tensor([[t]], ms.float32) # (1, 1) - # Forward pass - mean_pred, log_std_pred = model(u_tensor, y_t) + # # Forward pass + # mean_pred, log_std_pred = model(u_tensor, y_t) - # Get prediction value - pred_val = float(mean_pred[0, 0]) - predictions.append(pred_val) + # # Get prediction value + # pred_val = float(mean_pred[0, 0]) + # predictions.append(pred_val) return np.array(predictions).reshape(-1, 1) diff --git a/MindEnergy/application/deeponet-grid/src/trainer.py b/MindEnergy/application/deeponet-grid/src/trainer.py index da48b0d5f..3d99a596b 100644 --- a/MindEnergy/application/deeponet-grid/src/trainer.py +++ b/MindEnergy/application/deeponet-grid/src/trainer.py @@ -14,27 +14,18 @@ # ============================================================================== import logging import os -import time from typing import Any, Dict, List, Optional, Tuple import mindspore as ms import mindspore.nn as nn -import mindspore.numpy as mnp import mindspore.ops as ops import numpy as np -import yaml -from mindspore import Parameter, context -from mindspore.common.initializer import XavierNormal, Zero, initializer from mindspore.dataset import GeneratorDataset -from mindspore.train import Model -from mindspore.train.amp import auto_mixed_precision -from mindspore.train.callback import Callback, LossMonitor, TimeMonitor -from mindspore.train.loss_scale_manager import DynamicLossScaleManager -from tqdm.auto import trange +from mindspore.train.callback import Callback # Import metrics from .metrics import (compute_calibration_error, compute_mae, compute_metrics, - compute_mse, compute_r2_score, update_metrics_history) + compute_mse, compute_r2_score, MetricsCalculator) # Set up logging logging.basicConfig(level=logging.INFO) @@ -473,7 +464,6 @@ class DeepONetTrainer: ) # Compute fraction in confidence interval for each trajectory - from .metrics import MetricsCalculator calculator = MetricsCalculator() diff --git a/MindEnergy/application/deeponet-grid/train.py b/MindEnergy/application/deeponet-grid/train.py index d67a81139..26c247cca 100644 --- a/MindEnergy/application/deeponet-grid/train.py +++ b/MindEnergy/application/deeponet-grid/train.py @@ -159,7 +159,7 @@ def train(): default=None, help="Resume training from checkpoint") parser.add_argument( - "--test_only", + "--eval", action="store_true", help="Only run evaluation on test set") @@ -214,6 +214,21 @@ def train(): logger.info(f"Resuming from checkpoint: {args.resume}") trainer.load_model(args.resume) + if args.eval: + # Only run evaluation + logger.info("Running evaluation only...") + metrics = trainer.evaluate(datasets['test']) + + # Save evaluation results + import json + results_path = os.path.join( + config['output']['save_dir'], 'test_results.json') + with open(results_path, 'w') as f: + json.dump(metrics, f, indent=2) + logger.info(f"Test results saved to: {results_path}") + + return + # print config before training logger.info(f"Training config: {config}") -- Gitee From 6eced130994eb45bdfe4c419bdd9ecb02f8b4c96 Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Tue, 22 Jul 2025 14:24:55 +0800 Subject: [PATCH 12/25] Fix data expand. --- .../application/deeponet-grid/src/data.py | 38 ++++++++----------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/MindEnergy/application/deeponet-grid/src/data.py b/MindEnergy/application/deeponet-grid/src/data.py index fd7dde347..662e46eeb 100644 --- a/MindEnergy/application/deeponet-grid/src/data.py +++ b/MindEnergy/application/deeponet-grid/src/data.py @@ -330,30 +330,22 @@ def prepare_deeponet_data(u, y, s, time_points=None): # to (n_samples * n_points, n_sensors) and (n_samples * n_points, 1) # repeat u_batch to match each time point - # (n_samples * n_points, n_sensors) - u_expanded = np.repeat(u, n_points, axis=0) - # create query points for each time point of each sample - y_expanded = np.tile(y.reshape(-1, 1), (n_samples, 1) - ) # (n_samples * n_points, 1) - s_expanded = np.tile(s.reshape(-1, 1), (n_samples, 1) - ) # (n_samples * n_points, 1) - - # # Expand data for DeepONet training - # u_expanded = [] - # y_expanded = [] - # s_expanded = [] - - # for i in range(n_samples): - # for j in range(n_points): - # u_expanded.append(u[i]) # Same input function for all time points - # y_expanded.append([y[i][j]]) # Single time point - 确保是2D [value] - # s_expanded.append([s[i, j]]) # Target value at this time point - 确保是2D - # [value] - - # u_expanded = np.array(u_expanded) - # y_expanded = np.array(y_expanded) - # s_expanded = np.array(s_expanded) + # Expand data for DeepONet training + u_expanded = [] + y_expanded = [] + s_expanded = [] + + # np.tile + reshape is not applicable for large data + for i in range(n_samples): + for j in range(n_points): + u_expanded.append(u[i]) # Same input function for all time points + y_expanded.append([y[i][j]]) # Single time point + s_expanded.append([s[i, j]]) # Target value at this time point + + u_expanded = np.array(u_expanded) + y_expanded = np.array(y_expanded) + s_expanded = np.array(s_expanded) metadata = { "n_samples": n_samples, -- Gitee From ac6cebc02d73400e638133c40b4f64dba4bc916b Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Tue, 22 Jul 2025 14:29:25 +0800 Subject: [PATCH 13/25] Fix typo --- .../application/deeponet-grid/src/trainer.py | 493 +++++++++++++++++- 1 file changed, 487 insertions(+), 6 deletions(-) diff --git a/MindEnergy/application/deeponet-grid/src/trainer.py b/MindEnergy/application/deeponet-grid/src/trainer.py index 3d99a596b..61d59ed6e 100644 --- a/MindEnergy/application/deeponet-grid/src/trainer.py +++ b/MindEnergy/application/deeponet-grid/src/trainer.py @@ -35,10 +35,10 @@ logger = logging.getLogger(__name__) class CustomCallback(Callback): """Custom callback for training monitoring""" - def __init__(self, print_every: int = 10, save_every: int = 100): + def __init__(self, log_interval: int = 10, eval_interval: int = 100): super(CustomCallback, self).__init__() - self.print_every = print_every - self.save_every = save_every + self.log_interval = log_interval + self.eval_interval = eval_interval self.step = 0 self.losses = [] @@ -53,7 +53,7 @@ class CustomCallback(Callback): self.losses.append(float(loss)) - if self.step % self.print_every == 0: + if self.step % self.log_interval == 0: logger.info(f"Step {self.step}, Loss: {float(loss):.6f}") @@ -242,7 +242,7 @@ class DeepONetTrainer: import time epochs = self.training_config["epochs"] - print_every = self.training_config.get("print_every", 10) + log_interval = self.training_config.get("log_interval", 10) eval_every = self.training_config.get("eval_every", 100) # 100 steps verbose = self.training_config.get("verbose", True) @@ -290,7 +290,488 @@ class DeepONetTrainer: batch_count += 1 global_step += 1 # show loss - if global_step % print_every == 0: + if global_step % log_interval == 0: + # Negative log likelihood loss can be negative, so we print + # its negation for clarity. + msg = ( + f"Epoch {epoch+1}, Step {global_step}, Batch {batch_count}, " + f"Loss: {float(loss):.6f}, " + f"Step time: {step_time:.3f}s") + logging.info(msg) + # evaluate + if val_dataset is not None and global_step % eval_every == 0: + val_loss = self.validate(val_dataset) + logger_hist["val loss"].append((global_step, val_loss)) + # Negative log likelihood loss can be negative + msg = f"[Eval] Epoch {epoch+1}, Step {global_step}, Val-Loss: {float(val_loss):.6f} " + logging.info(msg) + # save best ckpt + if val_loss < best["prob loss"]: + best["prob loss"] = val_loss + self.save_model("best_model.ckpt") + + # record current learning rate + current_lr = self.optimizer.learning_rate + logger.info(f"Current learning rate: {current_lr:.2e}") + # Compute average epoch loss + try: + avg_epoch_loss = epoch_loss / batch_count + except ZeroDivisionError as e: + logger.error( + f"error: {e}, batch size larger than number of training examples") + continue + logger_hist["prob loss"].append(avg_epoch_loss) + # show after each epoch + if verbose: + # Negative log likelihood loss can be negative + logger.info(f"Epoch {epoch+1}/{epochs}:") + logger.info(f" Train-Loss: {float(avg_epoch_loss):.6f} ") + logger.info(f' Best-Loss: {float(best["prob loss"]):.6f} ') + return logger_hist + + def save_model(self, filename: str): + """Save model checkpoint + + Args: + filename: Name of the checkpoint file + """ + save_path = os.path.join(self.save_dir, filename) + ms.save_checkpoint(self.model, save_path) + logger.info(f"Model saved to: {save_path}") + + def load_model(self, filename: str): + """Load model checkpoint + + Args: + filename: Name of the checkpoint file + """ + load_path = os.path.join(self.save_dir, filename) + if os.path.exists(load_path): + ms.load_checkpoint(load_path, self.model) + logger.info(f"Model loaded from: {load_path}") + else: + logger.warning(f"Checkpoint not found: {load_path}") + + def predict(self, u: ms.Tensor, + y: ms.Tensor) -> Tuple[ms.Tensor, ms.Tensor]: + """Make predictions + + Args: + u: Input function values + y: Evaluation points + + Returns: + Tuple of (mean_pred, log_std_pred) + """ + self.model.set_train(False) + mean_pred, log_std_pred = self.model(u, y) + self.model.set_train(True) + return mean_pred, log_std_pred + + def evaluate(self, test_dataset: GeneratorDataset) -> Dict[str, float]: + """Evaluate model on test dataset + + Args: + test_dataset: Test dataset + + Returns: + Dictionary of evaluation metrics + """ + self.model.set_train(False) + + # Collect predictions and targets + all_predictions = [] + all_targets = [] + all_means = [] + all_stds = [] + + for u, y, target in test_dataset: + # Forward pass + mean_pred, log_std_pred = self.model(u, y) + std_pred = ops.Exp()(log_std_pred) + + # Store results + all_predictions.append(mean_pred) + all_targets.append(target) + all_means.append(mean_pred) + all_stds.append(std_pred) + + # Concatenate all batches + if all_predictions: + predictions = ops.Concat(axis=0)(all_predictions) + targets = ops.Concat(axis=0)(all_targets) + means = ops.Concat(axis=0)(all_means) + stds = ops.Concat(axis=0)(all_stds) + else: + return {} + + # Compute basic metrics + metrics = {} + metrics["mse"] = compute_mse(targets, predictions) + metrics["mae"] = compute_mae(targets, predictions) + metrics["r2"] = compute_r2_score(targets, predictions) + + # Compute relative errors + from .metrics import MetricsCalculator + + calculator = MetricsCalculator() + metrics["l1_relative_error"] = calculator.l1_relative_error( + targets, predictions + ) + metrics["l2_relative_error"] = calculator.l2_relative_error( + targets, predictions + ) + + # Compute uncertainty quantification metrics + metrics["calibration_error"] = compute_calibration_error( + targets, means, stds) + metrics["fraction_in_CI"] = calculator.fraction_in_CI( + targets, means, stds, xi=2.0 + ) + + # Compute trajectory errors if data is structured as trajectories + try: + l1_traj, l2_traj = calculator.trajectory_rel_error( + targets, predictions) + metrics["trajectory_l1_error"] = l1_traj + metrics["trajectory_l2_error"] = l2_traj + except BaseException: + pass + + return metrics + + def compute_trajectory_metrics( + self, + s_test: List[ms.Tensor], + mean_predictions: List[ms.Tensor], + std_predictions: List[ms.Tensor], + verbose: bool = False, + ) -> Dict[str, Any]: + """Compute metrics for trajectory predictions + + Args: + s_test: List of true solutions + mean_predictions: List of predicted means + std_predictions: List of predicted standard deviations + verbose: Whether to print results + + Returns: + Dictionary of trajectory metrics + """ + # Compute L1 and L2 relative errors + metrics_state = compute_metrics( + s_test, mean_predictions, ["l1", "l2"], verbose=verbose + ) + + # Compute fraction in confidence interval for each trajectory + + calculator = MetricsCalculator() + + ci_fractions = [] + for i in range(len(s_test)): + ci_frac = calculator.fraction_in_CI( + s_test[i], mean_predictions[i], std_predictions[i] + ) + ci_fractions.append(ci_frac) + + avg_ci_fraction = np.mean(ci_fractions) + + return { + "l1_metrics": metrics_state[0], # [max, min, mean] + "l2_metrics": metrics_state[1], # [max, min, mean] + "avg_ci_fraction": avg_ci_fraction, + "ci_fractions": ci_fractions, + } + + +def create_trainer( + model: nn.Cell, config: Dict[str, Any], save_dir: str = "outputs" +) -> DeepONetTrainer: + """Create trainer instance""" + return DeepONetTrainer(model, config, save_dir) + +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + + +# Import metrics + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class CustomCallback(Callback): + """Custom callback for training monitoring""" + + def __init__(self, log_interval: int = 10, eval_interval: int = 100): + super(CustomCallback, self).__init__() + self.log_interval = log_interval + self.eval_interval = eval_interval + self.step = 0 + self.losses = [] + + def step_end(self, run_context): + """Called at the end of each step""" + self.step += 1 + cb_params = run_context.original_args() + loss = cb_params.net_outputs + + if isinstance(loss, (list, tuple)): + loss = loss[0] + + self.losses.append(float(loss)) + + if self.step % self.log_interval == 0: + logger.info(f"Step {self.step}, Loss: {float(loss):.6f}") + + +class ProbabilisticLoss(nn.Cell): + """Negative log likelihood loss for probabilistic DeepONet + Equivalent to torch.distributions.Normal(mean_pred, torch.exp(log_std_pred)).log_prob(target).mean() + """ + + def __init__(self): + super(ProbabilisticLoss, self).__init__() + self.log = ops.Log() + self.exp = ops.Exp() + self.square = ops.Square() + self.reduce_mean = ops.ReduceMean() + self.log_2pi = np.log(2 * np.pi) + + def construct(self, mean_pred, log_std_pred, target): + """Compute negative log likelihood loss using normal distribution + + Args: + mean_pred: Predicted mean + log_std_pred: Predicted log standard deviation + target: Target values + + Returns: + Negative mean log probability + """ + # Convert log_std to std + std_pred = self.exp(log_std_pred) + + # Compute log probability of normal distribution + # log_prob = -0.5 * ((x - mu) / sigma)^2 - log(sigma) - 0.5 * log(2*pi) + log_prob = ( + -0.5 * self.square((target - mean_pred) / std_pred) + - log_std_pred + - 0.5 * self.log_2pi + ) + + # Return negative mean log probability (equivalent to + # -dist.log_prob(target).mean()) + return -self.reduce_mean(log_prob) + + +class MSELoss(nn.Cell): + """Mean squared error loss""" + + def __init__(self): + super(MSELoss, self).__init__() + self.mse = nn.MSELoss() + + def construct(self, mean_pred, log_std_pred, target): + """Compute MSE loss (ignores uncertainty predictions) + + Args: + mean_pred: Predicted mean + log_std_pred: Predicted log standard deviation (ignored) + target: Target values + + Returns: + MSE loss value + """ + return self.mse(mean_pred, target) + + +class DeepONetTrainer: + """Trainer class for DeepONet with uncertainty quantification""" + + def __init__( + self, model: nn.Cell, config: Dict[str, Any], save_dir: str = "outputs" + ): + + self.model = model + self.config = config + self.save_dir = save_dir + self.training_config = config["training"] + + # Initialize optimizer + optimizer_type = self.training_config.get("optimizer", "adam") + if self.training_config.get("scheduler", None) == "cosine": + min_lr = float(self.training_config.get("min_lr", 1e-7)) + max_lr = float(self.training_config.get("max_lr", 1e-3)) + total_step = int(self.training_config.get("total_step", 10000)) + step_per_epoch = int( + self.training_config.get("step_per_epoch", 1000)) + decay_epoch = int(self.training_config.get("decay_epoch", 1000)) + learning_rate = nn.cosine_decay_lr( + min_lr, max_lr, total_step, step_per_epoch, decay_epoch + ) # Ensure it's a float + else: + learning_rate = float( + self.training_config["learning_rate"] + ) # Ensure it's a float + weight_decay = float(self.training_config.get("weight_decay", 0.0)) + + if optimizer_type.lower() == "adam": + self.optimizer = nn.Adam( + self.model.trainable_params(), + learning_rate=learning_rate, + weight_decay=weight_decay, + ) + else: + raise ValueError(f"Unsupported optimizer: {optimizer_type}") + + # Initialize loss function + loss_type = self.training_config.get("loss_type", "nll") + + if loss_type.lower() == "nll": + self.loss_fn = ProbabilisticLoss() + elif loss_type.lower() == "mse": + self.loss_fn = MSELoss() + else: + raise ValueError(f"Unsupported loss type: {loss_type}") + + # Initialize metrics + self.metrics = { + "train_loss": [], + "val_loss": [], + "best_loss": float("inf"), + "patience_counter": 0, + } + + def train_step( + self, u: ms.Tensor, y: ms.Tensor, target: ms.Tensor + ) -> Tuple[ms.Tensor, ms.Tensor, ms.Tensor]: + """Single training step + + Args: + u: Input function values + y: Evaluation points + target: Target values + + Returns: + Tuple of (loss, mean_pred, log_std_pred) + """ + + def forward_fn(): + mean_pred, log_std_pred = self.model(u, y) + loss = self.loss_fn(mean_pred, log_std_pred, target) + return loss, mean_pred, log_std_pred + + grad_fn = ops.value_and_grad( + forward_fn, None, self.optimizer.parameters, has_aux=True + ) + + (loss, mean_pred, log_std_pred), grads = grad_fn() + self.optimizer(grads) + + return loss, mean_pred, log_std_pred + + def validate(self, val_dataset: GeneratorDataset) -> float: + """Validate model on validation dataset + + Args: + val_dataset: Validation dataset + + Returns: + Average validation loss + """ + self.model.set_train(False) + total_loss = 0.0 + num_batches = 0 + + for u, y, target in val_dataset: + mean_pred, log_std_pred = self.model(u, y) + loss = self.loss_fn(mean_pred, log_std_pred, target) + total_loss += float(loss) + num_batches += 1 + + self.model.set_train(True) + return total_loss / num_batches if num_batches > 0 else float("inf") + + def train( + self, + train_dataset: GeneratorDataset, + val_dataset: Optional[GeneratorDataset] = None, + ) -> Dict[str, List[float]]: + """Train the model + + Args: + train_dataset: Training dataset + val_dataset: Validation dataset (optional) + + Returns: + Training history + """ + import time + + epochs = self.training_config["epochs"] + log_interval = self.training_config.get("log_interval", 10) + eval_every = self.training_config.get("eval_every", 100) # 100 steps + verbose = self.training_config.get("verbose", True) + + # Log model parameter count + total_params = sum(p.size for p in self.model.get_parameters()) + trainable_params = sum(p.size for p in self.model.trainable_params()) + logging.info(f"Total model parameters: {total_params}") + logging.info(f"Trainable parameters: {trainable_params}") + # Log training data size + train_data_size = ( + train_dataset.get_dataset_size() * train_dataset.get_batch_size() + ) + logging.info( + f"Training data size (number of samples): {train_data_size}") + logging.info(f"Training batch size : {train_dataset.get_batch_size()}") + + # Print all parameter names, shapes, and sizes for debugging + logger.info("Model parameter details:") + for p in self.model.get_parameters(): + logger.info(f"{p.name}: shape={p.shape}, size={p.size}") + + if verbose: + logger.info( + f"\n***** Probabilistic Training for {epochs} epochs *****\n") + + # Initialize best values and logger + best = {} + best["prob loss"] = float("inf") + + logger_hist = {} + logger_hist["prob loss"] = [] + logger_hist["val loss"] = [] + + global_step = 0 + + for epoch in range(epochs): + self.model.set_train() + epoch_loss = 0 + batch_count = 0 + for u, y, target in train_dataset: + step_start_time = time.time() + loss, mean_pred, log_std_pred = self.train_step(u, y, target) + step_time = time.time() - step_start_time + epoch_loss += float(loss) + batch_count += 1 + global_step += 1 + # show loss + if global_step % log_interval == 0: # Negative log likelihood loss can be negative, so we print # its negation for clarity. msg = ( -- Gitee From 78df9884545ba4769c07630c4222159a04e75e61 Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Tue, 22 Jul 2025 14:39:00 +0800 Subject: [PATCH 14/25] Add log extraction. --- .../application/deeponet-grid/src/utils.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/MindEnergy/application/deeponet-grid/src/utils.py b/MindEnergy/application/deeponet-grid/src/utils.py index de329aab9..d9f0b821c 100644 --- a/MindEnergy/application/deeponet-grid/src/utils.py +++ b/MindEnergy/application/deeponet-grid/src/utils.py @@ -418,3 +418,33 @@ def load_trained_model(model, checkpoint_path: str): ms.load_checkpoint(checkpoint_path, model) return model + + +def extract_log(log_file: str, history_file: str): + print("DeepONet-Grid-UQ training log extraction") + print("=" * 40) + + if os.path.exists(log_file): + print(f"Found log file: {log_file}") + + # Parse and print basic statistics + losses, epochs, steps = parse_loss_log(log_file) + if losses: + print(f"Loss statistics:") + print(f" Total entries: {len(losses)}") + print(f" Min loss: {min(losses):.6f}") + print(f" Max loss: {max(losses):.6f}") + print(f" Mean loss: {np.mean(losses):.6f}") + print(f" Final loss: {losses[-1]:.6f}") + + # Plot loss curves + plot_loss_curves(log_file, save_path="loss_curves.png") + plot_loss_statistics(log_file, save_path="loss_statistics.png") + + # Check for training history + # history_file = "outputs/training_history.json" + if os.path.exists(history_file): + print(f"Found training history: {history_file}") + plot_training_history(history_file, save_path="training_history.png") + + print("Completed!") -- Gitee From d02ace9c30738ca7a1c474bbcf9d929f2ad79d81 Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Tue, 22 Jul 2025 15:10:17 +0800 Subject: [PATCH 15/25] Add training results. --- .../application/deeponet-grid/README.md | 19 +++++++++++++++++++ .../application/deeponet-grid/README_en.md | 18 ++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/MindEnergy/application/deeponet-grid/README.md b/MindEnergy/application/deeponet-grid/README.md index 32e4cde7f..71f073f42 100644 --- a/MindEnergy/application/deeponet-grid/README.md +++ b/MindEnergy/application/deeponet-grid/README.md @@ -76,8 +76,19 @@ python train.py \ python train.py --resume outputs/best_model.ckpt ``` + ### 4. 仅运行评估 +```bash +python train.py --eval --resume outputs/best_model.ckpt +``` + +### 5. 打印loss log曲线 + +执行`src/utils.py`中的`extract_log`函数,并将记录训练信息和评估信息的地址传入。 + +### 6. 仅运行推理 + ```bash # 指定数据推理 python inference.py --checkpoint outputs/best_model.ckpt \ @@ -192,3 +203,11 @@ def load_custom_data(data_path): # 自定义数据加载逻辑 return u, y, s ``` + + +## 训练结果 +使用10000条测试数据 (n_samples = 10000, n_sensors = 200, timesteps = 100) ,训练2500步后得到的训练结果(batch_size = 1024): + +| MSE | MAE | L1 | L2 | +| ----- | ------ | ------ | ------ | +| 0.033 | 0.1223 | 0.1581 | 0.2257 | \ No newline at end of file diff --git a/MindEnergy/application/deeponet-grid/README_en.md b/MindEnergy/application/deeponet-grid/README_en.md index 888ebee07..338f7611a 100644 --- a/MindEnergy/application/deeponet-grid/README_en.md +++ b/MindEnergy/application/deeponet-grid/README_en.md @@ -76,6 +76,16 @@ python train.py --resume outputs/best_model.ckpt ### 4. Run Evaluation Only +```bash +python train.py --eval --resume outputs/best_model.ckpt +``` + +### 5. Show loss log curve + +Call the function `extract_log` in `src/utils.py` with input parameters (log file path and evaluation json file path.) + +### 6. Run Inference Only + ```bash # Single data point inference python inference.py --checkpoint outputs/best_model.ckpt \ @@ -190,3 +200,11 @@ def load_custom_data(data_path): # Custom data loading logic return u, y, s ``` + + +## Training results +Used 10000 data samples (n_samples = 10000, n_sensors = 200, timesteps = 100) ,trained after 2500 steps with batch_size 1024: + +| MSE | MAE | L1 | L2 | +| ----- | ------ | ------ | ------ | +| 0.033 | 0.1223 | 0.1581 | 0.2257 | \ No newline at end of file -- Gitee From ce588edfadd0c01895193ec242d04d599566c6ef Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Tue, 22 Jul 2025 15:45:07 +0800 Subject: [PATCH 16/25] Add data parallel. --- MindEnergy/application/deeponet-grid/README.md | 10 +++++++++- MindEnergy/application/deeponet-grid/README_en.md | 7 +++++++ MindEnergy/application/deeponet-grid/src/data.py | 12 +++++++++++- MindEnergy/application/deeponet-grid/src/trainer.py | 4 ++++ MindEnergy/application/deeponet-grid/train.py | 11 ++++++++--- 5 files changed, 39 insertions(+), 5 deletions(-) diff --git a/MindEnergy/application/deeponet-grid/README.md b/MindEnergy/application/deeponet-grid/README.md index 71f073f42..681ab1b11 100644 --- a/MindEnergy/application/deeponet-grid/README.md +++ b/MindEnergy/application/deeponet-grid/README.md @@ -55,7 +55,13 @@ deeponet-grid/ ### 1. 使用真实数据训练 ```bash -python train.py --data_path /path/to/your/data.npz --epochs 500 +python train.py +``` + +多卡: + +```bash +mpirun -n 8 python train.py ``` ### 2. 自定义配置 @@ -83,6 +89,8 @@ python train.py --resume outputs/best_model.ckpt python train.py --eval --resume outputs/best_model.ckpt ``` + + ### 5. 打印loss log曲线 执行`src/utils.py`中的`extract_log`函数,并将记录训练信息和评估信息的地址传入。 diff --git a/MindEnergy/application/deeponet-grid/README_en.md b/MindEnergy/application/deeponet-grid/README_en.md index 338f7611a..bb31a5804 100644 --- a/MindEnergy/application/deeponet-grid/README_en.md +++ b/MindEnergy/application/deeponet-grid/README_en.md @@ -57,6 +57,13 @@ Edit the `configs/config.yaml` file to configure model parameters: python train.py --data_path /path/to/your/data.npz --epochs 500 ``` +multiple cards: + +```bash +mpirun -n 8 python train.py +``` + + ### 2. Custom Configuration ```bash diff --git a/MindEnergy/application/deeponet-grid/src/data.py b/MindEnergy/application/deeponet-grid/src/data.py index 662e46eeb..41a81145e 100644 --- a/MindEnergy/application/deeponet-grid/src/data.py +++ b/MindEnergy/application/deeponet-grid/src/data.py @@ -20,6 +20,7 @@ import mindspore.numpy as mnp import numpy as np import pandas as pd from mindspore import context +from mindspore.communication import get_rank, get_group_size from mindspore.dataset import GeneratorDataset from mindspore.ops import operations as ops from scipy import stats @@ -277,14 +278,23 @@ def create_datasets( data_gen = DataGenerator(u, y, s) # Create dataset with three columns: u, y, s + # ====== Data Parallel: add num_shards and shard_id for train set ====== if split_name == "train": + try: + rank_id = get_rank() + rank_size = get_group_size() + except Exception: + rank_id = 0 + rank_size = 1 dataset = GeneratorDataset( - source=data_gen, column_names=["u", "y", "s"], shuffle=True + source=data_gen, column_names=["u", "y", "s"], shuffle=True, + num_shards=rank_size, shard_id=rank_id ).batch(batch_size) else: dataset = GeneratorDataset( source=data_gen, column_names=["u", "y", "s"], shuffle=False ).batch(batch_size) + # ====== End Data Parallel ====== # set batch size datasets[split_name] = dataset diff --git a/MindEnergy/application/deeponet-grid/src/trainer.py b/MindEnergy/application/deeponet-grid/src/trainer.py index 61d59ed6e..1a2df3dd3 100644 --- a/MindEnergy/application/deeponet-grid/src/trainer.py +++ b/MindEnergy/application/deeponet-grid/src/trainer.py @@ -638,6 +638,9 @@ class DeepONetTrainer: else: raise ValueError(f"Unsupported optimizer: {optimizer_type}") + self.grad_reducer = nn.DistributedGradReducer( + self.optimizer.parameters) + # Initialize loss function loss_type = self.training_config.get("loss_type", "nll") @@ -680,6 +683,7 @@ class DeepONetTrainer: ) (loss, mean_pred, log_std_pred), grads = grad_fn() + grads = self.grad_reducer(grads) self.optimizer(grads) return loss, mean_pred, log_std_pred diff --git a/MindEnergy/application/deeponet-grid/train.py b/MindEnergy/application/deeponet-grid/train.py index 26c247cca..ae4971ab1 100644 --- a/MindEnergy/application/deeponet-grid/train.py +++ b/MindEnergy/application/deeponet-grid/train.py @@ -25,14 +25,13 @@ from pathlib import Path import mindspore as ms import yaml from mindspore import context +from mindspore.communication import init from src.data import load_and_preprocess_real_data from src.model import Prob_DeepONet from src.trainer import create_trainer from src.utils import load_config -# Add src to path -sys.path.append(os.path.join(os.path.dirname(__file__), "src")) # Set up logging logging.basicConfig( @@ -45,7 +44,13 @@ logging.basicConfig( ) logger = logging.getLogger(__name__) -context.set_context(mode=context.PYNATIVE_MODE, device_target="CPU") +# ====== Data Parallel Init (for distributed training) ====== +ms.set_context(mode=ms.GRAPH_MODE, device_target="Ascend") +ms.set_auto_parallel_context( + parallel_mode=ms.ParallelMode.DATA_PARALLEL, gradients_mean=True) +init() +ms.set_seed(1) +# ====== End Data Parallel Init ====== logger.info(f"Mode set to: {context.get_context('mode')}") logger.info(f"Device set to: {context.get_context('device_target')}") -- Gitee From 0b853b040ce7f137e2fbead02dd7ca3d9ba89d9a Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Tue, 22 Jul 2025 15:52:41 +0800 Subject: [PATCH 17/25] Fix typo --- .../application/deeponet-grid/src/trainer.py | 481 ------------------ 1 file changed, 481 deletions(-) diff --git a/MindEnergy/application/deeponet-grid/src/trainer.py b/MindEnergy/application/deeponet-grid/src/trainer.py index 1a2df3dd3..64a035a7c 100644 --- a/MindEnergy/application/deeponet-grid/src/trainer.py +++ b/MindEnergy/application/deeponet-grid/src/trainer.py @@ -118,487 +118,6 @@ class MSELoss(nn.Cell): return self.mse(mean_pred, target) -class DeepONetTrainer: - """Trainer class for DeepONet with uncertainty quantification""" - - def __init__( - self, model: nn.Cell, config: Dict[str, Any], save_dir: str = "outputs" - ): - - self.model = model - self.config = config - self.save_dir = save_dir - self.training_config = config["training"] - - # Initialize optimizer - optimizer_type = self.training_config.get("optimizer", "adam") - if self.training_config.get("scheduler", None) == "cosine": - min_lr = float(self.training_config.get("min_lr", 1e-7)) - max_lr = float(self.training_config.get("max_lr", 1e-3)) - total_step = int(self.training_config.get("total_step", 10000)) - step_per_epoch = int( - self.training_config.get("step_per_epoch", 1000)) - decay_epoch = int(self.training_config.get("decay_epoch", 1000)) - learning_rate = nn.cosine_decay_lr( - min_lr, max_lr, total_step, step_per_epoch, decay_epoch - ) # Ensure it's a float - else: - learning_rate = float( - self.training_config["learning_rate"] - ) # Ensure it's a float - weight_decay = float(self.training_config.get("weight_decay", 0.0)) - - if optimizer_type.lower() == "adam": - self.optimizer = nn.Adam( - self.model.trainable_params(), - learning_rate=learning_rate, - weight_decay=weight_decay, - ) - else: - raise ValueError(f"Unsupported optimizer: {optimizer_type}") - - # Initialize loss function - loss_type = self.training_config.get("loss_type", "nll") - - if loss_type.lower() == "nll": - self.loss_fn = ProbabilisticLoss() - elif loss_type.lower() == "mse": - self.loss_fn = MSELoss() - else: - raise ValueError(f"Unsupported loss type: {loss_type}") - - # Initialize metrics - self.metrics = { - "train_loss": [], - "val_loss": [], - "best_loss": float("inf"), - "patience_counter": 0, - } - - def train_step( - self, u: ms.Tensor, y: ms.Tensor, target: ms.Tensor - ) -> Tuple[ms.Tensor, ms.Tensor, ms.Tensor]: - """Single training step - - Args: - u: Input function values - y: Evaluation points - target: Target values - - Returns: - Tuple of (loss, mean_pred, log_std_pred) - """ - - def forward_fn(): - mean_pred, log_std_pred = self.model(u, y) - loss = self.loss_fn(mean_pred, log_std_pred, target) - return loss, mean_pred, log_std_pred - - grad_fn = ops.value_and_grad( - forward_fn, None, self.optimizer.parameters, has_aux=True - ) - - (loss, mean_pred, log_std_pred), grads = grad_fn() - self.optimizer(grads) - - return loss, mean_pred, log_std_pred - - def validate(self, val_dataset: GeneratorDataset) -> float: - """Validate model on validation dataset - - Args: - val_dataset: Validation dataset - - Returns: - Average validation loss - """ - self.model.set_train(False) - total_loss = 0.0 - num_batches = 0 - - for u, y, target in val_dataset: - mean_pred, log_std_pred = self.model(u, y) - loss = self.loss_fn(mean_pred, log_std_pred, target) - total_loss += float(loss) - num_batches += 1 - - self.model.set_train(True) - return total_loss / num_batches if num_batches > 0 else float("inf") - - def train( - self, - train_dataset: GeneratorDataset, - val_dataset: Optional[GeneratorDataset] = None, - ) -> Dict[str, List[float]]: - """Train the model - - Args: - train_dataset: Training dataset - val_dataset: Validation dataset (optional) - - Returns: - Training history - """ - import time - - epochs = self.training_config["epochs"] - log_interval = self.training_config.get("log_interval", 10) - eval_every = self.training_config.get("eval_every", 100) # 100 steps - verbose = self.training_config.get("verbose", True) - - # Log model parameter count - total_params = sum(p.size for p in self.model.get_parameters()) - trainable_params = sum(p.size for p in self.model.trainable_params()) - logging.info(f"Total model parameters: {total_params}") - logging.info(f"Trainable parameters: {trainable_params}") - # Log training data size - train_data_size = ( - train_dataset.get_dataset_size() * train_dataset.get_batch_size() - ) - logging.info( - f"Training data size (number of samples): {train_data_size}") - logging.info(f"Training batch size : {train_dataset.get_batch_size()}") - - # Print all parameter names, shapes, and sizes for debugging - logger.info("Model parameter details:") - for p in self.model.get_parameters(): - logger.info(f"{p.name}: shape={p.shape}, size={p.size}") - - if verbose: - logger.info( - f"\n***** Probabilistic Training for {epochs} epochs *****\n") - - # Initialize best values and logger - best = {} - best["prob loss"] = float("inf") - - logger_hist = {} - logger_hist["prob loss"] = [] - logger_hist["val loss"] = [] - - global_step = 0 - - for epoch in range(epochs): - self.model.set_train() - epoch_loss = 0 - batch_count = 0 - for u, y, target in train_dataset: - step_start_time = time.time() - loss, mean_pred, log_std_pred = self.train_step(u, y, target) - step_time = time.time() - step_start_time - epoch_loss += float(loss) - batch_count += 1 - global_step += 1 - # show loss - if global_step % log_interval == 0: - # Negative log likelihood loss can be negative, so we print - # its negation for clarity. - msg = ( - f"Epoch {epoch+1}, Step {global_step}, Batch {batch_count}, " - f"Loss: {float(loss):.6f}, " - f"Step time: {step_time:.3f}s") - logging.info(msg) - # evaluate - if val_dataset is not None and global_step % eval_every == 0: - val_loss = self.validate(val_dataset) - logger_hist["val loss"].append((global_step, val_loss)) - # Negative log likelihood loss can be negative - msg = f"[Eval] Epoch {epoch+1}, Step {global_step}, Val-Loss: {float(val_loss):.6f} " - logging.info(msg) - # save best ckpt - if val_loss < best["prob loss"]: - best["prob loss"] = val_loss - self.save_model("best_model.ckpt") - - # record current learning rate - current_lr = self.optimizer.learning_rate - logger.info(f"Current learning rate: {current_lr:.2e}") - # Compute average epoch loss - try: - avg_epoch_loss = epoch_loss / batch_count - except ZeroDivisionError as e: - logger.error( - f"error: {e}, batch size larger than number of training examples") - continue - logger_hist["prob loss"].append(avg_epoch_loss) - # show after each epoch - if verbose: - # Negative log likelihood loss can be negative - logger.info(f"Epoch {epoch+1}/{epochs}:") - logger.info(f" Train-Loss: {float(avg_epoch_loss):.6f} ") - logger.info(f' Best-Loss: {float(best["prob loss"]):.6f} ') - return logger_hist - - def save_model(self, filename: str): - """Save model checkpoint - - Args: - filename: Name of the checkpoint file - """ - save_path = os.path.join(self.save_dir, filename) - ms.save_checkpoint(self.model, save_path) - logger.info(f"Model saved to: {save_path}") - - def load_model(self, filename: str): - """Load model checkpoint - - Args: - filename: Name of the checkpoint file - """ - load_path = os.path.join(self.save_dir, filename) - if os.path.exists(load_path): - ms.load_checkpoint(load_path, self.model) - logger.info(f"Model loaded from: {load_path}") - else: - logger.warning(f"Checkpoint not found: {load_path}") - - def predict(self, u: ms.Tensor, - y: ms.Tensor) -> Tuple[ms.Tensor, ms.Tensor]: - """Make predictions - - Args: - u: Input function values - y: Evaluation points - - Returns: - Tuple of (mean_pred, log_std_pred) - """ - self.model.set_train(False) - mean_pred, log_std_pred = self.model(u, y) - self.model.set_train(True) - return mean_pred, log_std_pred - - def evaluate(self, test_dataset: GeneratorDataset) -> Dict[str, float]: - """Evaluate model on test dataset - - Args: - test_dataset: Test dataset - - Returns: - Dictionary of evaluation metrics - """ - self.model.set_train(False) - - # Collect predictions and targets - all_predictions = [] - all_targets = [] - all_means = [] - all_stds = [] - - for u, y, target in test_dataset: - # Forward pass - mean_pred, log_std_pred = self.model(u, y) - std_pred = ops.Exp()(log_std_pred) - - # Store results - all_predictions.append(mean_pred) - all_targets.append(target) - all_means.append(mean_pred) - all_stds.append(std_pred) - - # Concatenate all batches - if all_predictions: - predictions = ops.Concat(axis=0)(all_predictions) - targets = ops.Concat(axis=0)(all_targets) - means = ops.Concat(axis=0)(all_means) - stds = ops.Concat(axis=0)(all_stds) - else: - return {} - - # Compute basic metrics - metrics = {} - metrics["mse"] = compute_mse(targets, predictions) - metrics["mae"] = compute_mae(targets, predictions) - metrics["r2"] = compute_r2_score(targets, predictions) - - # Compute relative errors - from .metrics import MetricsCalculator - - calculator = MetricsCalculator() - metrics["l1_relative_error"] = calculator.l1_relative_error( - targets, predictions - ) - metrics["l2_relative_error"] = calculator.l2_relative_error( - targets, predictions - ) - - # Compute uncertainty quantification metrics - metrics["calibration_error"] = compute_calibration_error( - targets, means, stds) - metrics["fraction_in_CI"] = calculator.fraction_in_CI( - targets, means, stds, xi=2.0 - ) - - # Compute trajectory errors if data is structured as trajectories - try: - l1_traj, l2_traj = calculator.trajectory_rel_error( - targets, predictions) - metrics["trajectory_l1_error"] = l1_traj - metrics["trajectory_l2_error"] = l2_traj - except BaseException: - pass - - return metrics - - def compute_trajectory_metrics( - self, - s_test: List[ms.Tensor], - mean_predictions: List[ms.Tensor], - std_predictions: List[ms.Tensor], - verbose: bool = False, - ) -> Dict[str, Any]: - """Compute metrics for trajectory predictions - - Args: - s_test: List of true solutions - mean_predictions: List of predicted means - std_predictions: List of predicted standard deviations - verbose: Whether to print results - - Returns: - Dictionary of trajectory metrics - """ - # Compute L1 and L2 relative errors - metrics_state = compute_metrics( - s_test, mean_predictions, ["l1", "l2"], verbose=verbose - ) - - # Compute fraction in confidence interval for each trajectory - - calculator = MetricsCalculator() - - ci_fractions = [] - for i in range(len(s_test)): - ci_frac = calculator.fraction_in_CI( - s_test[i], mean_predictions[i], std_predictions[i] - ) - ci_fractions.append(ci_frac) - - avg_ci_fraction = np.mean(ci_fractions) - - return { - "l1_metrics": metrics_state[0], # [max, min, mean] - "l2_metrics": metrics_state[1], # [max, min, mean] - "avg_ci_fraction": avg_ci_fraction, - "ci_fractions": ci_fractions, - } - - -def create_trainer( - model: nn.Cell, config: Dict[str, Any], save_dir: str = "outputs" -) -> DeepONetTrainer: - """Create trainer instance""" - return DeepONetTrainer(model, config, save_dir) - -# Copyright 2025 Huawei Technologies Co., Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ============================================================================== - - -# Import metrics - -# Set up logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - - -class CustomCallback(Callback): - """Custom callback for training monitoring""" - - def __init__(self, log_interval: int = 10, eval_interval: int = 100): - super(CustomCallback, self).__init__() - self.log_interval = log_interval - self.eval_interval = eval_interval - self.step = 0 - self.losses = [] - - def step_end(self, run_context): - """Called at the end of each step""" - self.step += 1 - cb_params = run_context.original_args() - loss = cb_params.net_outputs - - if isinstance(loss, (list, tuple)): - loss = loss[0] - - self.losses.append(float(loss)) - - if self.step % self.log_interval == 0: - logger.info(f"Step {self.step}, Loss: {float(loss):.6f}") - - -class ProbabilisticLoss(nn.Cell): - """Negative log likelihood loss for probabilistic DeepONet - Equivalent to torch.distributions.Normal(mean_pred, torch.exp(log_std_pred)).log_prob(target).mean() - """ - - def __init__(self): - super(ProbabilisticLoss, self).__init__() - self.log = ops.Log() - self.exp = ops.Exp() - self.square = ops.Square() - self.reduce_mean = ops.ReduceMean() - self.log_2pi = np.log(2 * np.pi) - - def construct(self, mean_pred, log_std_pred, target): - """Compute negative log likelihood loss using normal distribution - - Args: - mean_pred: Predicted mean - log_std_pred: Predicted log standard deviation - target: Target values - - Returns: - Negative mean log probability - """ - # Convert log_std to std - std_pred = self.exp(log_std_pred) - - # Compute log probability of normal distribution - # log_prob = -0.5 * ((x - mu) / sigma)^2 - log(sigma) - 0.5 * log(2*pi) - log_prob = ( - -0.5 * self.square((target - mean_pred) / std_pred) - - log_std_pred - - 0.5 * self.log_2pi - ) - - # Return negative mean log probability (equivalent to - # -dist.log_prob(target).mean()) - return -self.reduce_mean(log_prob) - - -class MSELoss(nn.Cell): - """Mean squared error loss""" - - def __init__(self): - super(MSELoss, self).__init__() - self.mse = nn.MSELoss() - - def construct(self, mean_pred, log_std_pred, target): - """Compute MSE loss (ignores uncertainty predictions) - - Args: - mean_pred: Predicted mean - log_std_pred: Predicted log standard deviation (ignored) - target: Target values - - Returns: - MSE loss value - """ - return self.mse(mean_pred, target) - - class DeepONetTrainer: """Trainer class for DeepONet with uncertainty quantification""" -- Gitee From b5c04944b0f7016341a9fc4178b69a0ea658406b Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Thu, 24 Jul 2025 15:37:05 +0800 Subject: [PATCH 18/25] Fix typo. --- .../deeponet-grid/DeepONet_Grid_UQ_Probabilistic.ipynb | 2 +- MindEnergy/application/deeponet-grid/configs/config.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/MindEnergy/application/deeponet-grid/DeepONet_Grid_UQ_Probabilistic.ipynb b/MindEnergy/application/deeponet-grid/DeepONet_Grid_UQ_Probabilistic.ipynb index 3f77320a5..c9643e0dd 100644 --- a/MindEnergy/application/deeponet-grid/DeepONet_Grid_UQ_Probabilistic.ipynb +++ b/MindEnergy/application/deeponet-grid/DeepONet_Grid_UQ_Probabilistic.ipynb @@ -253,7 +253,7 @@ "print(\"Splitting data...\")\n", "data_splits = split_data(\n", " u_train_expanded, y_train_expanded, s_train_expanded,\n", - " train_ratio=0.1, val_split=0.5, test_split=0.4\n", + " train_ratio=0.1, val_ratio=0.5, test_ratio=0.4\n", ")\n", "\n", "# Create MindSpore datasets\n", diff --git a/MindEnergy/application/deeponet-grid/configs/config.yaml b/MindEnergy/application/deeponet-grid/configs/config.yaml index 0009886ec..6354aad71 100644 --- a/MindEnergy/application/deeponet-grid/configs/config.yaml +++ b/MindEnergy/application/deeponet-grid/configs/config.yaml @@ -44,8 +44,8 @@ data: data_path: "data/train-data-voltage-m-33-Q-100-mix.npz" # Real data path (if null, use synthetic data) use_synthetic: true # Whether to use synthetic data train_ratio: 0.1 # Training set ratio - val_split: 0.5 # Validation set ratio - test_split: 0.4 # Test set ratio + val_ratio: 0.5 # Validation set ratio + test_ratio: 0.4 # Test set ratio # Synthetic data parameters (only used when use_synthetic=true) n_samples: 1000 # Number of samples -- Gitee From 0248bd8393f031b189bdd0db04bcb8146079ca7f Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Fri, 25 Jul 2025 10:38:51 +0800 Subject: [PATCH 19/25] Edit codes --- MindEnergy/application/deeponet-grid/src/trainer.py | 3 --- MindEnergy/application/deeponet-grid/train.py | 7 ++++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/MindEnergy/application/deeponet-grid/src/trainer.py b/MindEnergy/application/deeponet-grid/src/trainer.py index 64a035a7c..9b805893f 100644 --- a/MindEnergy/application/deeponet-grid/src/trainer.py +++ b/MindEnergy/application/deeponet-grid/src/trainer.py @@ -314,9 +314,6 @@ class DeepONetTrainer: best["prob loss"] = val_loss self.save_model("best_model.ckpt") - # record current learning rate - current_lr = self.optimizer.learning_rate - logger.info(f"Current learning rate: {current_lr:.2e}") # Compute average epoch loss try: avg_epoch_loss = epoch_loss / batch_count diff --git a/MindEnergy/application/deeponet-grid/train.py b/MindEnergy/application/deeponet-grid/train.py index ae4971ab1..26d672aa5 100644 --- a/MindEnergy/application/deeponet-grid/train.py +++ b/MindEnergy/application/deeponet-grid/train.py @@ -145,7 +145,7 @@ def train(): help="Output directory (overrides config)", ) parser.add_argument( - "--epochs", type=int, default=10, help="Number of epochs (default: 10)" + "--epochs", type=int, help="Number of epochs" ) parser.add_argument( "--batch_size", @@ -185,8 +185,9 @@ def train(): # Create save directory os.makedirs(config["output"]["save_dir"], exist_ok=True) logger.info(f"Save directory created: {config['output']['save_dir']}") - # Set epochs to 10 as requested - config["training"]["epochs"] = args.epochs + # Set epochs + if args.epochs: + config["training"]["epochs"] = args.epochs if args.batch_size: config["training"]["batch_size"] = args.batch_size -- Gitee From f89e7d3fd348fcd47425c2bae2c3b332ae1dfaaf Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Fri, 25 Jul 2025 11:04:30 +0800 Subject: [PATCH 20/25] Edit command. --- MindEnergy/application/deeponet-grid/README.md | 2 +- MindEnergy/application/deeponet-grid/README_en.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MindEnergy/application/deeponet-grid/README.md b/MindEnergy/application/deeponet-grid/README.md index 681ab1b11..b3608086b 100644 --- a/MindEnergy/application/deeponet-grid/README.md +++ b/MindEnergy/application/deeponet-grid/README.md @@ -61,7 +61,7 @@ python train.py 多卡: ```bash -mpirun -n 8 python train.py +msrun -worker_num 8 python train.py ``` ### 2. 自定义配置 diff --git a/MindEnergy/application/deeponet-grid/README_en.md b/MindEnergy/application/deeponet-grid/README_en.md index bb31a5804..d940f1de6 100644 --- a/MindEnergy/application/deeponet-grid/README_en.md +++ b/MindEnergy/application/deeponet-grid/README_en.md @@ -60,7 +60,7 @@ python train.py --data_path /path/to/your/data.npz --epochs 500 multiple cards: ```bash -mpirun -n 8 python train.py +msrun -worker_num 8 python train.py ``` -- Gitee From d64b4f9914b86c9885008e7ea18859c250d1b0e1 Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Tue, 29 Jul 2025 15:48:39 +0800 Subject: [PATCH 21/25] Fix typo. --- MindEnergy/application/deeponet-grid/src/model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MindEnergy/application/deeponet-grid/src/model.py b/MindEnergy/application/deeponet-grid/src/model.py index d92819bd9..93cdcceb7 100644 --- a/MindEnergy/application/deeponet-grid/src/model.py +++ b/MindEnergy/application/deeponet-grid/src/model.py @@ -236,6 +236,7 @@ class Prob_DeepONet(DeepONet): self.branch_std.apply(self._init_weights) self.trunk_mu.apply(self._init_weights) self.trunk_std.apply(self._init_weights) + self.reduce_sum = ops.ReduceSum(keep_dims=True) def _init_weights(self, m): if isinstance(m, nn.Dense): @@ -257,10 +258,9 @@ class Prob_DeepONet(DeepONet): # dot product using element-wise multiplication and sum # Use ReduceSum operation for proper reduction - reduce_sum = ops.ReduceSum(keep_dims=True) - mu = reduce_sum(b_mu * t_mu, 1) # Reduce along feature dimension + mu = self.reduce_sum(b_mu * t_mu, 1) # Reduce along feature dimension # Reduce along feature dimension - log_std = reduce_sum(b_std * t_std, 1) + log_std = self.reduce_sum(b_std * t_std, 1) if self.use_bias: mu += self.bias_mu -- Gitee From 2a8b58b5781a4c171abe88b6ddbb8989e1e48ec7 Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Tue, 12 Aug 2025 15:17:12 +0800 Subject: [PATCH 22/25] Fix comments. --- .../DeepONet_Grid_UQ_Probabilistic.ipynb | 361 ++++++++++-------- .../application/deeponet-grid/README.md | 31 ++ .../application/deeponet-grid/README_en.md | 32 ++ .../application/deeponet-grid/inference.py | 311 ++++++--------- .../application/deeponet-grid/src/data.py | 135 ++----- .../application/deeponet-grid/src/metrics.py | 89 +---- .../application/deeponet-grid/src/model.py | 30 +- .../application/deeponet-grid/src/trainer.py | 161 +++----- .../application/deeponet-grid/src/utils.py | 19 +- MindEnergy/application/deeponet-grid/train.py | 264 ++++++------- 10 files changed, 603 insertions(+), 830 deletions(-) diff --git a/MindEnergy/application/deeponet-grid/DeepONet_Grid_UQ_Probabilistic.ipynb b/MindEnergy/application/deeponet-grid/DeepONet_Grid_UQ_Probabilistic.ipynb index c9643e0dd..0730d93f7 100644 --- a/MindEnergy/application/deeponet-grid/DeepONet_Grid_UQ_Probabilistic.ipynb +++ b/MindEnergy/application/deeponet-grid/DeepONet_Grid_UQ_Probabilistic.ipynb @@ -1,52 +1,152 @@ { "cells": [ { - "cell_type": "code", - "execution_count": 5, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "import os\n", - "import sys\n", - "import numpy as np\n", - "import mindspore as ms\n", - "from mindspore import context\n", - "import yaml" + "# DeepOnet-Grid" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "import os\n", - "import sys\n", - "import numpy as np\n", - "import mindspore as ms\n", - "from mindspore import context\n", - "import yaml" + "本工作构建了一个高效的网络DeepONet-Grid,用于对故障后的电力系统进行动态安全分析,该网络\n", + "\n", + "`\n", + "(i) 接收故障前和故障期间收集的轨迹作为输入,并且\n", + "(ii) 输出预测的故障后轨迹。\n", + "`\n", + "\n", + "此外,本网络还通过不确定性量化(Uncertainty Quantification)为其方法赋予了在效率与可靠/可信预测之间取得平衡的能力。\n", + "\n", + "原始论文:[DeepONet-grid-UQ: A trustworthy deep operator framework for predicting the power grid’s post-fault trajectories](!https://www.sciencedirect.com/science/article/abs/pii/S0925231223002503)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 研究背景与动机\n", + "\n", + "电力系统作为关键基础设施,其稳定性和可靠性对现代社会至关重要。然而,电网经常面临罕见但严重的故障和扰动,这些事件可能导致系统不稳定,甚至引发大规模停电。\n", + "\n", + "传统的动态安全分析需要求解复杂的非线性微分代数方程组,计算成本极高,难以实现实时分析。随着电网的转型,电力公司迫切需要能够进行近实时的动态安全评估。\n", + "\n", + "现有的机器学习方法主要关注二分类问题(稳定/不稳定),缺乏对故障后轨迹的定量预测能力。系统运营商和规划者需要了解故障后各种状态变量的轨迹,以评估电压或频率是否会违反预定义限制并触发负荷切除等保护措施。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 方法细节\n", + "\n", + "#### 工作原理\n", + "\n", + "DeepONet-Grid-UQ基于深度算子网络(Deep Operator Network)理论,其核心思想是将电力系统故障后的动态行为建模为一个算子映射问题:\n", + "\n", + "- **输入函数空间**:故障前和故障期间的轨迹数据 u(t) ∈ U \n", + "- **输出函数空间**:故障后预测轨迹 y(t) ∈ Y\n", + "- **算子映射**:G: U → Y\n", + "\n", + "该网络学习从输入轨迹到输出轨迹的非线性映射关系,即:\n", + "```\n", + "y(t) = G[u](t)\n", + "``` \n", + "\n", + "#### 网络架构设计\n", + "\n", + "图1: 网络结构图 \n", + "![网络架构图](images/arch.png)\n", + "\n", + "DeepONet采用分支-主干(Branch-Trunk)架构:\n", + "\n", + "**分支网络(Branch Network)**:\n", + "- 输入:故障前和故障期间的轨迹序列 u(t₁), u(t₂), ..., u(tₙ)\n", + "- 功能:提取输入轨迹的特征表示\n", + "- 输出:特征向量 b ∈ ℝᵖ\n", + "\n", + "**主干网络(Trunk Network)**:\n", + "- 输入:时间点 t\n", + "- 功能:学习时间基函数\n", + "- 输出:时间特征向量 τ(t) ∈ ℝᵖ\n", + "\n", + "**输出计算**:\n", + "```\n", + "G[u](t) = ⟨b, τ(t)⟩ = Σᵢ₌₁ᵖ bᵢτᵢ(t)\n", + "```\n", + "\n", + "其中 ⟨·,·⟩ 表示内积运算。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 不确定性量化方法\n", + "\n", + "原始工作介绍了两种不确定性量化方法,本工作只实现了概率DeepONet(Prob-DeepONet)不确定性量化方法:\n", + "\n", + "**1. 贝叶斯DeepONet (B-DeepONet)**:\n", + "- 使用随机梯度哈密顿蒙特卡洛(SGHMC)采样\n", + "- 从网络参数的后验分布中采样:θ ∼ p(θ|D)\n", + "- 预测不确定性通过多次前向传播获得:\n", + " ```\n", + " y(t) = ∫ G[u](t; θ) p(θ|D) dθ\n", + " ```\n", + "\n", + "**2. 概率DeepONet (Prob-DeepONet)**:\n", + "- 网络同时输出预测均值 μ(t) 和预测标准差 σ(t)\n", + "- 假设输出服从高斯分布:y(t) ∼ N(μ(t), σ²(t))\n", + "- 损失函数采用负对数似然:\n", + " ```\n", + " L = -log p(y|μ, σ) = -log N(y; μ, σ²)\n", + " ```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "实践前,确保已经正确安装最新版本的MindSpore。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "DeepOnet-grid实现分为以下6个步骤:\n", + "\n", + "1. 配置网络与训练参数\n", + "2. 数据集加载\n", + "3. 模型构建\n", + "4. 模型训练\n", + "5. 结果可视化\n", + "6. 模型推理" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ - "sys.path.append(os.path.join(os.getcwd(), 'deeponet-grid'))\n", - "sys.path.append(os.path.join(os.getcwd(), 'deeponet-grid/src'))\n", - "\n", + "import os\n", + "import sys\n", + "import numpy as np\n", + "import mindspore as ms\n", + "from mindspore import context\n", + "import yaml\n", "from src.model import Prob_DeepONet\n", - "# from mindenergy.models import Prob_DeepONet\n", "from src.data import load_real_data, prepare_deeponet_data, normalize_data, split_data, create_datasets, trajectory_prediction, batch_trajectory_prediction\n", "from src.trainer import create_trainer\n", - "from src.metrics import compute_metrics, update_metrics_history, test, MetricsCalculator" + "from src.metrics import compute_metrics, update_metrics_history, MetricsCalculator" ] }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 2, "metadata": {}, "outputs": [ { @@ -70,14 +170,14 @@ ], "source": [ "# ===================================\n", - "# Step 1: Configuration Parameters\n", + "# Configuration Parameters\n", "# ===================================\n", "print(\"=\" * 50)\n", "print(\"DeepONet-Grid-UQ MindSpore Configuration\")\n", "print(\"=\" * 50)\n", "\n", "# General Parameters\n", - "verbose = True\n", + "verbose = True # Whether to output detailed information\n", "seed = 1234\n", "\n", "# DeepONet parameters (based on your data)\n", @@ -95,10 +195,17 @@ "batch_size = 1024 # Batch size\n", "n_epochs = 1 # Number of epochs (for demo)\n", "\n", - "# Data parameters\n", - "version = \"v1\"\n", - "state = \"voltage\"\n", - "cont = \"mix\"\n", + "# Learning scheduler\n", + "scheduler = \"cosine\"\n", + "min_lr = 1e-7 # Minimum learning rate\n", + "max_lr = 1e-3 # Maximum learning rate\n", + "total_step = 10000 # Total number of steps\n", + "step_per_epoch = 1000 # Number of steps per epoch\n", + "decay_epoch = 10 # Number of epochs for decay\n", + "loss_type = \"nll\" # Loss function type: \"nll\" (negative log likelihood)\n", + "optimizer = \"adam\" # Optimizer\n", + "weight_decay = 0.0 # Weight decay\n", + "amp_level = \"O0\" # Mixed precision level: \"O0\", \"O1\", \"O2\"\n", "\n", "print(f\"Configuration:\")\n", "print(f\" Sensors (m): {m}\")\n", @@ -113,14 +220,14 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "[WARNING] ME(60943:8377666560,MainProcess):2025-07-10-11:08:40.849.842 [mindspore/context.py:1335] For 'context.set_context', the parameter 'device_target' will be deprecated and removed in a future version. Please use the api mindspore.set_device() instead.\n" + "[WARNING] ME(3634:8377666560,MainProcess):2025-08-12-14:55:51.873.838 [mindspore/context.py:1335] For 'context.set_context', the parameter 'device_target' will be deprecated and removed in a future version. Please use the api mindspore.set_device() instead.\n" ] }, { @@ -135,9 +242,6 @@ } ], "source": [ - "# ===================================\n", - "# Step 2: Set up device and context\n", - "# ===================================\n", "print(\"\\n\" + \"=\" * 50)\n", "print(\"Setting up MindSpore device and context\")\n", "print(\"=\" * 50)\n", @@ -147,12 +251,12 @@ "ms.set_seed(seed)\n", "\n", "# Set up device context\n", - "context.set_context(mode=context.PYNATIVE_MODE, device_target=\"CPU\")\n" + "context.set_context(mode=context.PYNATIVE_MODE, device_target=\"Ascend\")\n" ] }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -163,24 +267,24 @@ "==================================================\n", "Loading training and test data\n", "==================================================\n", - "Loading training data from: /Users/congwang/Documents/codes/M_deeponetgrid/deeponet_ms/data/grid_real/data/trustworthydatasetv1/train-data-voltage-m-33-Q-100-mix.npz\n", - "Training data shapes: u=(11574, 33), y=(11574, 100), s=(11574, 100)\n", - "Loading test data from: /Users/congwang/Documents/codes/M_deeponetgrid/deeponet_ms/data/grid_real/data/trustworthydatasetv1/test-data-voltage-m-33-mix.npz\n", - "Test data shapes: u=(1286, 33), y=(1286, 100), s=(1286, 100)\n" + "Loading training data from: /Users/congwang/Documents/codes/M_deeponetgrid/deeponet_ms/data/grid_real/data/trustworthydatasetv2/train-data-voltage-m-33-Q-100-mix.npz\n", + "Training data shapes: u=(77069, 33), y=(77069, 100), s=(77069, 100)\n", + "Loading test data from: /Users/congwang/Documents/codes/M_deeponetgrid/deeponet_ms/data/grid_real/data/trustworthydatasetv2/test-data-voltage-m-33-mix.npz\n", + "Test data shapes: u=(8564, 33), y=(8564, 100), s=(8564, 100)\n" ] } ], "source": [ "# ===================================\n", - "# Step 3: Load training and test data\n", + "# Load training and test data\n", "# ===================================\n", "print(\"\\n\" + \"=\" * 50)\n", "print(\"Loading training and test data\")\n", "print(\"=\" * 50)\n", "\n", "# Define data paths\n", - "train_path = f\"/Users/congwang/Documents/codes/M_deeponetgrid/deeponet_ms/data/grid_real/data/trustworthydataset{version}/train-data-{state}-m-{m}-Q-{q}-{cont}.npz\"\n", - "test_path = f\"/Users/congwang/Documents/codes/M_deeponetgrid/deeponet_ms/data/grid_real/data/trustworthydataset{version}/test-data-{state}-m-{m}-{cont}.npz\"\n", + "train_path = f\"/Users/congwang/Documents/codes/M_deeponetgrid/deeponet_ms/data/grid_real/data/trustworthydatasetv2/train-data-voltage-m-33-Q-100-mix.npz\"\n", + "test_path = f\"/Users/congwang/Documents/codes/M_deeponetgrid/deeponet_ms/data/grid_real/data/trustworthydatasetv2/test-data-voltage-m-33-mix.npz\"\n", "\n", "# Check if files exist\n", "if not os.path.exists(train_path):\n", @@ -204,7 +308,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -216,9 +320,9 @@ "Preparing data for DeepONet training\n", "==================================================\n", "Preparing training data...\n", - "Expanded training data: u=(1157400, 33), y=(1157400, 1), s=(1157400, 1)\n", + "Expanded training data: u=(7706900, 33), y=(7706900, 1), s=(7706900, 1)\n", "Preparing test data...\n", - "Expanded test data: u=(128600, 33), y=(128600, 1), s=(128600, 1)\n", + "Expanded test data: u=(856400, 33), y=(856400, 1), s=(856400, 1)\n", "Splitting data...\n", "Creating MindSpore datasets...\n", "Created datasets: ['train', 'val', 'test']\n" @@ -227,7 +331,7 @@ ], "source": [ "# ===================================\n", - "# Step 4: Prepare data for DeepONet\n", + "# Prepare data for DeepONet\n", "# ===================================\n", "print(\"\\n\" + \"=\" * 50)\n", "print(\"Preparing data for DeepONet training\")\n", @@ -243,11 +347,6 @@ "u_test_expanded, y_test_expanded, s_test_expanded, test_metadata = prepare_deeponet_data(u_test, y_test, s_test)\n", "print(f\"Expanded test data: u={u_test_expanded.shape}, y={y_test_expanded.shape}, s={s_test_expanded.shape}\")\n", "\n", - "# Normalize data (optional - you can comment this out if not needed)\n", - "# print(\"Normalizing data...\")\n", - "# u_train_norm, y_train_norm, s_train_norm, scalers = normalize_data(\n", - "# u_train_expanded, y_train_expanded, s_train_expanded, method='standard'\n", - "# )\n", "\n", "# Split data, only use 10% of data for training for demo\n", "print(\"Splitting data...\")\n", @@ -264,7 +363,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -283,7 +382,7 @@ ], "source": [ "# ===================================\n", - "# Step 5: Build Probabilistic DeepONet\n", + "# Build Probabilistic DeepONet\n", "# ===================================\n", "print(\"\\n\" + \"=\" * 50)\n", "print(\"Building Probabilistic DeepONet model\")\n", @@ -316,17 +415,9 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": null, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "[WARNING] ME(60943:8377666560,MainProcess):2025-07-10-11:08:44.528.767 [mindspore/context.py:1335] For 'context.set_context', the parameter 'device_target' will be deprecated and removed in a future version. Please use the api mindspore.set_device() instead.\n", - "INFO:src.trainer:Device set to: CPU, Mode: PYNATIVE_MODE\n" - ] - }, { "name": "stdout", "output_type": "stream", @@ -341,7 +432,7 @@ ], "source": [ "# ===================================\n", - "# Step 6: Create trainer and training configuration\n", + "# Create trainer and training configuration\n", "# ===================================\n", "print(\"\\n\" + \"=\" * 50)\n", "print(\"Creating trainer and training configuration\")\n", @@ -361,21 +452,21 @@ " 'use_bias': True\n", " },\n", " 'training': {\n", - " 'learning_rate': learning_rate,\n", " 'batch_size': batch_size,\n", " 'epochs': n_epochs,\n", - " 'log_interval': 10,\n", - " 'eval_interval': 100,\n", - " \n", + " 'log_interval': 100,\n", + " 'eval_interval': 1000,\n", + " 'scheduler': scheduler,\n", + " 'min_lr': min_lr,\n", + " 'max_lr': max_lr,\n", + " 'total_step': total_step,\n", + " 'step_per_epoch': step_per_epoch,\n", + " 'decay_epoch': decay_epoch,\n", " 'verbose': verbose,\n", " 'loss_type': 'nll',\n", " 'optimizer': 'adam',\n", " 'weight_decay': 0.0\n", " },\n", - " 'device': {\n", - " 'target': 'CPU',\n", - " 'mode': 'PYNATIVE_MODE'\n", - " },\n", " 'output': {\n", " 'save_dir': 'outputs',\n", " 'save_best': True\n", @@ -383,7 +474,7 @@ "}\n", "\n", "# Create trainer\n", - "trainer = create_trainer(model, config, save_dir='outputs')\n", + "trainer = create_trainer(model, config, save_dir='outputs', distributed=0)\n", "print(f\"Trainer created successfully\")" ] }, @@ -408,7 +499,7 @@ ], "source": [ "# ===================================\n", - "# Step 7: Initial test (before training)\n", + "# Initial test (before training)\n", "# ===================================\n", "print(\"\\n\" + \"=\" * 50)\n", "print(\"Initial test (before training)\")\n", @@ -430,17 +521,16 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "INFO:root:Total model parameters: 343202\n", - "INFO:root:Trainable parameters: 343202\n", - "INFO:root:Training data size (number of samples): 116736\n", - "INFO:root:Training batch size : 1024\n" + "INFO:src.trainer:\n", + "***** Probabilistic Training for 1 epochs *****\n", + "\n" ] }, { @@ -450,80 +540,45 @@ "\n", "==================================================\n", "Starting training\n", - "==================================================\n", - "Model parameter details:\n", - "bias_mu: shape=(1,), size=1\n", - "bias_std: shape=(1,), size=1\n", - "branch.net.0.weight: shape=(200, 33), size=6600\n", - "branch.net.0.bias: shape=(200,), size=200\n", - "branch.net.1.weight: shape=(200, 200), size=40000\n", - "branch.net.1.bias: shape=(200,), size=200\n", - "branch.U.0.weight: shape=(200, 33), size=6600\n", - "branch.U.0.bias: shape=(200,), size=200\n", - "branch.V.0.weight: shape=(200, 33), size=6600\n", - "branch.V.0.bias: shape=(200,), size=200\n", - "trunk.net.0.weight: shape=(200, 1), size=200\n", - "trunk.net.0.bias: shape=(200,), size=200\n", - "trunk.net.1.weight: shape=(200, 200), size=40000\n", - "trunk.net.1.bias: shape=(200,), size=200\n", - "trunk.U.0.weight: shape=(200, 1), size=200\n", - "trunk.U.0.bias: shape=(200,), size=200\n", - "trunk.V.0.weight: shape=(200, 1), size=200\n", - "trunk.V.0.bias: shape=(200,), size=200\n", - "branch_mu.1.weight: shape=(200, 200), size=40000\n", - "branch_mu.1.bias: shape=(200,), size=200\n", - "branch_mu.3.weight: shape=(100, 200), size=20000\n", - "branch_mu.3.bias: shape=(100,), size=100\n", - "branch_std.1.weight: shape=(200, 200), size=40000\n", - "branch_std.1.bias: shape=(200,), size=200\n", - "branch_std.3.weight: shape=(100, 200), size=20000\n", - "branch_std.3.bias: shape=(100,), size=100\n", - "trunk_mu.1.weight: shape=(200, 200), size=40000\n", - "trunk_mu.1.bias: shape=(200,), size=200\n", - "trunk_mu.3.weight: shape=(100, 200), size=20000\n", - "trunk_mu.3.bias: shape=(100,), size=100\n", - "trunk_std.1.weight: shape=(200, 200), size=40000\n", - "trunk_std.1.bias: shape=(200,), size=200\n", - "trunk_std.3.weight: shape=(100, 200), size=20000\n", - "trunk_std.3.bias: shape=(100,), size=100\n", - "\n", - "***** Probabilistic Training for 1 epochs *****\n", - "\n" + "==================================================\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "INFO:root:Epoch 1, Step 10, Batch 10, Loss: 0.621717, Step time: 0.077s\n", - "INFO:root:Epoch 1, Step 20, Batch 20, Loss: 0.412504, Step time: 0.082s\n", - "INFO:root:Epoch 1, Step 30, Batch 30, Loss: 0.241822, Step time: 0.093s\n", - "INFO:root:Epoch 1, Step 40, Batch 40, Loss: 0.108371, Step time: 0.089s\n", - "INFO:root:Epoch 1, Step 50, Batch 50, Loss: -0.016050, Step time: 0.088s\n", - "INFO:root:Epoch 1, Step 60, Batch 60, Loss: -0.252723, Step time: 0.077s\n", - "INFO:root:Epoch 1, Step 70, Batch 70, Loss: -0.371130, Step time: 0.084s\n", - "INFO:root:Epoch 1, Step 80, Batch 80, Loss: -0.305221, Step time: 0.098s\n", - "INFO:root:Epoch 1, Step 90, Batch 90, Loss: -0.583641, Step time: 0.090s\n", - "INFO:root:Epoch 1, Step 100, Batch 100, Loss: -0.720639, Step time: 0.086s\n", - "INFO:root:[Eval] Epoch 1, Step 100, Val-Loss: -0.721453 \n", + "[WARNING] ME(3634:8377666560,MainProcess):2025-08-12-14:56:49.442.836 [mindspore/common/_decorator.py:40] 'identity' is deprecated from version 2.0 and will be removed in a future version, use 'nn.Identity' instead.\n", + "INFO:root:Epoch 1, Step 100, Batch 100, Loss: 2.176179, Step time: 0.108s\n", + "INFO:root:[Eval] Epoch 1, Step 100, Val-Loss: 2.280707 \n", "INFO:src.trainer:Model saved to: outputs/best_model.ckpt\n", - "INFO:root:Epoch 1, Step 110, Batch 110, Loss: -0.874301, Step time: 0.048s\n" + "INFO:root:Epoch 1, Step 200, Batch 200, Loss: 1.608493, Step time: 0.097s\n" ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Epoch 1/1:\n", - " Train-Loss: -0.121288 \n", - " Best-Loss: -0.721453 \n", - "Training completed!\n" + "ename": "KeyboardInterrupt", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[12], line 9\u001b[0m\n\u001b[1;32m 6\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m=\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;241m*\u001b[39m \u001b[38;5;241m50\u001b[39m)\n\u001b[1;32m 8\u001b[0m \u001b[38;5;66;03m# Train the model\u001b[39;00m\n\u001b[0;32m----> 9\u001b[0m history \u001b[38;5;241m=\u001b[39m \u001b[43mtrainer\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mtrain\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 10\u001b[0m \u001b[43m \u001b[49m\u001b[43mtrain_dataset\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdatasets\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mtrain\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 11\u001b[0m \u001b[43m \u001b[49m\u001b[43mval_dataset\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdatasets\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mval\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m]\u001b[49m\n\u001b[1;32m 12\u001b[0m \u001b[43m)\u001b[49m\n\u001b[1;32m 14\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mTraining completed!\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "File \u001b[0;32m~/Documents/codes/M_deeponetgrid/deeponet-grid/src/trainer.py:260\u001b[0m, in \u001b[0;36mDeepONetTrainer.train\u001b[0;34m(self, train_dataset, val_dataset)\u001b[0m\n\u001b[1;32m 258\u001b[0m \u001b[38;5;66;03m# evaluate\u001b[39;00m\n\u001b[1;32m 259\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m val_dataset \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;129;01mand\u001b[39;00m global_step \u001b[38;5;241m%\u001b[39m eval_every \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[0;32m--> 260\u001b[0m val_loss \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mvalidate\u001b[49m\u001b[43m(\u001b[49m\u001b[43mval_dataset\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 261\u001b[0m logger_hist[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mval loss\u001b[39m\u001b[38;5;124m\"\u001b[39m]\u001b[38;5;241m.\u001b[39mappend((global_step, val_loss))\n\u001b[1;32m 262\u001b[0m \u001b[38;5;66;03m# Negative log likelihood loss can be negative\u001b[39;00m\n", + "File \u001b[0;32m~/Documents/codes/M_deeponetgrid/deeponet-grid/src/trainer.py:189\u001b[0m, in \u001b[0;36mDeepONetTrainer.validate\u001b[0;34m(self, val_dataset)\u001b[0m\n\u001b[1;32m 186\u001b[0m num_batches \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m0\u001b[39m\n\u001b[1;32m 188\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m u, y, target \u001b[38;5;129;01min\u001b[39;00m val_dataset:\n\u001b[0;32m--> 189\u001b[0m mean_pred, log_std_pred \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmodel\u001b[49m\u001b[43m(\u001b[49m\u001b[43mu\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43my\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 190\u001b[0m loss \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mloss_fn(mean_pred, log_std_pred, target)\n\u001b[1;32m 191\u001b[0m total_loss \u001b[38;5;241m+\u001b[39m\u001b[38;5;241m=\u001b[39m \u001b[38;5;28mfloat\u001b[39m(loss)\n", + "File \u001b[0;32m~/anaconda3/envs/ms25_py310/lib/python3.10/site-packages/mindspore/nn/cell.py:743\u001b[0m, in \u001b[0;36mCell.__call__\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 740\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mrequires_grad \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_dynamic_shape_inputs \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmixed_precision_type):\n\u001b[1;32m 741\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_forward_pre_hook \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_forward_hook \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_backward_pre_hook \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_backward_hook \u001b[38;5;129;01mor\u001b[39;00m\n\u001b[1;32m 742\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_shard_fn \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_recompute_cell \u001b[38;5;129;01mor\u001b[39;00m (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mhas_bprop \u001b[38;5;129;01mand\u001b[39;00m _pynative_executor\u001b[38;5;241m.\u001b[39mrequires_grad())):\n\u001b[0;32m--> 743\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mconstruct\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 745\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_run_construct(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n\u001b[1;32m 747\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_complex_call(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n", + "File \u001b[0;32m~/Documents/codes/M_deeponetgrid/deeponet-grid/src/model.py:235\u001b[0m, in \u001b[0;36mProb_DeepONet.construct\u001b[0;34m(self, xu, xy)\u001b[0m\n\u001b[1;32m 233\u001b[0m u, y \u001b[38;5;241m=\u001b[39m xu, xy\n\u001b[1;32m 234\u001b[0m b \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mbranch(u)\n\u001b[0;32m--> 235\u001b[0m t \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mtrunk\u001b[49m\u001b[43m(\u001b[49m\u001b[43my\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 236\u001b[0m \u001b[38;5;66;03m# branch prediction and UQ\u001b[39;00m\n\u001b[1;32m 237\u001b[0m b_mu \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mbranch_mu(b)\n", + "File \u001b[0;32m~/anaconda3/envs/ms25_py310/lib/python3.10/site-packages/mindspore/nn/cell.py:743\u001b[0m, in \u001b[0;36mCell.__call__\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 740\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mrequires_grad \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_dynamic_shape_inputs \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmixed_precision_type):\n\u001b[1;32m 741\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_forward_pre_hook \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_forward_hook \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_backward_pre_hook \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_backward_hook \u001b[38;5;129;01mor\u001b[39;00m\n\u001b[1;32m 742\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_shard_fn \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_recompute_cell \u001b[38;5;129;01mor\u001b[39;00m (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mhas_bprop \u001b[38;5;129;01mand\u001b[39;00m _pynative_executor\u001b[38;5;241m.\u001b[39mrequires_grad())):\n\u001b[0;32m--> 743\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mconstruct\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 745\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_run_construct(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n\u001b[1;32m 747\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_complex_call(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n", + "File \u001b[0;32m~/Documents/codes/M_deeponetgrid/deeponet-grid/src/model.py:81\u001b[0m, in \u001b[0;36mmodified_MLP.construct\u001b[0;34m(self, x)\u001b[0m\n\u001b[1;32m 79\u001b[0m v \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mactivation(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mV(x))\n\u001b[1;32m 80\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m k \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mrange\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlen):\n\u001b[0;32m---> 81\u001b[0m y \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mnet\u001b[49m\u001b[43m[\u001b[49m\u001b[43mk\u001b[49m\u001b[43m]\u001b[49m\u001b[43m(\u001b[49m\u001b[43mx\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 82\u001b[0m y \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mactivation(y)\n\u001b[1;32m 83\u001b[0m x \u001b[38;5;241m=\u001b[39m y \u001b[38;5;241m*\u001b[39m u \u001b[38;5;241m+\u001b[39m (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mones(y) \u001b[38;5;241m-\u001b[39m y) \u001b[38;5;241m*\u001b[39m v\n", + "File \u001b[0;32m~/anaconda3/envs/ms25_py310/lib/python3.10/site-packages/mindspore/nn/cell.py:743\u001b[0m, in \u001b[0;36mCell.__call__\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 740\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mrequires_grad \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_dynamic_shape_inputs \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmixed_precision_type):\n\u001b[1;32m 741\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_forward_pre_hook \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_forward_hook \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_backward_pre_hook \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_backward_hook \u001b[38;5;129;01mor\u001b[39;00m\n\u001b[1;32m 742\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_shard_fn \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_recompute_cell \u001b[38;5;129;01mor\u001b[39;00m (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mhas_bprop \u001b[38;5;129;01mand\u001b[39;00m _pynative_executor\u001b[38;5;241m.\u001b[39mrequires_grad())):\n\u001b[0;32m--> 743\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mconstruct\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 745\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_run_construct(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n\u001b[1;32m 747\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_complex_call(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n", + "File \u001b[0;32m~/anaconda3/envs/ms25_py310/lib/python3.10/site-packages/mindspore/nn/layer/basic.py:742\u001b[0m, in \u001b[0;36mDense.construct\u001b[0;34m(self, x)\u001b[0m\n\u001b[1;32m 741\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21mconstruct\u001b[39m(\u001b[38;5;28mself\u001b[39m, x):\n\u001b[0;32m--> 742\u001b[0m x_shape \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mshape_op\u001b[49m\u001b[43m(\u001b[49m\u001b[43mx\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 743\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(x_shape) \u001b[38;5;241m!=\u001b[39m \u001b[38;5;241m2\u001b[39m:\n\u001b[1;32m 744\u001b[0m x \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mreshape(x, (\u001b[38;5;241m-\u001b[39m\u001b[38;5;241m1\u001b[39m, x_shape[\u001b[38;5;241m-\u001b[39m\u001b[38;5;241m1\u001b[39m]))\n", + "File \u001b[0;32m~/anaconda3/envs/ms25_py310/lib/python3.10/site-packages/mindspore/ops/operations/manually_defined/ops_def.py:857\u001b[0m, in \u001b[0;36mShape.__call__\u001b[0;34m(self, x)\u001b[0m\n\u001b[1;32m 855\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21m__call__\u001b[39m(\u001b[38;5;28mself\u001b[39m, x):\n\u001b[1;32m 856\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(x, (Tensor, COOTensor, CSRTensor, Tensor_)):\n\u001b[0;32m--> 857\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mx\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mshape\u001b[49m\n\u001b[1;32m 858\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mFor primitive[\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mname\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m], the input argument must be Tensor, but got \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mtype\u001b[39m(x)\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m.\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "File \u001b[0;32m~/anaconda3/envs/ms25_py310/lib/python3.10/site-packages/mindspore/common/_stub_tensor.py:91\u001b[0m, in \u001b[0;36mStubTensor.shape\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 89\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mstub:\n\u001b[1;32m 90\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mhasattr\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mstub_shape\u001b[39m\u001b[38;5;124m\"\u001b[39m):\n\u001b[0;32m---> 91\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mstub_shape \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstub\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_shape\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 92\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mstub_shape\n\u001b[1;32m 93\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mtensor\u001b[38;5;241m.\u001b[39mshape\n", + "\u001b[0;31mKeyboardInterrupt\u001b[0m: " ] } ], "source": [ "# ===================================\n", - "# Step 8: Training\n", + "# Training\n", "# ===================================\n", "print(\"\\n\" + \"=\" * 50)\n", "print(\"Starting training\")\n", @@ -540,7 +595,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -553,21 +608,21 @@ "==================================================\n", "Evaluating on test set...\n", "Test Results:\n", - " MSE: 0.007808\n", - " MAE: 0.055851\n", - " R2: -0.711629\n", - " L1_RELATIVE_ERROR: 0.055933\n", - " L2_RELATIVE_ERROR: 0.088294\n", + " MSE: 0.796076\n", + " MAE: 0.747389\n", + " R2: -15.094687\n", + " L1_RELATIVE_ERROR: 0.965667\n", + " L2_RELATIVE_ERROR: 1.107990\n", " CALIBRATION_ERROR: 0.000000\n", - " FRACTION_IN_CI: 0.970874\n", - " TRAJECTORY_L1_ERROR: 0.055933\n", - " TRAJECTORY_L2_ERROR: 0.088294\n" + " FRACTION_IN_CI: 0.860200\n", + " TRAJECTORY_L1_ERROR: 0.965667\n", + " TRAJECTORY_L2_ERROR: 1.107990\n" ] } ], "source": [ "# ===================================\n", - "# Step 9: Evaluation and testing\n", + "# Evaluation and testing\n", "# ===================================\n", "print(\"\\n\" + \"=\" * 50)\n", "print(\"Evaluation and testing\")\n", @@ -603,7 +658,7 @@ ], "source": [ "# ===================================\n", - "# Step 10: Trajectory prediction and UQ\n", + "# Trajectory prediction and UQ\n", "# ===================================\n", "print(\"\\n\" + \"=\" * 50)\n", "print(\"Trajectory prediction and uncertainty quantification\")\n", @@ -656,7 +711,7 @@ ], "source": [ "# ===================================\n", - "# Step 11: Save results\n", + "# Save results\n", "# ===================================\n", "print(\"\\n\" + \"=\" * 50)\n", "print(\"Saving results\")\n", @@ -694,7 +749,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "ms25_py310", "language": "python", "name": "python3" }, diff --git a/MindEnergy/application/deeponet-grid/README.md b/MindEnergy/application/deeponet-grid/README.md index b3608086b..c55dff9b8 100644 --- a/MindEnergy/application/deeponet-grid/README.md +++ b/MindEnergy/application/deeponet-grid/README.md @@ -2,6 +2,31 @@ 用于电力系统故障后进行预测的DeepONet-Grid网络 +## 背景介绍 + +### 需求来源及价值概述 + +本工作构建了一个高效的网络DeepONet-Grid,用于对故障后的电力系统进行动态安全分析,该网络 + + (i) 接收故障前和故障期间收集的轨迹作为输入,并且 + (ii) 输出预测的故障后轨迹。 + +此外,本网络还通过不确定性量化(Uncertainty Quantification)为其方法赋予了在效率与可靠/可信预测之间取得平衡的能力。 + +原始论文:[DeepONet-grid-UQ: A trustworthy deep operator framework for predicting the power grid's post-fault trajectories](!https://www.sciencedirect.com/science/article/abs/pii/S0925231223002503) + +原始代码仓:[Github Link](!https://github.com/cmoyacal/DeepONet-Grid-UQ) + + + +### 研究背景与动机 + +电力系统作为关键基础设施,其稳定性和可靠性对现代社会至关重要。然而,电网经常面临罕见但严重的故障和扰动,这些事件可能导致系统不稳定,甚至引发大规模停电。 + +传统的动态安全分析需要求解复杂的非线性微分代数方程组,计算成本极高,难以实现实时分析。随着电网的转型,电力公司迫切需要能够进行近实时的动态安全评估。 + +现有的机器学习方法主要关注二分类问题(稳定/不稳定),缺乏对故障后轨迹的定量预测能力。系统运营商和规划者需要了解故障后各种状态变量的轨迹,以评估电压或频率是否会违反预定义限制并触发负荷切除等保护措施。 + ## 项目结构 ``` @@ -214,6 +239,12 @@ def load_custom_data(data_path): ## 训练结果 + +| 参数 | 指标 | +| :-----------: | :-----------: | +| 硬件资源 | Atlas 800T A2 | +| MindSpore版本 | >=2.5.0 | + 使用10000条测试数据 (n_samples = 10000, n_sensors = 200, timesteps = 100) ,训练2500步后得到的训练结果(batch_size = 1024): | MSE | MAE | L1 | L2 | diff --git a/MindEnergy/application/deeponet-grid/README_en.md b/MindEnergy/application/deeponet-grid/README_en.md index d940f1de6..b713f8e12 100644 --- a/MindEnergy/application/deeponet-grid/README_en.md +++ b/MindEnergy/application/deeponet-grid/README_en.md @@ -2,6 +2,30 @@ DeepONet-Grid network for power system fault prediction +## Background Introduction + +### Source of Requirements and Value Overview + +This work build an efficient DeepONet that + + (i) takes as inputs the trajectories collected before and during the fault and + (ii) outputs the predicted post-fault trajectories. + +In addition, they also endow their method with the much-needed ability to balance efficiency with reliable/trustworthy predictions via Ucertainty Quantification. + +Original Paper : [DeepONet-grid-UQ: A trustworthy deep operator framework for predicting the power grid’s post-fault trajectories](!https://www.sciencedirect.com/science/article/abs/pii/S0925231223002503) +Original Code on torch: [Github](!https://github.com/cmoyacal/DeepONet-Grid-UQ) + + + +### Research Background and Motivation + +Power systems, as critical infrastructure, are essential for the stability and reliability of modern society. However, power grids frequently face rare but severe faults and disturbances, which can lead to system instability and even trigger large-scale blackouts. + +Traditional dynamic security analysis requires solving complex nonlinear differential-algebraic equations, with extremely high computational costs, making real-time analysis difficult to achieve. With the transformation of power grids, power companies urgently need the capability to perform near real-time dynamic security assessment. + +Existing machine learning methods mainly focus on binary classification problems (stable/unstable), lacking quantitative prediction capabilities for post-fault trajectories. System operators and planners need to understand the trajectories of various state variables after faults to assess whether voltage or frequency will violate predefined limits and trigger protection measures such as load shedding. + ## Project Structure ``` @@ -210,6 +234,14 @@ def load_custom_data(data_path): ## Training results + + + +| Name | Version | +| :---------------: | :-----------: | +| Hardware | Atlas 800T A2 | +| MindSpore version | >=2.5.0 | + Used 10000 data samples (n_samples = 10000, n_sensors = 200, timesteps = 100) ,trained after 2500 steps with batch_size 1024: | MSE | MAE | L1 | L2 | diff --git a/MindEnergy/application/deeponet-grid/inference.py b/MindEnergy/application/deeponet-grid/inference.py index 0d9811473..c849d44db 100644 --- a/MindEnergy/application/deeponet-grid/inference.py +++ b/MindEnergy/application/deeponet-grid/inference.py @@ -1,4 +1,19 @@ #!/usr/bin/env python3 +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + """ Main inference script for DeepONet-Grid-UQ """ @@ -14,128 +29,71 @@ import mindspore as ms import numpy as np import yaml from mindspore import context - from src.data import ( - load_real_data, + batch_trajectory_prediction, load_and_preprocess_real_data, + load_real_data, trajectory_prediction, - batch_trajectory_prediction, ) -from src.model import Prob_DeepONet from src.metrics import MetricsCalculator +from src.model import Prob_DeepONet from src.utils import load_config, load_trained_model -# Add src to path -sys.path.append(os.path.join(os.path.dirname(__file__), "src")) - - -# Set up logging -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - handlers=[ - logging.FileHandler("inference.log"), - logging.StreamHandler( - sys.stdout)], -) -logger = logging.getLogger(__name__) - -# Set device context -context.set_context(mode=context.PYNATIVE_MODE, device_target="CPU") -logger.info(f"Mode set to: {context.get_context('mode')}") -logger.info(f"Device set to: {context.get_context('device_target')}") - - -def create_model(config: dict, n_sensors=33) -> Prob_DeepONet: - """Create DeepONet model based on configuration and data metadata - - Args: - config: Configuration dictionary - metadata: Data metadata containing input/output dimensions - - Returns: - DeepONet model instance - """ - model_config = config["model"] - - # Get network parameters from metadata if available, otherwise use config - # defaults - m = n_sensors # Number of sensors - # Input dimension for trunk (always 1 for DeepONet) - dim = model_config.get("dim", 1) - width = model_config.get("width", 200) # Network width - # Network depth (number of hidden layers) - depth = model_config.get("depth", 3) - n_basis = model_config.get("n_basis", 100) # Number of basis functions - - # Get network types - branch_type = model_config.get("branch_type", "modified") - trunk_type = model_config.get("trunk_type", "modified") - activation = model_config.get("activation", "sin") - - # Compute layer sizes automatically - branch_layer_size = [m] + [width] * depth + [n_basis] - trunk_layer_size = [dim] + [width] * depth + [n_basis] - - # Create branch configuration - branch_config = { - "type": branch_type, - "layer_size": branch_layer_size, - "activation": activation, - } - - # Create trunk configuration - trunk_config = { - "type": trunk_type, - "layer_size": trunk_layer_size, - "activation": activation, - } - logger.info(f"Branch network: {branch_layer_size}") - logger.info(f"Trunk network: {trunk_layer_size}") - logger.info(f"Branch type: {branch_type}, Trunk type: {trunk_type}") - logger.info(f"Activation: {activation}") - logger.info(f"Depth: {depth} (number of hidden layers)") - logger.info(f"Input sensors (m): {m}") - logger.info(f"Trunk input dimension (dim): {dim}") - - # Create model - model = Prob_DeepONet( - branch=branch_config, - trunk=trunk_config, - use_bias=model_config["use_bias"]) +def parse_args(): + """Parse command line arguments""" + parser = argparse.ArgumentParser(description="DeepONet-Grid-UQ Inference") + parser.add_argument( + "--config", + type=str, + default="configs/config.yaml", + help="Path to configuration file", + ) + parser.add_argument( + "--checkpoint", + type=str, + default="outputs/best_model.ckpt", + help="Path to model checkpoint", + ) + parser.add_argument( + "--data_path", + type=str, + default="data/test-data-voltage-m-33-mix.npz", + help="Path to data file for dataset inference", + ) + parser.add_argument( + "--output_dir", + type=str, + default="inference_results", + help="Output directory for results", + ) + parser.add_argument( + "--trajectory_prediction", + action="store_true", + help="Perform single data point (with multiple time points) inference", + ) + parser.add_argument( + "--data_index", + type=int, + default=0, + help="Index of data point for single inference (default: 0)", + ) - logger.info("Model created successfully") - return model + return parser.parse_args() def batch_inference( model: Prob_DeepONet, u: np.ndarray, y: np.ndarray ) -> Tuple[np.ndarray, np.ndarray]: - """Perform inference on batch of data - - Args: - model: Trained DeepONet model - u: Input function values (shape: [batch_size, n_sensors]) - y: Evaluation points (shape: [batch_size, n_points, 1]) - - Returns: - Tuple of (mean_pred, std_pred) - """ - # Convert to MindSpore tensors + """Perform inference on batch of data""" u_tensor = ms.Tensor(u, ms.float32) y_tensor = ms.Tensor(y, ms.float32) - # Set model to evaluation mode model.set_train(False) - # Forward pass mean_pred, log_std_pred = model(u_tensor, y_tensor) - - # Convert log_std to std std_pred = ms.ops.Exp()(log_std_pred) - # Convert back to numpy mean_np = mean_pred.asnumpy() std_np = std_pred.asnumpy() @@ -148,20 +106,9 @@ def inference_on_dataset( checkpoint_path: str, output_dir: str = "inference_results", ) -> None: - """Perform inference on entire dataset - - Args: - model: DeepONet model - dataset_path: Path to dataset file - checkpoint_path: Path to model checkpoint - output_dir: Output directory for results - """ - # Load trained model + """Perform inference on entire dataset""" model = load_trained_model(model, checkpoint_path) - logger.info(f"Model loaded from: {checkpoint_path}") - # Load dataset - logger.info(f"Loading dataset from: {dataset_path}") datasets, metadata = load_and_preprocess_real_data( { "data": { @@ -175,11 +122,8 @@ def inference_on_dataset( } ) - # Create output directory os.makedirs(output_dir, exist_ok=True) - # Perform inference on test set - logger.info("Performing inference on test dataset...") test_dataset = datasets["test"] all_predictions = [] @@ -188,30 +132,25 @@ def inference_on_dataset( all_stds = [] for u, y, target in test_dataset: - # Forward pass mean_pred, log_std_pred = model(u, y) std_pred = ms.ops.Exp()(log_std_pred) - # Store results all_predictions.append(mean_pred) all_targets.append(target) all_means.append(mean_pred) all_stds.append(std_pred) - # Concatenate all batches if all_predictions: predictions = ms.ops.Concat(axis=0)(all_predictions) targets = ms.ops.Concat(axis=0)(all_targets) means = ms.ops.Concat(axis=0)(all_means) stds = ms.ops.Concat(axis=0)(all_stds) - # Convert to numpy for saving predictions_np = predictions.asnumpy() targets_np = targets.asnumpy() means_np = means.asnumpy() stds_np = stds.asnumpy() - # Save results results = { "predictions": predictions_np.tolist(), "targets": targets_np.tolist(), @@ -223,82 +162,69 @@ def inference_on_dataset( with open(results_path, "w") as f: json.dump(results, f, indent=2) - logger.info(f"Inference results saved to: {results_path}") - logger.info(f"Predictions shape: {predictions_np.shape}") - logger.info(f"Targets shape: {targets_np.shape}") - -def inference(): +def inference(args, logger): """Main inference function""" - parser = argparse.ArgumentParser(description="DeepONet-Grid-UQ Inference") - parser.add_argument( - "--config", - type=str, - default="configs/config.yaml", - help="Path to configuration file", - ) - parser.add_argument( - "--checkpoint", - type=str, - default="outputs/best_model.ckpt", - help="Path to model checkpoint") - parser.add_argument( - "--data_path", - type=str, - default="data/test-data-voltage-m-33-mix.npz", - help="Path to data file for dataset inference", - ) - parser.add_argument( - "--output_dir", - type=str, - default="inference_results", - help="Output directory for results", - ) - parser.add_argument( - "--trajectory_prediction", - action="store_true", - help="Perform single data point (with multiple time points) inference", - ) - parser.add_argument( - "--data_index", - type=int, - default=0, - help="Index of data point for single inference (default: 0)", - ) - - args = parser.parse_args() - # Load configuration - logger.info(f"Loading configuration from {args.config}") + logger.debug(f"Loading configuration from {args.config}") config = load_config(args.config) - # Create model - logger.info("Creating model...") - model = create_model(config, n_sensors=33) + logger.debug("Creating model...") + model_config = config["model"] + + m = model_config.get("m", 33) + dim = model_config.get("dim", 1) + width = model_config.get("width", 200) + depth = model_config.get("depth", 3) + n_basis = model_config.get("n_basis", 100) + + branch_type = model_config.get("branch_type", "modified") + trunk_type = model_config.get("trunk_type", "modified") + activation = model_config.get("activation", "sin") + + branch_layer_size = [m] + [width] * depth + [n_basis] + trunk_layer_size = [dim] + [width] * depth + [n_basis] + + branch_config = { + "type": branch_type, + "layer_size": branch_layer_size, + "activation": activation, + } + + trunk_config = { + "type": trunk_type, + "layer_size": trunk_layer_size, + "activation": activation, + } + + model = Prob_DeepONet( + branch=branch_config, + trunk=trunk_config, + use_bias=model_config["use_bias"]) + + logger.debug("Model created successfully") if args.trajectory_prediction: - # Single data point inference from dataset - # Load trained model + # Test single sample trajectory prediction model = load_trained_model(model, args.checkpoint) - logger.info(f"Model loaded from: {args.checkpoint}") + logger.debug(f"Model loaded from: {args.checkpoint}") if args.data_path is None: logger.error("For single inference, --data_path must be provided") return - # Load dataset and get specific data point - logger.info(f"Loading dataset from: {args.data_path}") + logger.debug(f"Loading dataset from: {args.data_path}") u, y, s = load_real_data(args.data_path) - # Test single sample trajectory prediction u_single = u[args.data_index] y_single = y[args.data_index] predictions = trajectory_prediction(u_single, y_single, model) - logger.info( - f" Single sample prediction shape: {predictions.shape} on data index {args.data_index}") - logger.info(f" Predictions: {predictions.flatten()}") - logger.info(f" Target: {s[args.data_index].flatten()}") + logger.debug( + f"Single sample prediction shape: {predictions.shape} on data index {args.data_index}" + ) + logger.debug(f"Predictions: {predictions.flatten()}") + logger.debug(f"Target: {s[args.data_index].flatten()}") calculator = MetricsCalculator() y_true = ms.Tensor(s[args.data_index], ms.float32) @@ -306,18 +232,12 @@ def inference(): # Test L1 and L2 relative errors l1_error = calculator.l1_relative_error(y_true, y_pred) l2_error = calculator.l2_relative_error(y_true, y_pred) - logger.info(f" L1 relative error: {l1_error:.6f}") - logger.info(f" L2 relative error: {l2_error:.6f}") + logger.debug(f" L1 relative error: {l1_error:.6f}") + logger.debug(f" L2 relative error: {l2_error:.6f}") # Test trajectory relative error l1_traj, l2_traj = calculator.trajectory_rel_error(y_true, y_pred) - logger.info(f" Trajectory L1: {l1_traj:.6f}, L2: {l2_traj:.6f}") - - # batch trajectory prediction, if you want to inference [:data_index] - # u_batch = u[:args.data_index] # First 3 samples, shape (3, 33) - # y_batch = y[:args.data_index] # First 3 samples, shape (3, 100) - # batch_predictions = batch_trajectory_prediction( - # u_batch, y_batch, model) + logger.debug(f" Trajectory L1: {l1_traj:.6f}, L2: {l2_traj:.6f}") else: # Dataset inference @@ -332,8 +252,23 @@ def inference(): args.checkpoint, args.output_dir) - logger.info("Dataset inference completed successfully!") + logger.debug("Dataset inference completed successfully!") if __name__ == "__main__": - inference() + args = parse_args() + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[ + logging.FileHandler("inference.log"), + logging.StreamHandler(sys.stdout), + ], + ) + logger = logging.getLogger(__name__) + + context.set_context(mode=context.PYNATIVE_MODE, device_target="Ascend") + logger.debug(f"Mode set to: {context.get_context('mode')}") + logger.debug(f"Device set to: {context.get_context('device_target')}") + + inference(args, logger) diff --git a/MindEnergy/application/deeponet-grid/src/data.py b/MindEnergy/application/deeponet-grid/src/data.py index 41a81145e..ab81a095e 100644 --- a/MindEnergy/application/deeponet-grid/src/data.py +++ b/MindEnergy/application/deeponet-grid/src/data.py @@ -20,7 +20,7 @@ import mindspore.numpy as mnp import numpy as np import pandas as pd from mindspore import context -from mindspore.communication import get_rank, get_group_size +from mindspore.communication import get_group_size, get_rank from mindspore.dataset import GeneratorDataset from mindspore.ops import operations as ops from scipy import stats @@ -42,21 +42,16 @@ class DataGenerator: self.dtype = ms.float32 if dtype == "float32" else ms.float64 def __getitem__(self, index): - # Return data as MindSpore tensors with float32 dtype by default - # Ensure proper shapes for DeepONet u_data = self.u[index] y_data = self.y[index] s_data = self.s[index] - # Ensure y_data is 1D for DeepONet if len(y_data.shape) != 1: raise ValueError(f"y_data must be 1D, got shape {y_data.shape}") - # Ensure s_data is 1D if len(s_data.shape) != 1: raise ValueError(f"s_data must be 1D, got shape {s_data.shape}") - # Convert to MindSpore tensors with specified dtype u_tensor = ms.Tensor(u_data, self.dtype) y_tensor = ms.Tensor(y_data, self.dtype) s_tensor = ms.Tensor(s_data, self.dtype) @@ -70,20 +65,7 @@ class DataGenerator: def generate_synthetic_data( n_samples: int = 1000, n_sensors: int = 33, n_points: int = 1, seed: int = 1234 ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: - """Generate synthetic data for testing - - Args: - n_samples: Number of samples - n_sensors: Number of sensors - n_points: Number of evaluation points - seed: Random seed - - Returns: - Tuple of (u, y, s) where: - - u: Input function values at sensor locations - - y: Evaluation points - - s: True solution values - """ + """Generate synthetic data for testing""" np.random.seed(seed) # Generate input functions (u) - random functions @@ -260,7 +242,7 @@ def split_data( def create_datasets( - data_splits: Dict[str, Tuple], batch_size: int = 32 + data_splits: Dict[str, Tuple], batch_size: int = 32, distributed: int = 0 ) -> Dict[str, GeneratorDataset]: """Create datasets from data splits @@ -278,18 +260,26 @@ def create_datasets( data_gen = DataGenerator(u, y, s) # Create dataset with three columns: u, y, s - # ====== Data Parallel: add num_shards and shard_id for train set ====== + # ====== Data Parallel: add num_shards and shard_id for train set ===== if split_name == "train": - try: - rank_id = get_rank() - rank_size = get_group_size() - except Exception: - rank_id = 0 - rank_size = 1 - dataset = GeneratorDataset( - source=data_gen, column_names=["u", "y", "s"], shuffle=True, - num_shards=rank_size, shard_id=rank_id - ).batch(batch_size) + if distributed: + try: + rank_id = get_rank() + rank_size = get_group_size() + except Exception: + rank_id = 0 + rank_size = 1 + dataset = GeneratorDataset( + source=data_gen, + column_names=["u", "y", "s"], + shuffle=True, + num_shards=rank_size, + shard_id=rank_id, + ).batch(batch_size) + else: + dataset = GeneratorDataset( + source=data_gen, column_names=["u", "y", "s"], shuffle=True + ).batch(batch_size) else: dataset = GeneratorDataset( source=data_gen, column_names=["u", "y", "s"], shuffle=False @@ -305,18 +295,6 @@ def create_datasets( def prepare_deeponet_data(u, y, s, time_points=None): """ Prepare data for DeepONet training from user's data format - - Args: - u: Input functions, shape (n_samples, n_sensors) - y: Time points, shape (n_samples, n_points) - this will be converted to individual query points - s: Target values, shape (n_samples, n_points) - time_points: Optional time points array, if None will use y as time points - - Returns: - u_expanded: Expanded input functions, shape (n_samples * n_points, n_sensors) - y_expanded: Individual query points, shape (n_samples * n_points, 1) - s_expanded: Target values, shape (n_samples * n_points, 1) - metadata: Dictionary with data information """ n_samples, n_sensors = u.shape @@ -377,14 +355,6 @@ def trajectory_prediction( model) -> np.ndarray: """ Predict trajectory for a single sample using DeepONet model - - Args: - u: Input function values for single sample, shape (n_sensors,) - time_points: Time points to predict, shape (n_time_points,) - model: Trained DeepONet model - - Returns: - predictions: Predicted values at time points, shape (n_time_points, 1) """ # Ensure u is 2D for model input @@ -392,20 +362,14 @@ def trajectory_prediction( u = u.reshape(1, -1) # (1, n_sensors) n_time_points = len(time_points) - # Convert to MindSpore tensor predictions = [] - # Predict for each time point using batch - # create query points for each time point of the sample - u_expanded = np.repeat( - u, n_time_points, axis=0 - ) + u_expanded = np.repeat(u, n_time_points, axis=0) y_expanded = np.tile( time_points.reshape(-1, 1), (1, 1) ) # (batch_size * n_time_points, 1) - # convert to MindSpore tensors u_tensor = ms.Tensor(u_expanded, ms.float32) y_tensor = ms.Tensor(y_expanded, ms.float32) @@ -414,61 +378,35 @@ def trajectory_prediction( # reshape results predictions = mean_pred.reshape(1, n_time_points, 1) - std_predictions = ms.ops.exp(log_std_pred).reshape( - 1, n_time_points, 1) - - # for t in time_points: - # # Create single query point - # y_t = ms.Tensor([[t]], ms.float32) # (1, 1) - - # # Forward pass - # mean_pred, log_std_pred = model(u_tensor, y_t) - - # # Get prediction value - # pred_val = float(mean_pred[0, 0]) - # predictions.append(pred_val) + std_predictions = ms.ops.exp(log_std_pred).reshape(1, n_time_points, 1) return np.array(predictions).reshape(-1, 1) def batch_trajectory_prediction(model, u_batch, time_points): """ - Batch prediction of trajectories - - Args: - model: Trained DeepONet model - u_batch: Input function batch, shape (batch_size, n_sensors) - time_points: Time points array, shape (n_time_points,) - - Returns: - mean_predictions: Predicted mean, shape (batch_size, n_time_points, 1) - std_predictions: Predicted standard deviation, shape (batch_size, n_time_points, 1) + Batch prediction of trajectories, + this function is used for small test set, + dont input too much samples in one batch. """ + batch_size = u_batch.shape[0] n_time_points = len(time_points) # expand time points to batch dimension - # from (batch_size, n_sensors) and (n_time_points,) - # to (batch_size * n_time_points, n_sensors) and (batch_size * n_time_points, 1) - - # repeat u_batch to match each time point u_expanded = np.repeat( u_batch, n_time_points, axis=0 ) # (batch_size * n_time_points, n_sensors) - # create query points for each time point of each sample y_expanded = np.tile( time_points.reshape(-1, 1), (batch_size, 1) ) # (batch_size * n_time_points, 1) - # convert to MindSpore tensors u_tensor = ms.Tensor(u_expanded, ms.float32) y_tensor = ms.Tensor(y_expanded, ms.float32) - # one inference to get all predictions mean_pred, log_std_pred = model(u_tensor, y_tensor) - # reshape results mean_predictions = mean_pred.reshape(batch_size, n_time_points, 1) std_predictions = ms.ops.exp(log_std_pred).reshape( batch_size, n_time_points, 1) @@ -477,22 +415,13 @@ def batch_trajectory_prediction(model, u_batch, time_points): def load_and_preprocess_real_data(config: dict): - # Find training data data_path = config["data"]["data_path"] - # Load raw data u, y, s = load_real_data(data_path) - # Prepare data for DeepONet (convert multi-time-point to single query - # points) u_expanded, y_expanded, s_expanded, prep_metadata = prepare_deeponet_data( u, y, s) - # Normalize data - # logger.info("Normalizing data...") - # u_norm, y_norm, s_norm, scalers = normalize_data(u_expanded, y_expanded, s_expanded, method='standard') - - # Split data data_splits = split_data( u_expanded, y_expanded, @@ -502,11 +431,12 @@ def load_and_preprocess_real_data(config: dict): test_ratio=config["data"]["test_ratio"], ) - # Create MindSpore datasets datasets = create_datasets( - data_splits, batch_size=config["training"]["batch_size"]) + data_splits, + batch_size=config["training"]["batch_size"], + distributed=config["training"]["distributed"], + ) - # Prepare metadata metadata = { "input_dim": u_expanded.shape[-1], "output_dim": s_expanded.shape[-1], @@ -519,7 +449,6 @@ def load_and_preprocess_real_data(config: dict): }, } - # Add preparation metadata if prep_metadata: metadata.update(prep_metadata) diff --git a/MindEnergy/application/deeponet-grid/src/metrics.py b/MindEnergy/application/deeponet-grid/src/metrics.py index a65b57b49..d52ad63df 100644 --- a/MindEnergy/application/deeponet-grid/src/metrics.py +++ b/MindEnergy/application/deeponet-grid/src/metrics.py @@ -66,18 +66,7 @@ class MetricsCalculator: xi: float = 2.0, verbose: bool = False, ) -> float: - """Compute fraction of true trajectory in predicted confidence interval - - Args: - s: True values - s_mean: Predicted mean - s_std: Predicted standard deviation - xi: Confidence interval multiplier (default: 2.0 for 95% CI) - verbose: Whether to print results - - Returns: - Fraction of points within confidence interval - """ + """Compute fraction of true trajectory in predicted confidence interval""" # Reshape to 1D if needed s = s.reshape(-1) s_mean = s_mean.reshape(-1) @@ -96,16 +85,7 @@ class MetricsCalculator: def trajectory_rel_error( self, s_true: ms.Tensor, s_pred: ms.Tensor, verbose: bool = False ) -> Tuple[float, float]: - """Compute trajectory relative errors - - Args: - s_true: True trajectory - s_pred: Predicted trajectory - verbose: Whether to print results - - Returns: - Tuple of (L1_error, L2_error) - """ + """Compute trajectory relative errors""" s_true_flat = s_true.reshape(-1) s_pred_flat = s_pred.reshape(-1) @@ -125,17 +105,7 @@ def compute_metrics( metrics: List[str], verbose: bool = False, ) -> List[List[float]]: - """Compute metrics for multiple trajectories - - Args: - s_true: List of true trajectories - s_pred: List of predicted trajectories - metrics: List of metric names ('l1', 'l2') - verbose: Whether to print results - - Returns: - List of [max, min, mean] for each metric - """ + """Compute metrics for multiple trajectories""" calculator = MetricsCalculator() out = [] @@ -150,7 +120,6 @@ def compute_metrics( raise ValueError(f"Unsupported metric: {metric_name}") temp.append(error) - # Convert to numpy for statistics temp_np = np.array(temp) out.append( [ @@ -177,15 +146,7 @@ def compute_metrics( def update_metrics_history( history: Dict[str, List[float]], state: List[float] ) -> Dict[str, List[float]]: - """Update metrics history - - Args: - history: Current history dictionary - state: New state [max, min, mean] - - Returns: - Updated history - """ + """Update metrics history""" if "max" not in history: history["max"] = [] if "min" not in history: @@ -203,17 +164,7 @@ def update_metrics_history( def compute_calibration_error( s_true: ms.Tensor, s_mean: ms.Tensor, s_std: ms.Tensor, n_bins: int = 10 ) -> float: - """Compute calibration error for uncertainty quantification - - Args: - s_true: True values - s_mean: Predicted mean - s_std: Predicted standard deviation - n_bins: Number of bins for calibration - - Returns: - Calibration error - """ + """Compute calibration error for uncertainty quantification""" # Normalize residuals residuals = (s_true - s_mean) / s_std @@ -244,15 +195,7 @@ def compute_calibration_error( def compute_r2_score(y_true: ms.Tensor, y_pred: ms.Tensor) -> float: - """Compute R² score - - Args: - y_true: True values - y_pred: Predicted values - - Returns: - R² score - """ + """Compute R² score""" ss_res = ops.ReduceSum()((y_true - y_pred) ** 2) ss_tot = ops.ReduceSum()((y_true - ops.ReduceMean()(y_true)) ** 2) @@ -261,28 +204,12 @@ def compute_r2_score(y_true: ms.Tensor, y_pred: ms.Tensor) -> float: def compute_mae(y_true: ms.Tensor, y_pred: ms.Tensor) -> float: - """Compute Mean Absolute Error - - Args: - y_true: True values - y_pred: Predicted values - - Returns: - MAE value - """ + """Compute Mean Absolute Error""" mae = ops.ReduceMean()(ops.Abs()(y_true - y_pred)) return float(mae) def compute_mse(y_true: ms.Tensor, y_pred: ms.Tensor) -> float: - """Compute Mean Squared Error - - Args: - y_true: True values - y_pred: Predicted values - - Returns: - MSE value - """ + """Compute Mean Squared Error""" mse = ops.ReduceMean()((y_true - y_pred) ** 2) return float(mse) diff --git a/MindEnergy/application/deeponet-grid/src/model.py b/MindEnergy/application/deeponet-grid/src/model.py index 93cdcceb7..0823fcd21 100644 --- a/MindEnergy/application/deeponet-grid/src/model.py +++ b/MindEnergy/application/deeponet-grid/src/model.py @@ -20,7 +20,6 @@ from mindspore import Parameter, mint, nn, ops from mindspore.common.initializer import XavierNormal, Zero, initializer -# MLP class MLP(nn.Cell): def __init__(self, layer_size: list, activation: str) -> None: super(MLP, self).__init__() @@ -44,7 +43,6 @@ class MLP(nn.Cell): return self.net(x) -# modified MLP class modified_MLP(nn.Cell): def __init__(self, layer_size: list, activation: str) -> None: super(modified_MLP, self).__init__() @@ -66,6 +64,9 @@ class modified_MLP(nn.Cell): self.U.apply(self._init_weights) self.V.apply(self._init_weights) + self.len = ms.Tensor(len(layer_size) - 1, ms.int32) + self.ones = ops.OnesLike() + def _init_weights(self, m: Any) -> None: if isinstance(m, nn.Dense): m.weight.set_data( @@ -76,15 +77,14 @@ class modified_MLP(nn.Cell): def construct(self, x: ms.Tensor): u = self.activation(self.U(x)) v = self.activation(self.V(x)) - for k in range(len(self.net) - 1): + for k in range(self.len): y = self.net[k](x) y = self.activation(y) - x = y * u + (1 - y) * v + x = y * u + (self.ones(y) - y) * v y = self.net[-1](x) return y -# get activation function from str def get_activation(identifier: str) -> Any: """get activation function.""" return { @@ -95,7 +95,6 @@ def get_activation(identifier: str) -> Any: "leaky": nn.LeakyReLU(), "tanh": nn.Tanh(), "sin": sin_act(), - # "softplus": nn.Softplus(), "Rrelu": nn.RReLU(), "gelu": nn.GELU(), "silu": nn.SiLU(), @@ -103,7 +102,6 @@ def get_activation(identifier: str) -> Any: }[identifier] -# sin activation function class sin_act(nn.Cell): def __init__(self): super(sin_act, self).__init__() @@ -113,14 +111,11 @@ class sin_act(nn.Cell): class DeepONet(nn.Cell): - """ - Base DeepONet class that serves as an interface for different DeepONet implementations. - """ + """Base DeepONet class that serves as an interface for different DeepONet implementations.""" def __init__(self, branch: dict, trunk: dict, use_bias: bool = True) -> None: super(DeepONet, self).__init__() - # Branch if branch["type"] == "MLP": self.branch = MLP(branch["layer_size"][:-2], branch["activation"]) elif branch["type"] == "modified": @@ -130,7 +125,7 @@ class DeepONet(nn.Cell): raise ValueError( f"Unsupported branch type: {branch['type']}. Supported: 'MLP', 'modified'." ) - # Trunk + if trunk["type"] == "MLP": self.trunk = MLP(trunk["layer_size"][:-2], trunk["activation"]) elif trunk["type"] == "modified": @@ -152,16 +147,7 @@ class DeepONet(nn.Cell): m.bias.set_data(initializer(Zero(), m.bias.shape, m.bias.dtype)) def construct(self, xu, xy): - """ - Forward pass interface. To be implemented by subclasses. - - Args: - xu: Branch input tensor - xy: Trunk input tensor - - Returns: - Model output (to be defined by subclasses) - """ + """Forward pass interface. To be implemented by subclasses.""" raise NotImplementedError( "construct method must be implemented by subclasses") diff --git a/MindEnergy/application/deeponet-grid/src/trainer.py b/MindEnergy/application/deeponet-grid/src/trainer.py index 9b805893f..665dd79dc 100644 --- a/MindEnergy/application/deeponet-grid/src/trainer.py +++ b/MindEnergy/application/deeponet-grid/src/trainer.py @@ -23,11 +23,15 @@ import numpy as np from mindspore.dataset import GeneratorDataset from mindspore.train.callback import Callback -# Import metrics -from .metrics import (compute_calibration_error, compute_mae, compute_metrics, - compute_mse, compute_r2_score, MetricsCalculator) +from .metrics import ( + MetricsCalculator, + compute_calibration_error, + compute_mae, + compute_metrics, + compute_mse, + compute_r2_score, +) -# Set up logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -58,9 +62,7 @@ class CustomCallback(Callback): class ProbabilisticLoss(nn.Cell): - """Negative log likelihood loss for probabilistic DeepONet - Equivalent to torch.distributions.Normal(mean_pred, torch.exp(log_std_pred)).log_prob(target).mean() - """ + """Negative log likelihood loss for probabilistic DeepONet""" def __init__(self): super(ProbabilisticLoss, self).__init__() @@ -71,17 +73,7 @@ class ProbabilisticLoss(nn.Cell): self.log_2pi = np.log(2 * np.pi) def construct(self, mean_pred, log_std_pred, target): - """Compute negative log likelihood loss using normal distribution - - Args: - mean_pred: Predicted mean - log_std_pred: Predicted log standard deviation - target: Target values - - Returns: - Negative mean log probability - """ - # Convert log_std to std + """Compute negative log likelihood loss using normal distribution""" std_pred = self.exp(log_std_pred) # Compute log probability of normal distribution @@ -105,16 +97,7 @@ class MSELoss(nn.Cell): self.mse = nn.MSELoss() def construct(self, mean_pred, log_std_pred, target): - """Compute MSE loss (ignores uncertainty predictions) - - Args: - mean_pred: Predicted mean - log_std_pred: Predicted log standard deviation (ignored) - target: Target values - - Returns: - MSE loss value - """ + """Compute MSE loss (ignores uncertainty predictions)""" return self.mse(mean_pred, target) @@ -122,7 +105,11 @@ class DeepONetTrainer: """Trainer class for DeepONet with uncertainty quantification""" def __init__( - self, model: nn.Cell, config: Dict[str, Any], save_dir: str = "outputs" + self, + model: nn.Cell, + config: Dict[str, Any], + save_dir: str = "outputs", + distributed: int = 0, ): self.model = model @@ -137,8 +124,9 @@ class DeepONetTrainer: max_lr = float(self.training_config.get("max_lr", 1e-3)) total_step = int(self.training_config.get("total_step", 10000)) step_per_epoch = int( - self.training_config.get("step_per_epoch", 1000)) - decay_epoch = int(self.training_config.get("decay_epoch", 1000)) + self.training_config.get( + "step_per_epoch", 1000)) + decay_epoch = int(self.training_config.get("decay_epoch", 10)) learning_rate = nn.cosine_decay_lr( min_lr, max_lr, total_step, step_per_epoch, decay_epoch ) # Ensure it's a float @@ -157,8 +145,11 @@ class DeepONetTrainer: else: raise ValueError(f"Unsupported optimizer: {optimizer_type}") - self.grad_reducer = nn.DistributedGradReducer( - self.optimizer.parameters) + if distributed: + self.grad_reducer = nn.DistributedGradReducer( + self.optimizer.parameters) + else: + self.grad_reducer = nn.Identity() # Initialize loss function loss_type = self.training_config.get("loss_type", "nll") @@ -181,16 +172,7 @@ class DeepONetTrainer: def train_step( self, u: ms.Tensor, y: ms.Tensor, target: ms.Tensor ) -> Tuple[ms.Tensor, ms.Tensor, ms.Tensor]: - """Single training step - - Args: - u: Input function values - y: Evaluation points - target: Target values - - Returns: - Tuple of (loss, mean_pred, log_std_pred) - """ + """Single training step""" def forward_fn(): mean_pred, log_std_pred = self.model(u, y) @@ -208,14 +190,7 @@ class DeepONetTrainer: return loss, mean_pred, log_std_pred def validate(self, val_dataset: GeneratorDataset) -> float: - """Validate model on validation dataset - - Args: - val_dataset: Validation dataset - - Returns: - Average validation loss - """ + """Validate model on validation dataset""" self.model.set_train(False) total_loss = 0.0 num_batches = 0 @@ -234,15 +209,7 @@ class DeepONetTrainer: train_dataset: GeneratorDataset, val_dataset: Optional[GeneratorDataset] = None, ) -> Dict[str, List[float]]: - """Train the model - - Args: - train_dataset: Training dataset - val_dataset: Validation dataset (optional) - - Returns: - Training history - """ + """Train the model""" import time epochs = self.training_config["epochs"] @@ -253,20 +220,16 @@ class DeepONetTrainer: # Log model parameter count total_params = sum(p.size for p in self.model.get_parameters()) trainable_params = sum(p.size for p in self.model.trainable_params()) - logging.info(f"Total model parameters: {total_params}") - logging.info(f"Trainable parameters: {trainable_params}") + logging.debug(f"Total model parameters: {total_params}") + logging.debug(f"Trainable parameters: {trainable_params}") # Log training data size train_data_size = ( train_dataset.get_dataset_size() * train_dataset.get_batch_size() ) - logging.info( + logging.debug( f"Training data size (number of samples): {train_data_size}") - logging.info(f"Training batch size : {train_dataset.get_batch_size()}") - - # Print all parameter names, shapes, and sizes for debugging - logger.info("Model parameter details:") - for p in self.model.get_parameters(): - logger.info(f"{p.name}: shape={p.shape}, size={p.size}") + logging.debug( + f"Training batch size : {train_dataset.get_batch_size()}") if verbose: logger.info( @@ -314,12 +277,12 @@ class DeepONetTrainer: best["prob loss"] = val_loss self.save_model("best_model.ckpt") - # Compute average epoch loss try: avg_epoch_loss = epoch_loss / batch_count except ZeroDivisionError as e: logger.error( - f"error: {e}, batch size larger than number of training examples") + f"error: {e}, batch size larger than number of training examples" + ) continue logger_hist["prob loss"].append(avg_epoch_loss) # show after each epoch @@ -331,21 +294,13 @@ class DeepONetTrainer: return logger_hist def save_model(self, filename: str): - """Save model checkpoint - - Args: - filename: Name of the checkpoint file - """ + """Save model checkpoint""" save_path = os.path.join(self.save_dir, filename) ms.save_checkpoint(self.model, save_path) logger.info(f"Model saved to: {save_path}") def load_model(self, filename: str): - """Load model checkpoint - - Args: - filename: Name of the checkpoint file - """ + """Load model checkpoint""" load_path = os.path.join(self.save_dir, filename) if os.path.exists(load_path): ms.load_checkpoint(load_path, self.model) @@ -355,49 +310,30 @@ class DeepONetTrainer: def predict(self, u: ms.Tensor, y: ms.Tensor) -> Tuple[ms.Tensor, ms.Tensor]: - """Make predictions - - Args: - u: Input function values - y: Evaluation points - - Returns: - Tuple of (mean_pred, log_std_pred) - """ + """Make predictions""" self.model.set_train(False) mean_pred, log_std_pred = self.model(u, y) self.model.set_train(True) return mean_pred, log_std_pred def evaluate(self, test_dataset: GeneratorDataset) -> Dict[str, float]: - """Evaluate model on test dataset - - Args: - test_dataset: Test dataset - - Returns: - Dictionary of evaluation metrics - """ + """Evaluate model on test dataset""" self.model.set_train(False) - # Collect predictions and targets all_predictions = [] all_targets = [] all_means = [] all_stds = [] for u, y, target in test_dataset: - # Forward pass mean_pred, log_std_pred = self.model(u, y) std_pred = ops.Exp()(log_std_pred) - # Store results all_predictions.append(mean_pred) all_targets.append(target) all_means.append(mean_pred) all_stds.append(std_pred) - # Concatenate all batches if all_predictions: predictions = ops.Concat(axis=0)(all_predictions) targets = ops.Concat(axis=0)(all_targets) @@ -406,7 +342,6 @@ class DeepONetTrainer: else: return {} - # Compute basic metrics metrics = {} metrics["mse"] = compute_mse(targets, predictions) metrics["mae"] = compute_mae(targets, predictions) @@ -448,24 +383,13 @@ class DeepONetTrainer: std_predictions: List[ms.Tensor], verbose: bool = False, ) -> Dict[str, Any]: - """Compute metrics for trajectory predictions - - Args: - s_test: List of true solutions - mean_predictions: List of predicted means - std_predictions: List of predicted standard deviations - verbose: Whether to print results - - Returns: - Dictionary of trajectory metrics - """ + """Compute metrics for trajectory predictions""" # Compute L1 and L2 relative errors metrics_state = compute_metrics( s_test, mean_predictions, ["l1", "l2"], verbose=verbose ) # Compute fraction in confidence interval for each trajectory - calculator = MetricsCalculator() ci_fractions = [] @@ -486,7 +410,10 @@ class DeepONetTrainer: def create_trainer( - model: nn.Cell, config: Dict[str, Any], save_dir: str = "outputs" + model: nn.Cell, + config: Dict[str, Any], + save_dir: str = "outputs", + distributed: int = 0, ) -> DeepONetTrainer: """Create trainer instance""" - return DeepONetTrainer(model, config, save_dir) + return DeepONetTrainer(model, config, save_dir, distributed) diff --git a/MindEnergy/application/deeponet-grid/src/utils.py b/MindEnergy/application/deeponet-grid/src/utils.py index d9f0b821c..6d6e247b1 100644 --- a/MindEnergy/application/deeponet-grid/src/utils.py +++ b/MindEnergy/application/deeponet-grid/src/utils.py @@ -50,8 +50,6 @@ def parse_loss_log( return losses, epochs, steps # Regular expression to match the log format - # INFO:root:Epoch 1, Step 100, Batch 100, Loss: -0.524214, Step time: - # 0.040s pattern = r"INFO:root:Epoch (\d+), Step (\d+), Batch \d+, Loss: ([-\d.]+), Step time: \d+\.\d+s" with open(log_file_path, "r", encoding="utf-8") as f: @@ -93,8 +91,6 @@ def parse_val_loss_log( return val_losses, epochs, steps # Regular expression to match validation loss log format - # INFO:root:[Eval] Epoch 1, Step 100, Val-Loss: -0.705899 (negated for - # display) pattern = r"INFO:root:\[Eval\] Epoch (\d+), Step (\d+), Val-Loss: ([-\d.]+) \(negated for display\)" with open(log_file_path, "r", encoding="utf-8") as f: @@ -128,7 +124,6 @@ def plot_loss_curves( show_plot: Whether to display the plot figsize: Figure size (width, height) """ - # Parse loss data losses, epochs, steps = parse_loss_log(log_file_path) val_losses, val_epochs, val_steps = parse_val_loss_log(log_file_path) @@ -136,10 +131,8 @@ def plot_loss_curves( print("No loss data found in log file") return - # Create figure fig, (ax1, ax2) = plt.subplots(2, 1, figsize=figsize) - # Plot training loss ax1.plot( steps, losses, @@ -186,12 +179,10 @@ def plot_loss_curves( plt.tight_layout() - # Save plot if specified if save_path: plt.savefig(save_path, dpi=300, bbox_inches="tight") print(f"Loss curve saved to: {save_path}") - # Show plot if requested if show_plot: plt.show() @@ -367,14 +358,7 @@ def plot_training_history( history_file_path: str, save_path: Optional[str] = None, show_plot: bool = True) -> None: - """ - Plot training history from JSON file - - Args: - history_file_path: Path to the training history JSON file - save_path: Path to save the plot (optional) - show_plot: Whether to display the plot - """ + """Plot training history from JSON file""" history = load_training_history(history_file_path) if not history: @@ -442,7 +426,6 @@ def extract_log(log_file: str, history_file: str): plot_loss_statistics(log_file, save_path="loss_statistics.png") # Check for training history - # history_file = "outputs/training_history.json" if os.path.exists(history_file): print(f"Found training history: {history_file}") plot_training_history(history_file, save_path="training_history.png") diff --git a/MindEnergy/application/deeponet-grid/train.py b/MindEnergy/application/deeponet-grid/train.py index 26d672aa5..aa769bee2 100644 --- a/MindEnergy/application/deeponet-grid/train.py +++ b/MindEnergy/application/deeponet-grid/train.py @@ -26,104 +26,14 @@ import mindspore as ms import yaml from mindspore import context from mindspore.communication import init - from src.data import load_and_preprocess_real_data from src.model import Prob_DeepONet from src.trainer import create_trainer from src.utils import load_config -# Set up logging -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - handlers=[ - logging.FileHandler("training.log"), - logging.StreamHandler( - sys.stdout)], -) -logger = logging.getLogger(__name__) - -# ====== Data Parallel Init (for distributed training) ====== -ms.set_context(mode=ms.GRAPH_MODE, device_target="Ascend") -ms.set_auto_parallel_context( - parallel_mode=ms.ParallelMode.DATA_PARALLEL, gradients_mean=True) -init() -ms.set_seed(1) -# ====== End Data Parallel Init ====== -logger.info(f"Mode set to: {context.get_context('mode')}") -logger.info(f"Device set to: {context.get_context('device_target')}") - - -def create_model(config: dict, metadata: dict) -> Prob_DeepONet: - """Create DeepONet model based on configuration and data metadata - - Args: - config: Configuration dictionary - metadata: Data metadata containing input/output dimensions - - Returns: - DeepONet model instance - """ - model_config = config["model"] - - # Get network parameters from metadata if available, otherwise use config - # defaults - m = metadata.get( - "n_sensors", model_config.get( - "m", 33)) # Number of sensors - # Input dimension for trunk (always 1 for DeepONet) - dim = model_config.get("dim", 1) - width = model_config.get("width", 200) # Network width - # Network depth (number of hidden layers) - depth = model_config.get("depth", 3) - n_basis = model_config.get("n_basis", 100) # Number of basis functions - - # Get network types - branch_type = model_config.get("branch_type", "modified") - trunk_type = model_config.get("trunk_type", "modified") - activation = model_config.get("activation", "sin") - - # Compute layer sizes automatically (following original author's formula) - # [input] + [width] * depth + [output] - # depth = 3 means 3 hidden layers - branch_layer_size = [m] + [width] * depth + [n_basis] - trunk_layer_size = [dim] + [width] * depth + [n_basis] - - # Create branch configuration - branch_config = { - "type": branch_type, - "layer_size": branch_layer_size, - "activation": activation, - } - - # Create trunk configuration - trunk_config = { - "type": trunk_type, - "layer_size": trunk_layer_size, - "activation": activation, - } - - logger.info(f"Branch network: {branch_layer_size}") - logger.info(f"Trunk network: {trunk_layer_size}") - logger.info(f"Branch type: {branch_type}, Trunk type: {trunk_type}") - logger.info(f"Activation: {activation}") - logger.info(f"Depth: {depth} (number of hidden layers)") - logger.info(f"Input sensors (m): {m}") - logger.info(f"Trunk input dimension (dim): {dim}") - - # Create model - model = Prob_DeepONet( - branch=branch_config, - trunk=trunk_config, - use_bias=model_config["use_bias"]) - - logger.info("Model created successfully") - return model - - -def train(): - """Main training function""" +def parse_args(): + """Parse command line arguments""" parser = argparse.ArgumentParser( description="Train DeepONet-Grid-UQ model") parser.add_argument( @@ -144,9 +54,7 @@ def train(): default=None, help="Output directory (overrides config)", ) - parser.add_argument( - "--epochs", type=int, help="Number of epochs" - ) + parser.add_argument("--epochs", type=int, help="Number of epochs") parser.add_argument( "--batch_size", type=int, @@ -164,17 +72,35 @@ def train(): default=None, help="Resume training from checkpoint") parser.add_argument( - "--eval", - action="store_true", - help="Only run evaluation on test set") + "--eval", action="store_true", help="Only run evaluation on test set" + ) + parser.add_argument( + "--distributed", type=int, default=0, help="Distributed training" + ) + parser.add_argument( + "--pynative", + type=int, + default=1, + help="Pynative training") + + return parser.parse_args() + - args = parser.parse_args() +def train(args, logger): + """Main training function""" - # Load configuration - logger.info(f"Loading configuration from {args.config}") + logger.debug(f"Loading configuration from {args.config}") config = load_config(args.config) - # Override config with command line arguments + config["training"]["distributed"] = args.distributed + + if config["training"]["distributed"] == 0: + device_id = context.get_context(attr_key="device_id") + else: + device_id = int(os.environ.get("MS_NODE_ID")) + is_main_process = (True if device_id == + 0 or config["training"]["distributed"] == 0 else False) + if args.data_path: config["data"]["data_path"] = args.data_path config["data"]["use_synthetic"] = False @@ -182,10 +108,9 @@ def train(): if args.output_dir: config["output"]["save_dir"] = args.output_dir - # Create save directory os.makedirs(config["output"]["save_dir"], exist_ok=True) - logger.info(f"Save directory created: {config['output']['save_dir']}") - # Set epochs + logger.debug(f"Save directory created: {config['output']['save_dir']}") + if args.epochs: config["training"]["epochs"] = args.epochs @@ -195,88 +120,131 @@ def train(): if args.learning_rate: config["training"]["learning_rate"] = args.learning_rate - # Load and preprocess real data - logger.info("Loading and preprocessing real training data...") datasets, metadata = load_and_preprocess_real_data(config) - logger.info(f"Data loaded successfully:") - logger.info(f" Input dimension: {metadata['input_dim']}") - logger.info(f" Output dimension: {metadata['output_dim']}") - logger.info(f" Total samples: {metadata['n_samples']}") - logger.info(f" Number of sensors: {metadata['n_sensors']}") + logger.debug(f"Data loaded successfully:") + logger.debug(f" Input dimension: {metadata['input_dim']}") + logger.debug(f" Output dimension: {metadata['output_dim']}") + logger.debug(f" Total samples: {metadata['n_samples']}") + logger.debug(f" Number of sensors: {metadata['n_sensors']}") + + # create model + model_config = config["model"] + + m = metadata.get("n_sensors", model_config.get("m", 33)) + dim = model_config.get("dim", 1) + width = model_config.get("width", 200) + depth = model_config.get("depth", 3) + n_basis = model_config.get("n_basis", 100) + + branch_type = model_config.get("branch_type", "modified") + trunk_type = model_config.get("trunk_type", "modified") + activation = model_config.get("activation", "sin") - # Create model - logger.info("Creating model...") - model = create_model(config, metadata) + # Compute layer sizes automatically (following original author's formula) + # [input] + [width] * depth + [output] + # depth = 3 means 3 hidden layers + branch_layer_size = [m] + [width] * depth + [n_basis] + trunk_layer_size = [dim] + [width] * depth + [n_basis] + + branch_config = { + "type": branch_type, + "layer_size": branch_layer_size, + "activation": activation, + } + + trunk_config = { + "type": trunk_type, + "layer_size": trunk_layer_size, + "activation": activation, + } + + model = Prob_DeepONet( + branch=branch_config, + trunk=trunk_config, + use_bias=model_config["use_bias"]) - # Create trainer - logger.info("Creating trainer...") trainer = create_trainer( - model=model, config=config, save_dir=config["output"]["save_dir"] + model=model, + config=config, + save_dir=config["output"]["save_dir"], + distributed=config["training"]["distributed"], ) - # Load checkpoint if resuming if args.resume: - logger.info(f"Resuming from checkpoint: {args.resume}") + logger.debug(f"Resuming from checkpoint: {args.resume}") trainer.load_model(args.resume) if args.eval: - # Only run evaluation - logger.info("Running evaluation only...") - metrics = trainer.evaluate(datasets['test']) + logger.debug("Running evaluation only...") + metrics = trainer.evaluate(datasets["test"]) - # Save evaluation results - import json results_path = os.path.join( - config['output']['save_dir'], 'test_results.json') - with open(results_path, 'w') as f: + config["output"]["save_dir"], + "test_results.json") + with open(results_path, "w") as f: json.dump(metrics, f, indent=2) - logger.info(f"Test results saved to: {results_path}") + logger.debug(f"Test results saved to: {results_path}") return - # print config before training - logger.info(f"Training config: {config}") + logger.debug(f"Training config: {config}") - # Train model - logger.info( - f"Starting training for {config['training']['epochs']} epochs...") history = trainer.train( train_dataset=datasets["train"], val_dataset=datasets["val"] ) - # Save training history - import json - history_path = os.path.join( config["output"]["save_dir"], "training_history.json") with open(history_path, "w") as f: json.dump(history, f, indent=2) - logger.info(f"Training history saved to: {history_path}") + logger.debug(f"Training history saved to: {history_path}") - # Evaluate on test set - logger.info("Evaluating on test set...") + logger.debug("Evaluating on test set...") test_metrics = trainer.evaluate(datasets["test"]) - # Save test results test_results_path = os.path.join( config["output"]["save_dir"], "test_results.json") with open(test_results_path, "w") as f: json.dump(test_metrics, f, indent=2) - logger.info(f"Test results saved to: {test_results_path}") + logger.debug(f"Test results saved to: {test_results_path}") - # Save final model trainer.save_model("final_model.ckpt") + logger.debug( + "Training completed successfully, model saved to final_model.ckpt") - logger.info("Training completed successfully!") - - # Print final results - logger.info("Final Results:") + logger.debug("Final Results:") for metric, value in test_metrics.items(): - logger.info(f" {metric.upper()}: {value:.6f}") + logger.debug(f" {metric.upper()}: {value:.6f}") if __name__ == "__main__": - train() + args = parse_args() + + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[ + logging.FileHandler("training.log"), + logging.StreamHandler(sys.stdout), + ], + ) + logger = logging.getLogger(__name__) + + # Data Parallel Init for distributed training + ms.set_context( + mode=ms.PYNATIVE_MODE if args.pynative else ms.GRAPH_MODE, + device_target="Ascend", + ) + if args.distributed: + ms.set_auto_parallel_context( + parallel_mode=ms.ParallelMode.DATA_PARALLEL, gradients_mean=True + ) + init() + ms.set_seed(1) + + logger.debug(f"Mode set to: {context.get_context('mode')}") + logger.debug(f"Device set to: {context.get_context('device_target')}") + train(args, logger) -- Gitee From 2876d26b75e4b2e48cf95c614e14fef85d6b2a91 Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Tue, 12 Aug 2025 15:20:30 +0800 Subject: [PATCH 23/25] Fix comments. --- .../application/deeponet-grid/inference.py | 82 +++++++------- MindEnergy/application/deeponet-grid/train.py | 105 +++++++++--------- 2 files changed, 89 insertions(+), 98 deletions(-) diff --git a/MindEnergy/application/deeponet-grid/inference.py b/MindEnergy/application/deeponet-grid/inference.py index c849d44db..76c36dfd3 100644 --- a/MindEnergy/application/deeponet-grid/inference.py +++ b/MindEnergy/application/deeponet-grid/inference.py @@ -40,48 +40,6 @@ from src.model import Prob_DeepONet from src.utils import load_config, load_trained_model -def parse_args(): - """Parse command line arguments""" - parser = argparse.ArgumentParser(description="DeepONet-Grid-UQ Inference") - parser.add_argument( - "--config", - type=str, - default="configs/config.yaml", - help="Path to configuration file", - ) - parser.add_argument( - "--checkpoint", - type=str, - default="outputs/best_model.ckpt", - help="Path to model checkpoint", - ) - parser.add_argument( - "--data_path", - type=str, - default="data/test-data-voltage-m-33-mix.npz", - help="Path to data file for dataset inference", - ) - parser.add_argument( - "--output_dir", - type=str, - default="inference_results", - help="Output directory for results", - ) - parser.add_argument( - "--trajectory_prediction", - action="store_true", - help="Perform single data point (with multiple time points) inference", - ) - parser.add_argument( - "--data_index", - type=int, - default=0, - help="Index of data point for single inference (default: 0)", - ) - - return parser.parse_args() - - def batch_inference( model: Prob_DeepONet, u: np.ndarray, y: np.ndarray ) -> Tuple[np.ndarray, np.ndarray]: @@ -256,7 +214,45 @@ def inference(args, logger): if __name__ == "__main__": - args = parse_args() + """Parse command line arguments""" + parser = argparse.ArgumentParser(description="DeepONet-Grid-UQ Inference") + parser.add_argument( + "--config", + type=str, + default="configs/config.yaml", + help="Path to configuration file", + ) + parser.add_argument( + "--checkpoint", + type=str, + default="outputs/best_model.ckpt", + help="Path to model checkpoint", + ) + parser.add_argument( + "--data_path", + type=str, + default="data/test-data-voltage-m-33-mix.npz", + help="Path to data file for dataset inference", + ) + parser.add_argument( + "--output_dir", + type=str, + default="inference_results", + help="Output directory for results", + ) + parser.add_argument( + "--trajectory_prediction", + action="store_true", + help="Perform single data point (with multiple time points) inference", + ) + parser.add_argument( + "--data_index", + type=int, + default=0, + help="Index of data point for single inference (default: 0)", + ) + + args = parser.parse_args() logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", diff --git a/MindEnergy/application/deeponet-grid/train.py b/MindEnergy/application/deeponet-grid/train.py index aa769bee2..e37456312 100644 --- a/MindEnergy/application/deeponet-grid/train.py +++ b/MindEnergy/application/deeponet-grid/train.py @@ -32,60 +32,6 @@ from src.trainer import create_trainer from src.utils import load_config -def parse_args(): - """Parse command line arguments""" - parser = argparse.ArgumentParser( - description="Train DeepONet-Grid-UQ model") - parser.add_argument( - "--config", - type=str, - default="configs/config.yaml", - help="Path to configuration file", - ) - parser.add_argument( - "--data_path", - type=str, - default=None, - help="Path to data file (overrides config)", - ) - parser.add_argument( - "--output_dir", - type=str, - default=None, - help="Output directory (overrides config)", - ) - parser.add_argument("--epochs", type=int, help="Number of epochs") - parser.add_argument( - "--batch_size", - type=int, - default=None, - help="Batch size (overrides config)") - parser.add_argument( - "--learning_rate", - type=float, - default=None, - help="Learning rate (overrides config)", - ) - parser.add_argument( - "--resume", - type=str, - default=None, - help="Resume training from checkpoint") - parser.add_argument( - "--eval", action="store_true", help="Only run evaluation on test set" - ) - parser.add_argument( - "--distributed", type=int, default=0, help="Distributed training" - ) - parser.add_argument( - "--pynative", - type=int, - default=1, - help="Pynative training") - - return parser.parse_args() - - def train(args, logger): """Main training function""" @@ -221,7 +167,56 @@ def train(args, logger): if __name__ == "__main__": - args = parse_args() + parser = argparse.ArgumentParser( + description="Train DeepONet-Grid-UQ model") + parser.add_argument( + "--config", + type=str, + default="configs/config.yaml", + help="Path to configuration file", + ) + parser.add_argument( + "--data_path", + type=str, + default=None, + help="Path to data file (overrides config)", + ) + parser.add_argument( + "--output_dir", + type=str, + default=None, + help="Output directory (overrides config)", + ) + parser.add_argument("--epochs", type=int, help="Number of epochs") + parser.add_argument( + "--batch_size", + type=int, + default=None, + help="Batch size (overrides config)") + parser.add_argument( + "--learning_rate", + type=float, + default=None, + help="Learning rate (overrides config)", + ) + parser.add_argument( + "--resume", + type=str, + default=None, + help="Resume training from checkpoint") + parser.add_argument( + "--eval", action="store_true", help="Only run evaluation on test set" + ) + parser.add_argument( + "--distributed", type=int, default=0, help="Distributed training" + ) + parser.add_argument( + "--pynative", + type=int, + default=1, + help="Pynative training") + + args = parser.parse_args() logging.basicConfig( level=logging.INFO, -- Gitee From f13c6d518c786e82086e12870207ca02777fa153 Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Tue, 12 Aug 2025 15:44:49 +0800 Subject: [PATCH 24/25] Remove logging. --- .../application/deeponet-grid/inference.py | 18 -------------- .../application/deeponet-grid/src/trainer.py | 11 +++------ MindEnergy/application/deeponet-grid/train.py | 24 +++---------------- 3 files changed, 6 insertions(+), 47 deletions(-) diff --git a/MindEnergy/application/deeponet-grid/inference.py b/MindEnergy/application/deeponet-grid/inference.py index 76c36dfd3..c752d555a 100644 --- a/MindEnergy/application/deeponet-grid/inference.py +++ b/MindEnergy/application/deeponet-grid/inference.py @@ -124,10 +124,8 @@ def inference_on_dataset( def inference(args, logger): """Main inference function""" - logger.debug(f"Loading configuration from {args.config}") config = load_config(args.config) - logger.debug("Creating model...") model_config = config["model"] m = model_config.get("m", 33) @@ -160,29 +158,20 @@ def inference(args, logger): trunk=trunk_config, use_bias=model_config["use_bias"]) - logger.debug("Model created successfully") - if args.trajectory_prediction: # Test single sample trajectory prediction model = load_trained_model(model, args.checkpoint) - logger.debug(f"Model loaded from: {args.checkpoint}") if args.data_path is None: logger.error("For single inference, --data_path must be provided") return - logger.debug(f"Loading dataset from: {args.data_path}") u, y, s = load_real_data(args.data_path) u_single = u[args.data_index] y_single = y[args.data_index] predictions = trajectory_prediction(u_single, y_single, model) - logger.debug( - f"Single sample prediction shape: {predictions.shape} on data index {args.data_index}" - ) - logger.debug(f"Predictions: {predictions.flatten()}") - logger.debug(f"Target: {s[args.data_index].flatten()}") calculator = MetricsCalculator() y_true = ms.Tensor(s[args.data_index], ms.float32) @@ -190,12 +179,9 @@ def inference(args, logger): # Test L1 and L2 relative errors l1_error = calculator.l1_relative_error(y_true, y_pred) l2_error = calculator.l2_relative_error(y_true, y_pred) - logger.debug(f" L1 relative error: {l1_error:.6f}") - logger.debug(f" L2 relative error: {l2_error:.6f}") # Test trajectory relative error l1_traj, l2_traj = calculator.trajectory_rel_error(y_true, y_pred) - logger.debug(f" Trajectory L1: {l1_traj:.6f}, L2: {l2_traj:.6f}") else: # Dataset inference @@ -210,8 +196,6 @@ def inference(args, logger): args.checkpoint, args.output_dir) - logger.debug("Dataset inference completed successfully!") - if __name__ == "__main__": """Parse command line arguments""" @@ -264,7 +248,5 @@ if __name__ == "__main__": logger = logging.getLogger(__name__) context.set_context(mode=context.PYNATIVE_MODE, device_target="Ascend") - logger.debug(f"Mode set to: {context.get_context('mode')}") - logger.debug(f"Device set to: {context.get_context('device_target')}") inference(args, logger) diff --git a/MindEnergy/application/deeponet-grid/src/trainer.py b/MindEnergy/application/deeponet-grid/src/trainer.py index 665dd79dc..c85796817 100644 --- a/MindEnergy/application/deeponet-grid/src/trainer.py +++ b/MindEnergy/application/deeponet-grid/src/trainer.py @@ -220,16 +220,11 @@ class DeepONetTrainer: # Log model parameter count total_params = sum(p.size for p in self.model.get_parameters()) trainable_params = sum(p.size for p in self.model.trainable_params()) - logging.debug(f"Total model parameters: {total_params}") - logging.debug(f"Trainable parameters: {trainable_params}") + # Log training data size train_data_size = ( train_dataset.get_dataset_size() * train_dataset.get_batch_size() ) - logging.debug( - f"Training data size (number of samples): {train_data_size}") - logging.debug( - f"Training batch size : {train_dataset.get_batch_size()}") if verbose: logger.info( @@ -264,14 +259,14 @@ class DeepONetTrainer: f"Epoch {epoch+1}, Step {global_step}, Batch {batch_count}, " f"Loss: {float(loss):.6f}, " f"Step time: {step_time:.3f}s") - logging.info(msg) + logger.info(msg) # evaluate if val_dataset is not None and global_step % eval_every == 0: val_loss = self.validate(val_dataset) logger_hist["val loss"].append((global_step, val_loss)) # Negative log likelihood loss can be negative msg = f"[Eval] Epoch {epoch+1}, Step {global_step}, Val-Loss: {float(val_loss):.6f} " - logging.info(msg) + logger.info(msg) # save best ckpt if val_loss < best["prob loss"]: best["prob loss"] = val_loss diff --git a/MindEnergy/application/deeponet-grid/train.py b/MindEnergy/application/deeponet-grid/train.py index e37456312..3d7e08268 100644 --- a/MindEnergy/application/deeponet-grid/train.py +++ b/MindEnergy/application/deeponet-grid/train.py @@ -35,7 +35,6 @@ from src.utils import load_config def train(args, logger): """Main training function""" - logger.debug(f"Loading configuration from {args.config}") config = load_config(args.config) config["training"]["distributed"] = args.distributed @@ -55,7 +54,6 @@ def train(args, logger): config["output"]["save_dir"] = args.output_dir os.makedirs(config["output"]["save_dir"], exist_ok=True) - logger.debug(f"Save directory created: {config['output']['save_dir']}") if args.epochs: config["training"]["epochs"] = args.epochs @@ -68,12 +66,6 @@ def train(args, logger): datasets, metadata = load_and_preprocess_real_data(config) - logger.debug(f"Data loaded successfully:") - logger.debug(f" Input dimension: {metadata['input_dim']}") - logger.debug(f" Output dimension: {metadata['output_dim']}") - logger.debug(f" Total samples: {metadata['n_samples']}") - logger.debug(f" Number of sensors: {metadata['n_sensors']}") - # create model model_config = config["model"] @@ -118,11 +110,9 @@ def train(args, logger): ) if args.resume: - logger.debug(f"Resuming from checkpoint: {args.resume}") trainer.load_model(args.resume) if args.eval: - logger.debug("Running evaluation only...") metrics = trainer.evaluate(datasets["test"]) results_path = os.path.join( @@ -130,12 +120,9 @@ def train(args, logger): "test_results.json") with open(results_path, "w") as f: json.dump(metrics, f, indent=2) - logger.debug(f"Test results saved to: {results_path}") return - logger.debug(f"Training config: {config}") - history = trainer.train( train_dataset=datasets["train"], val_dataset=datasets["val"] ) @@ -145,9 +132,7 @@ def train(args, logger): "training_history.json") with open(history_path, "w") as f: json.dump(history, f, indent=2) - logger.debug(f"Training history saved to: {history_path}") - logger.debug("Evaluating on test set...") test_metrics = trainer.evaluate(datasets["test"]) test_results_path = os.path.join( @@ -155,15 +140,14 @@ def train(args, logger): "test_results.json") with open(test_results_path, "w") as f: json.dump(test_metrics, f, indent=2) - logger.debug(f"Test results saved to: {test_results_path}") trainer.save_model("final_model.ckpt") - logger.debug( + logger.info( "Training completed successfully, model saved to final_model.ckpt") - logger.debug("Final Results:") + logger.info("Final Results:") for metric, value in test_metrics.items(): - logger.debug(f" {metric.upper()}: {value:.6f}") + logger.info(f" {metric.upper()}: {value:.6f}") if __name__ == "__main__": @@ -240,6 +224,4 @@ if __name__ == "__main__": init() ms.set_seed(1) - logger.debug(f"Mode set to: {context.get_context('mode')}") - logger.debug(f"Device set to: {context.get_context('device_target')}") train(args, logger) -- Gitee From b48c71493345900abc9cc0e279bc590ffc319f73 Mon Sep 17 00:00:00 2001 From: WANG Cong Date: Tue, 12 Aug 2025 16:17:42 +0800 Subject: [PATCH 25/25] Fix. --- MindEnergy/application/deeponet-grid/inference.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MindEnergy/application/deeponet-grid/inference.py b/MindEnergy/application/deeponet-grid/inference.py index c752d555a..2580e65e3 100644 --- a/MindEnergy/application/deeponet-grid/inference.py +++ b/MindEnergy/application/deeponet-grid/inference.py @@ -76,7 +76,10 @@ def inference_on_dataset( "val_ratio": 0.0, "test_ratio": 1.0, }, - "training": {"batch_size": 1024}, + "training": { + "batch_size": 1024, + "distributed": 0, + }, } ) -- Gitee