OneScience commited on
Commit
8880eca
·
verified ·
1 Parent(s): 68693c0

Upload folder using huggingface_hub

Browse files
README.md ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ tags:
6
+ - OneScience
7
+ - fluid dynamics
8
+ - steady-state laminar flow prediction
9
+ - CFD surrogate modeling
10
+ frameworks: PyTorch
11
+ ---
12
+ <p align="center">
13
+ <strong>
14
+ <span style="font-size: 30px;">DeepCFD</span>
15
+ </strong>
16
+ </p>
17
+
18
+ # Model Overview
19
+ DeepCFD is a U-Net-based surrogate model developed by the German Aerospace Center for two-dimensional steady-state laminar flows. It uses geometric information to rapidly predict velocity and pressure fields, accelerating the evaluation of channel flows, flows around obstacles, and aerodynamic designs.
20
+
21
+ Paper: [DeepCFD: Efficient Steady-State Laminar Flow
22
+ Approximation with Deep Convolutional Neural Networks](https://arxiv.org/abs/2004.08826).
23
+
24
+ # Model Description
25
+ DeepCFD uses a U-Net deep convolutional architecture that takes a signed distance field (SDF) and flow-region mask as inputs to rapidly predict velocity and pressure fields for two-dimensional, nonuniform, steady-state laminar flows.
26
+
27
+ ## Use Cases
28
+
29
+ | Use Case | Description |
30
+ | ---------- | ---------------------------------- |
31
+ | Steady-state laminar flow prediction | Approximate velocity and pressure fields in two-dimensional, nonuniform, steady-state laminar flows |
32
+ | Channel-flow simulation | Predict flow distributions around randomly shaped obstacles in a channel |
33
+ | CFD surrogate modeling | Replace conventional CFD workflows such as OpenFOAM to improve inference efficiency |
34
+ | Aerodynamic and fluid-shape optimization | Evaluate large numbers of candidate geometries for low-speed laminar-flow design |
35
+
36
+
37
+ # Usage
38
+
39
+ ## 1. OneCode
40
+
41
+ Use the online OneCode environment for an intelligent, one-click AI for Science (AI4S) programming experience:
42
+
43
+ [Launch OneCode for one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
44
+
45
+ ## 2. Manual Setup
46
+
47
+ **Hardware Requirements**
48
+
49
+ - A GPU or DCU is recommended.
50
+ - A CPU can be used for import checks and small-scale pipeline validation, but full training and inference will be slow.
51
+ - DCU users must install DTK in advance. DTK 25.04.2 or later, or the OneScience-recommended version for the target cluster, is recommended.
52
+
53
+
54
+
55
+ ### Download the Model Package
56
+
57
+ ```bash
58
+ modelscope download --model OneScience/DeepCFD --local_dir ./DeepCFD
59
+ cd DeepCFD
60
+ ```
61
+
62
+ ### Set Up the Runtime Environment
63
+
64
+
65
+ **DCU Environment**
66
+
67
+ ```bash
68
+ # Activate DTK and Conda first
69
+ conda create -n onescience311 python=3.11 -y
70
+ conda activate onescience311
71
+ # Installation with uv is also supported
72
+ pip install onescience[cfd-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
73
+ ```
74
+
75
+ **GPU Environment**
76
+ ```bash
77
+ # Activate Conda first
78
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
79
+ conda activate onescience311
80
+ # Installation with uv is also supported
81
+ pip install onescience[cfd-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
82
+ ```
83
+
84
+ ### Training Data
85
+ The OneScience community provides the `deepcfd` dataset for training. Download it with the command below and verify that the data path in `conf/config.yaml` is configured correctly:
86
+
87
+ ```bash
88
+ modelscope download --dataset OneScience/deepcfd --local_dir ./data
89
+ ```
90
+
91
+ The project dataset can also be [downloaded from Zenodo](https://zenodo.org/record/3666056/files/DeepCFD.zip?download=1).
92
+
93
+ The dataset contains two files:
94
+ - `dataX.pkl`: Geometric input data for 981 channel-flow samples
95
+ - `dataY.pkl`: Ground-truth CFD solutions for the corresponding samples, computed with the `simpleFOAM` solver
96
+
97
+ ### Training
98
+
99
+ Single GPU:
100
+
101
+ - Training parameters are read from the `root.datapipe`, `root.model`, and `root.training` sections of `config/config.yaml`. The default configuration is intended for a minimal smoke test.
102
+ - For full training, set `root.datapipe.source.data_dir` to the actual DeepCFD dataset directory and increase the model size, batch size, and number of epochs as needed.
103
+
104
+ ```bash
105
+ python scripts/train.py
106
+ ```
107
+ Multiple GPUs:
108
+
109
+ ```bash
110
+ torchrun --standalone --nproc_per_node=<num_GPUs> scripts/train.py
111
+ ```
112
+ By default, training saves the best checkpoint to:
113
+
114
+ ```text
115
+ ./weight/best_model.pt
116
+ ```
117
+
118
+ ### Model Weights
119
+
120
+ This repository will provide pretrained DeepCFD weights in the `weights/` directory. The weights will be uploaded soon.
121
+
122
+ ### Inference
123
+
124
+ ```bash
125
+ python scripts/inference.py
126
+ ```
127
+
128
+ ### Evaluation and Visualization
129
+
130
+ ```bash
131
+ python scripts/result.py
132
+ ```
133
+
134
+
135
+ # Official OneScience Resources
136
+
137
+ | Platform | OneScience Repository | Skills Repository |
138
+ | --- | --- | --- |
139
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
140
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
141
+
142
+ # Citations and License
143
+
144
+ - Original DeepCFD paper: [DeepCFD: Efficient Steady-State Laminar Flow Approximation with Deep Convolutional Neural Networks](https://arxiv.org/abs/2004.08826)
145
+ - This repository retains source attribution and has been adapted for automated execution through OneScience and ModelScope.
config/config.yaml ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ root:
2
+ datapipe:
3
+ verbose: false
4
+ source:
5
+ data_dir: "./data/deepcfd"
6
+ data_x_name: "dataX.pkl"
7
+ data_y_name: "dataY.pkl"
8
+
9
+ data:
10
+ split_ratio: 0.75
11
+ seed: 0
12
+
13
+ dataloader:
14
+ batch_size: 1
15
+ num_workers: 0
16
+
17
+ model:
18
+ name: "UNetEx"
19
+ in_channels: 3
20
+ out_channels: 3
21
+ base_channels: 2
22
+ num_stages: 1
23
+ kernel_size: 3
24
+ bilinear: true
25
+ normtype: "none"
26
+
27
+ training:
28
+ output_dir: "./weight"
29
+ checkpoint_name: "best_model.pt"
30
+ num_epochs: 1
31
+ lr: 0.001
32
+ weight_decay: 0.005
33
+ patience: 20
34
+ eval_interval: 1
35
+
36
+ inference:
37
+ checkpoint_path: "./weight/best_model.pt"
38
+ result_dir: "./result/deepcfd"
39
+ num_visualize: 2
40
+
41
+ fake_data:
42
+ num_samples: 4
43
+ height: 16
44
+ width: 16
45
+ seed: 42
configuration.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"framework":"Pytorch","task":"other"}
model/__init__.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .unet import UNet
2
+ from .unetex import UNetEx
3
+
4
+
5
+ def build_model(config):
6
+ name = config["name"] if isinstance(config, dict) else config.name
7
+ model_map = {
8
+ "UNet": UNet,
9
+ "UNetEx": UNetEx,
10
+ }
11
+ if name not in model_map:
12
+ raise ValueError(f"Unknown DeepCFD model: {name}")
13
+
14
+ get = config.get if isinstance(config, dict) else lambda key, default=None: getattr(config, key, default)
15
+ return model_map[name](
16
+ in_channels=get("in_channels"),
17
+ out_channels=get("out_channels"),
18
+ base_channels=get("base_channels", 16),
19
+ num_stages=get("num_stages", 2),
20
+ bilinear=get("bilinear", True),
21
+ normtype=get("normtype", "bn"),
22
+ kernel_size=get("kernel_size", 3),
23
+ )
model/unet.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch.nn as nn
2
+
3
+ from onescience.modules.decoder.unet_decoder import UNetDecoder2D
4
+ from onescience.modules.encoder.unet_encoder import UNetEncoder2D
5
+ from onescience.modules.head.unet_head import UNetHead2D
6
+
7
+
8
+ class UNet(nn.Module):
9
+ def __init__(
10
+ self,
11
+ in_channels: int,
12
+ out_channels: int,
13
+ base_channels: int = 16,
14
+ num_stages: int = 2,
15
+ bilinear: bool = True,
16
+ normtype: str = "bn",
17
+ kernel_size: int = 3,
18
+ ):
19
+ super().__init__()
20
+ self.encoder = UNetEncoder2D(
21
+ in_channels=in_channels,
22
+ base_channels=base_channels,
23
+ num_stages=num_stages,
24
+ bilinear=bilinear,
25
+ normtype=normtype,
26
+ kernel_size=kernel_size,
27
+ )
28
+ self.decoder = UNetDecoder2D(
29
+ base_channels=base_channels,
30
+ num_stages=num_stages,
31
+ bilinear=bilinear,
32
+ normtype=normtype,
33
+ kernel_size=kernel_size,
34
+ )
35
+ self.head = UNetHead2D(
36
+ in_channels=base_channels,
37
+ out_channels=out_channels,
38
+ kernel_size=1,
39
+ )
40
+
41
+ def forward(self, x):
42
+ features = self.encoder(x)
43
+ decoded = self.decoder(features)
44
+ return self.head(decoded)
model/unetex.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+ from onescience.modules.decoder.unet_decoder import UNetDecoder2D
5
+ from onescience.modules.encoder.unet_encoder import UNetEncoder2D
6
+ from onescience.modules.head.unet_head import UNetHead2D
7
+
8
+
9
+ class DecoderPath(nn.Module):
10
+ def __init__(self, base_channels, num_stages, bilinear, normtype, kernel_size):
11
+ super().__init__()
12
+ self.decoder = UNetDecoder2D(
13
+ base_channels=base_channels,
14
+ num_stages=num_stages,
15
+ bilinear=bilinear,
16
+ normtype=normtype,
17
+ kernel_size=kernel_size,
18
+ )
19
+ self.head = UNetHead2D(
20
+ in_channels=base_channels,
21
+ out_channels=1,
22
+ kernel_size=1,
23
+ )
24
+
25
+ def forward(self, features):
26
+ return self.head(self.decoder(features))
27
+
28
+
29
+ class UNetEx(nn.Module):
30
+ def __init__(
31
+ self,
32
+ in_channels: int,
33
+ out_channels: int,
34
+ base_channels: int = 16,
35
+ num_stages: int = 2,
36
+ bilinear: bool = True,
37
+ normtype: str = "bn",
38
+ kernel_size: int = 3,
39
+ ):
40
+ super().__init__()
41
+ self.encoder = UNetEncoder2D(
42
+ in_channels=in_channels,
43
+ base_channels=base_channels,
44
+ num_stages=num_stages,
45
+ bilinear=bilinear,
46
+ normtype=normtype,
47
+ kernel_size=kernel_size,
48
+ )
49
+ self.decoders = nn.ModuleList(
50
+ [
51
+ DecoderPath(base_channels, num_stages, bilinear, normtype, kernel_size)
52
+ for _ in range(out_channels)
53
+ ]
54
+ )
55
+
56
+ def forward(self, x):
57
+ features = self.encoder(x)
58
+ outputs = [decoder_path(features) for decoder_path in self.decoders]
59
+ return torch.cat(outputs, dim=1)
scripts/fake_data.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pickle
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ import numpy as np
6
+
7
+
8
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
9
+ sys.path.insert(0, str(PROJECT_ROOT))
10
+
11
+ from onescience.utils.YParams import YParams
12
+
13
+
14
+ def resolve_path(path_value):
15
+ path = Path(path_value)
16
+ return path if path.is_absolute() else PROJECT_ROOT / path
17
+
18
+
19
+ def make_fields(num_samples, height, width, seed):
20
+ rng = np.random.default_rng(seed)
21
+ y_axis = np.linspace(-1.0, 1.0, height, dtype=np.float32)
22
+ x_axis = np.linspace(-1.0, 1.0, width, dtype=np.float32)
23
+ yy, xx = np.meshgrid(y_axis, x_axis, indexing="ij")
24
+
25
+ x = np.empty((num_samples, 3, height, width), dtype=np.float32)
26
+ target = np.empty_like(x)
27
+
28
+ for i in range(num_samples):
29
+ cx = 0.2 * np.sin(i)
30
+ cy = 0.2 * np.cos(i * 0.7)
31
+ radius = 0.25 + 0.03 * (i % 3)
32
+ distance = np.sqrt((xx - cx) ** 2 + (yy - cy) ** 2) - radius
33
+ obstacle = (distance < 0.0).astype(np.float32)
34
+ inlet = np.ones_like(xx, dtype=np.float32) * (1.0 + 0.05 * i)
35
+
36
+ x[i, 0] = obstacle
37
+ x[i, 1] = distance
38
+ x[i, 2] = inlet
39
+
40
+ ux = inlet * (1.0 - obstacle) + 0.1 * np.sin(np.pi * yy)
41
+ uy = -0.2 * yy * np.exp(-4.0 * np.maximum(distance, 0.0))
42
+ pressure = 0.5 * (1.0 - xx) + 0.1 * obstacle
43
+
44
+ noise = 0.005 * rng.standard_normal((3, height, width)).astype(np.float32)
45
+ target[i, 0] = ux
46
+ target[i, 1] = uy
47
+ target[i, 2] = pressure
48
+ target[i] += noise
49
+
50
+ return x, target
51
+
52
+
53
+ def main():
54
+ cfg = YParams(str(PROJECT_ROOT / "config" / "config.yaml"), "root")
55
+ data_dir = resolve_path(cfg.datapipe.source.data_dir)
56
+ data_dir.mkdir(parents=True, exist_ok=True)
57
+
58
+ x, y = make_fields(
59
+ num_samples=cfg.fake_data.num_samples,
60
+ height=cfg.fake_data.height,
61
+ width=cfg.fake_data.width,
62
+ seed=cfg.fake_data.seed,
63
+ )
64
+
65
+ with open(data_dir / cfg.datapipe.source.data_x_name, "wb") as f:
66
+ pickle.dump(x, f)
67
+ with open(data_dir / cfg.datapipe.source.data_y_name, "wb") as f:
68
+ pickle.dump(y, f)
69
+
70
+ print(f"Fake DeepCFD data written to {data_dir}")
71
+ print(f"x shape={x.shape}, y shape={y.shape}, dtype={x.dtype}")
72
+
73
+
74
+ if __name__ == "__main__":
75
+ main()
scripts/inference.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import importlib.util
4
+ from pathlib import Path
5
+
6
+ os.environ.setdefault("MPLBACKEND", "Agg")
7
+
8
+ import numpy as np
9
+ import torch
10
+
11
+
12
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
13
+ sys.path.insert(0, str(PROJECT_ROOT))
14
+
15
+ from model import build_model
16
+ from onescience.distributed.manager import DistributedManager
17
+ from onescience.utils.YParams import YParams
18
+ from onescience.utils.deepcfd.functions import visualize
19
+ import onescience
20
+
21
+
22
+ def resolve_path(path_value):
23
+ path = Path(path_value)
24
+ return path if path.is_absolute() else PROJECT_ROOT / path
25
+
26
+
27
+ def load_config():
28
+ cfg = YParams(str(PROJECT_ROOT / "config" / "config.yaml"), "root")
29
+ cfg.datapipe.source.data_dir = str(resolve_path(cfg.datapipe.source.data_dir))
30
+ cfg.inference.checkpoint_path = str(resolve_path(cfg.inference.checkpoint_path))
31
+ cfg.inference.result_dir = str(resolve_path(cfg.inference.result_dir))
32
+ return cfg
33
+
34
+
35
+ def load_deepcfd_datapipe_class():
36
+ runtime_root = Path(onescience.__file__).resolve().parent
37
+ datapipe_file = runtime_root / "datapipes" / "cfd" / "deepcfd.py"
38
+ spec = importlib.util.spec_from_file_location("_onescience_deepcfd_datapipe", datapipe_file)
39
+ if spec is None or spec.loader is None:
40
+ raise ImportError(f"Cannot load DeepCFD datapipe from {datapipe_file}")
41
+ module = importlib.util.module_from_spec(spec)
42
+ sys.modules[spec.name] = module
43
+ spec.loader.exec_module(module)
44
+ return module.DeepCFDDatapipe
45
+
46
+
47
+ def main():
48
+ DistributedManager.initialize()
49
+ dist = DistributedManager()
50
+ device = dist.device
51
+ cfg = load_config()
52
+ DeepCFDDatapipe = load_deepcfd_datapipe_class()
53
+
54
+ checkpoint_path = Path(cfg.inference.checkpoint_path)
55
+ if not checkpoint_path.exists():
56
+ raise FileNotFoundError(f"Checkpoint not found: {checkpoint_path}")
57
+
58
+ checkpoint = torch.load(checkpoint_path, map_location=device)
59
+ model_config = checkpoint.get("config", cfg.model.to_dict())
60
+ model = build_model(model_config).to(device)
61
+ model.load_state_dict(checkpoint["model_state"])
62
+ model.eval()
63
+
64
+ datapipe = DeepCFDDatapipe(cfg.datapipe, distributed=False)
65
+ test_loader, _ = datapipe.test_dataloader()
66
+ batch = next(iter(test_loader))
67
+
68
+ x = batch["x"].to(device)
69
+ y = batch["y"].to(device)
70
+ with torch.no_grad():
71
+ out = model(x)
72
+
73
+ error = torch.abs(out.cpu() - y.cpu())
74
+ mse = torch.mean((out.cpu() - y.cpu()) ** 2, dim=(0, 2, 3)).numpy()
75
+ mae = torch.mean(error, dim=(0, 2, 3)).numpy()
76
+
77
+ result_dir = Path(cfg.inference.result_dir)
78
+ vis_dir = result_dir / "vis_results"
79
+ pred_dir = result_dir / "predictions"
80
+ vis_dir.mkdir(parents=True, exist_ok=True)
81
+ pred_dir.mkdir(parents=True, exist_ok=True)
82
+
83
+ np.save(pred_dir / "prediction_batch.npy", out.cpu().numpy())
84
+ np.save(pred_dir / "target_batch.npy", y.cpu().numpy())
85
+ np.save(pred_dir / "absolute_error_batch.npy", error.numpy())
86
+
87
+ y_np = y.cpu().numpy()
88
+ out_np = out.cpu().numpy()
89
+ err_np = error.numpy()
90
+ for i in range(min(cfg.inference.num_visualize, x.shape[0])):
91
+ visualize(y_np, out_np, err_np, i, save_dir=str(vis_dir))
92
+
93
+ if dist.rank == 0:
94
+ print(f"Checkpoint: {checkpoint_path}")
95
+ print(f"MSE per channel [Ux, Uy, p]: {mse}")
96
+ print(f"MAE per channel [Ux, Uy, p]: {mae}")
97
+ print(f"Results saved to {result_dir}")
98
+
99
+ dist.cleanup()
100
+
101
+
102
+ if __name__ == "__main__":
103
+ main()
scripts/result.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ import numpy as np
5
+ import torch
6
+
7
+
8
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
9
+ sys.path.insert(0, str(PROJECT_ROOT))
10
+
11
+ from onescience.utils.YParams import YParams
12
+
13
+
14
+ def resolve_path(path_value):
15
+ path = Path(path_value)
16
+ return path if path.is_absolute() else PROJECT_ROOT / path
17
+
18
+
19
+ def main():
20
+ cfg = YParams(str(PROJECT_ROOT / "config" / "config.yaml"), "root")
21
+ checkpoint_path = resolve_path(cfg.inference.checkpoint_path)
22
+ pred_dir = resolve_path(cfg.inference.result_dir) / "predictions"
23
+
24
+ if checkpoint_path.exists():
25
+ ckpt = torch.load(checkpoint_path, map_location="cpu")
26
+ print(f"Checkpoint: {checkpoint_path}")
27
+ print(f"Epoch: {ckpt.get('epoch')}, val_loss: {ckpt.get('val_loss')}")
28
+ print(f"Model config: {ckpt.get('config')}")
29
+ else:
30
+ print(f"Checkpoint not found: {checkpoint_path}")
31
+
32
+ pred_path = pred_dir / "prediction_batch.npy"
33
+ if pred_path.exists():
34
+ pred = np.load(pred_path)
35
+ print(f"Prediction batch: shape={pred.shape}, dtype={pred.dtype}, mean={pred.mean():.6f}")
36
+ else:
37
+ print(f"Prediction batch not found: {pred_path}")
38
+
39
+
40
+ if __name__ == "__main__":
41
+ main()
scripts/train.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import importlib.util
3
+ from pathlib import Path
4
+
5
+ import torch
6
+ from torch.nn.parallel import DistributedDataParallel as DDP
7
+ from tqdm import tqdm
8
+
9
+
10
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
11
+ sys.path.insert(0, str(PROJECT_ROOT))
12
+
13
+ from model import build_model
14
+ from onescience.distributed.manager import DistributedManager
15
+ from onescience.utils.YParams import YParams
16
+ import onescience
17
+
18
+
19
+ def resolve_path(path_value):
20
+ path = Path(path_value)
21
+ return path if path.is_absolute() else PROJECT_ROOT / path
22
+
23
+
24
+ def load_config():
25
+ cfg = YParams(str(PROJECT_ROOT / "config" / "config.yaml"), "root")
26
+ cfg.datapipe.source.data_dir = str(resolve_path(cfg.datapipe.source.data_dir))
27
+ cfg.training.output_dir = str(resolve_path(cfg.training.output_dir))
28
+ return cfg
29
+
30
+
31
+ def load_deepcfd_datapipe_class():
32
+ runtime_root = Path(onescience.__file__).resolve().parent
33
+ datapipe_file = runtime_root / "datapipes" / "cfd" / "deepcfd.py"
34
+ spec = importlib.util.spec_from_file_location("_onescience_deepcfd_datapipe", datapipe_file)
35
+ if spec is None or spec.loader is None:
36
+ raise ImportError(f"Cannot load DeepCFD datapipe from {datapipe_file}")
37
+ module = importlib.util.module_from_spec(spec)
38
+ sys.modules[spec.name] = module
39
+ spec.loader.exec_module(module)
40
+ return module.DeepCFDDatapipe
41
+
42
+
43
+ def loss_func(output, target, weights):
44
+ lossu = (output[:, 0] - target[:, 0]) ** 2
45
+ lossv = (output[:, 1] - target[:, 1]) ** 2
46
+ lossp = torch.abs(output[:, 2] - target[:, 2])
47
+ loss_stack = torch.stack([lossu, lossv, lossp], dim=1)
48
+ return torch.sum(loss_stack / weights)
49
+
50
+
51
+ def evaluate(model, loader, device, weights, dist):
52
+ model.eval()
53
+ total_loss = 0.0
54
+ total_ux_mse = 0.0
55
+ total_uy_mse = 0.0
56
+ total_p_mse = 0.0
57
+ num_batches = 0
58
+
59
+ with torch.no_grad():
60
+ iterator = tqdm(loader, desc="Evaluating", disable=(dist.rank != 0))
61
+ for batch in iterator:
62
+ x = batch["x"].to(device)
63
+ y = batch["y"].to(device)
64
+ output = model(x)
65
+
66
+ total_loss += loss_func(output, y, weights).item()
67
+ total_ux_mse += torch.sum((output[:, 0] - y[:, 0]) ** 2).item()
68
+ total_uy_mse += torch.sum((output[:, 1] - y[:, 1]) ** 2).item()
69
+ total_p_mse += torch.sum((output[:, 2] - y[:, 2]) ** 2).item()
70
+ num_batches += 1
71
+
72
+ if num_batches == 0:
73
+ raise RuntimeError("Evaluation loader is empty. Check split_ratio and dataset size.")
74
+ return total_loss / num_batches, total_ux_mse, total_uy_mse, total_p_mse
75
+
76
+
77
+ def main():
78
+ DistributedManager.initialize()
79
+ dist = DistributedManager()
80
+ device = dist.device
81
+ cfg = load_config()
82
+ DeepCFDDatapipe = load_deepcfd_datapipe_class()
83
+
84
+ output_dir = Path(cfg.training.output_dir)
85
+ if dist.rank == 0:
86
+ output_dir.mkdir(parents=True, exist_ok=True)
87
+ print(f"Config: {PROJECT_ROOT / 'config' / 'config.yaml'}")
88
+ print(f"Data: {cfg.datapipe.source.data_dir}")
89
+ print(f"Checkpoint directory: {output_dir}")
90
+
91
+ datapipe = DeepCFDDatapipe(cfg.datapipe, distributed=(dist.world_size > 1))
92
+ train_loader, train_sampler = datapipe.train_dataloader()
93
+ test_loader, _ = datapipe.test_dataloader()
94
+ loss_weights = datapipe.get_loss_weights().to(device)
95
+
96
+ model = build_model(cfg.model).to(device)
97
+ if dist.world_size > 1:
98
+ device_ids = [dist.local_rank] if device.type == "cuda" else None
99
+ model = DDP(model, device_ids=device_ids)
100
+
101
+ optimizer = torch.optim.AdamW(
102
+ model.parameters(),
103
+ lr=cfg.training.lr,
104
+ weight_decay=cfg.training.weight_decay,
105
+ )
106
+
107
+ best_val_loss = float("inf")
108
+ patience_counter = 0
109
+
110
+ for epoch in range(cfg.training.num_epochs):
111
+ if train_sampler:
112
+ train_sampler.set_epoch(epoch)
113
+
114
+ model.train()
115
+ train_loss = 0.0
116
+ iterator = tqdm(train_loader, desc=f"Epoch {epoch}", disable=(dist.rank != 0))
117
+
118
+ for batch in iterator:
119
+ x = batch["x"].to(device)
120
+ y = batch["y"].to(device)
121
+
122
+ optimizer.zero_grad(set_to_none=True)
123
+ output = model(x)
124
+ loss = loss_func(output, y, loss_weights)
125
+ loss.backward()
126
+ optimizer.step()
127
+
128
+ train_loss += loss.item()
129
+ if dist.rank == 0:
130
+ iterator.set_postfix({"loss": f"{loss.item():.4e}"})
131
+
132
+ if len(train_loader) == 0:
133
+ raise RuntimeError("Training loader is empty. Check split_ratio and dataset size.")
134
+ avg_train_loss = train_loss / len(train_loader)
135
+
136
+ if (epoch + 1) % cfg.training.eval_interval == 0:
137
+ val_loss, ux_err, uy_err, p_err = evaluate(model, test_loader, device, loss_weights, dist)
138
+
139
+ if dist.rank == 0:
140
+ print(f"Epoch {epoch} | Train Loss: {avg_train_loss:.4e} | Val Loss: {val_loss:.4e}")
141
+ print(f"Metrics (Sum Sq Err): Ux={ux_err:.2e}, Uy={uy_err:.2e}, P={p_err:.2e}")
142
+
143
+ if val_loss < best_val_loss:
144
+ best_val_loss = val_loss
145
+ patience_counter = 0
146
+ model_to_save = model.module if hasattr(model, "module") else model
147
+ ckpt = {
148
+ "model_state": model_to_save.state_dict(),
149
+ "config": cfg.model.to_dict(),
150
+ "epoch": epoch,
151
+ "val_loss": val_loss,
152
+ }
153
+ torch.save(ckpt, output_dir / cfg.training.checkpoint_name)
154
+ print(f"Saved best model to {output_dir / cfg.training.checkpoint_name}")
155
+ else:
156
+ patience_counter += 1
157
+
158
+ stop_flag = torch.tensor([0], device=device)
159
+ if dist.rank == 0 and patience_counter >= cfg.training.patience:
160
+ stop_flag += 1
161
+ if dist.world_size > 1:
162
+ torch.distributed.broadcast(stop_flag, src=0)
163
+ if stop_flag.item() > 0:
164
+ break
165
+
166
+ dist.cleanup()
167
+
168
+
169
+ if __name__ == "__main__":
170
+ main()
weight/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+