-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtrain_lbm_neural_network_physics_aware.py
More file actions
782 lines (624 loc) · 35 KB
/
train_lbm_neural_network_physics_aware.py
File metadata and controls
782 lines (624 loc) · 35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
#!/usr/bin/env python3
"""
Physics-Aware Neural Network Training Script for LBM Flow Prediction
This script addresses critical physics violations in the current model:
1. Negative densities and temperatures (physically impossible)
2. Lack of conservation law enforcement
3. Poor numerical stability
KEY IMPROVEMENTS:
- Physics-informed loss function with conservation constraints
- Proper activation functions for physics fields
- Advanced normalization preserving physical constraints
- Regularization techniques for stability
- Physics violation monitoring during training
Author: Enhanced for physics-aware LBM prediction
"""
import os
import glob
import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import h5py
import json
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, MinMaxScaler
import matplotlib.pyplot as plt
def load_ml_training_data(data_dir="ml_training_data", max_cases=None, sample_timesteps=None):
"""
Load processed ML training data with physics-aware preprocessing
"""
print("Loading ML Training Data with Physics Constraints")
print("=" * 60)
# Find all case directories
case_pattern = os.path.join(data_dir, "case_*")
case_dirs = sorted(glob.glob(case_pattern))
if max_cases:
case_dirs = case_dirs[:max_cases]
print(f"Limited to first {max_cases} cases for testing")
print(f"Loading data from {len(case_dirs)} cases...")
# Initialize lists to store data
geometries = []
parameters = []
velocity_fields = []
heat_flux_fields = []
density_fields = []
energy_fields = []
temperature_fields = []
fluid_masks = [] # Store fluid masks for loss function
# Physics statistics for monitoring
physics_stats = {
'density_violations': 0,
'temperature_violations': 0,
'total_samples': 0
}
total_samples = 0
for i, case_dir in enumerate(case_dirs):
case_name = os.path.basename(case_dir)
# Load metadata
metadata_file = os.path.join(case_dir, f"{case_name}_metadata.json")
if not os.path.exists(metadata_file):
print(f" Skipping {case_name}: no metadata file")
continue
with open(metadata_file, 'r') as f:
metadata = json.load(f)
case_info = metadata['case_info']
nu_val = case_info['nu_value']
temp_val = case_info['temp_value']
alpha_val = case_info['alpha_value']
geom_id = case_info['geom_id']
# Load geometry
geometry_file = f"microstructure_geom_{geom_id}.csv"
geometry_path = os.path.join("/Users/nsawant/apps/nileshsawant/mlForLBM", geometry_file)
if os.path.exists(geometry_path):
import pandas as pd
geom_df = pd.read_csv(geometry_path)
# Reshape to 3D grid (60x40x30)
geometry_3d = np.zeros((60, 40, 30))
for _, row in geom_df.iterrows():
x, y, z = int(row['X']), int(row['Y']), int(row['Z'])
if x < 60 and y < 40 and z < 30:
geometry_3d[x, y, z] = row['tag']
else:
print(f" Warning: Geometry file not found: {geometry_path}")
continue
# Load NPZ files for this case
npz_pattern = os.path.join(case_dir, "plt*_ml.npz")
npz_files = sorted(glob.glob(npz_pattern))
if sample_timesteps:
npz_files = npz_files[::sample_timesteps]
case_samples = 0
for npz_file in npz_files:
try:
data = np.load(npz_file, allow_pickle=True)
# Extract simulation time
filename = os.path.basename(npz_file)
plt_number = filename.split('_')[0]
timestep = int(plt_number[3:])
time_val = float(timestep) / 1000.0
# Check required fields
required_fields = ['rho', 'vel_x', 'vel_y', 'vel_z', 'qx', 'qy', 'qz', 'two_rho_e', 'temperature']
missing_fields = [field for field in required_fields if field not in data.keys()]
if missing_fields:
continue
# Load data arrays (physics-aware loading)
rho = np.array(data['rho']).astype(np.float32)
velx = np.array(data['vel_x']).astype(np.float32)
vely = np.array(data['vel_y']).astype(np.float32)
velz = np.array(data['vel_z']).astype(np.float32)
qx = np.array(data['qx']).astype(np.float32)
qy = np.array(data['qy']).astype(np.float32)
qz = np.array(data['qz']).astype(np.float32)
two_rho_e = np.array(data['two_rho_e']).astype(np.float32)
temperature = np.array(data['temperature']).astype(np.float32)
# PHYSICS CHECK: Monitor violations ONLY in fluid regions (solid regions should have zero values)
if 'is_fluid' in data:
is_fluid = np.array(data['is_fluid']).astype(np.float32) # Keep as 0.0/1.0 values
fluid_mask = is_fluid.astype(bool) # Boolean version for indexing
fluid_rho = rho[fluid_mask]
fluid_temp = temperature[fluid_mask]
density_violations = np.sum(fluid_rho <= 0)
temp_violations = np.sum(fluid_temp <= 0)
physics_stats['density_violations'] += density_violations
physics_stats['temperature_violations'] += temp_violations
physics_stats['total_samples'] += np.sum(fluid_mask) # Only count fluid cells
# Store is_fluid array (not boolean mask) for loss function
fluid_masks.append(is_fluid)
# Warn about REAL violations (only in fluid regions)
if density_violations > 0:
print(f" Warning: {density_violations} non-positive densities in FLUID regions of {npz_file}")
# Only clamp fluid regions
rho[fluid_mask] = np.maximum(rho[fluid_mask], 1e-6)
if temp_violations > 0:
print(f" Warning: {temp_violations} non-positive temperatures in FLUID regions of {npz_file}")
# Only clamp fluid regions
temperature[fluid_mask] = np.maximum(temperature[fluid_mask], 1e-6)
else:
# Fallback: check all regions if no fluid mask available
density_violations = np.sum(rho <= 0)
temp_violations = np.sum(temperature <= 0)
physics_stats['density_violations'] += density_violations
physics_stats['temperature_violations'] += temp_violations
physics_stats['total_samples'] += rho.size
# Create a default is_fluid array (assume all fluid for safety)
is_fluid = np.ones_like(rho, dtype=np.float32)
fluid_masks.append(is_fluid)
if density_violations > 0:
print(f" Warning: {density_violations} non-positive densities in {npz_file} (no fluid mask - includes solid regions)")
rho = np.maximum(rho, 1e-6)
if temp_violations > 0:
print(f" Warning: {temp_violations} non-positive temperatures in {npz_file} (no fluid mask - includes solid regions)")
temperature = np.maximum(temperature, 1e-6)
# Store data
geometries.append(geometry_3d)
# NOTE: parameter ordering is standardized to [nu, temperature, alpha, time]
parameters.append([nu_val, temp_val, alpha_val, time_val])
# Stack components
velocity_fields.append(np.stack([velx, vely, velz], axis=-1))
heat_flux_fields.append(np.stack([qx, qy, qz], axis=-1))
density_fields.append(rho[..., np.newaxis])
energy_fields.append(two_rho_e[..., np.newaxis])
temperature_fields.append(temperature[..., np.newaxis])
case_samples += 1
except Exception as e:
print(f" Error loading {npz_file}: {e}")
continue
total_samples += case_samples
print(f" {case_name}: Loaded {case_samples} timesteps")
if (i + 1) % 10 == 0:
print(f" Progress: {i+1}/{len(case_dirs)} cases, {total_samples} total samples")
print(f"\nLoaded {total_samples} training samples from {len(case_dirs)} cases")
# Report physics violations in training data
print(f"\n Physics Violations in Training Data:")
print(f" Density violations: {physics_stats['density_violations']:,} / {physics_stats['total_samples']:,} ({100*physics_stats['density_violations']/physics_stats['total_samples']:.2f}%)")
print(f" Temperature violations: {physics_stats['temperature_violations']:,} / {physics_stats['total_samples']:,} ({100*physics_stats['temperature_violations']/physics_stats['total_samples']:.2f}%)")
# Convert to numpy arrays
training_data = {
'geometries': np.array(geometries),
'parameters': np.array(parameters),
'velocity_fields': np.array(velocity_fields),
'heat_flux_fields': np.array(heat_flux_fields),
'density_fields': np.array(density_fields),
'energy_fields': np.array(energy_fields),
'temperature_fields': np.array(temperature_fields),
'fluid_masks': np.array(fluid_masks)
}
return training_data
def create_physics_aware_model(input_shape_geom=(60, 40, 30, 1), input_shape_params=(4,)):
"""
Create physics-aware neural network model with proper constraints
KEY IMPROVEMENTS:
1. Physics-appropriate activation functions
2. Batch normalization for stability
3. Residual connections
4. Proper output constraints
"""
# Inputs
geom_input = keras.Input(shape=input_shape_geom, name='geometry')
param_input = keras.Input(shape=input_shape_params, name='parameters')
# Geometry encoding with batch normalization
x = layers.Conv3D(16, (3, 3, 3), padding='same')(geom_input)
x = layers.BatchNormalization()(x)
x = layers.ReLU()(x)
shortcut1 = x
x = layers.Conv3D(24, (3, 3, 3), padding='same')(x)
x = layers.BatchNormalization()(x)
x = layers.ReLU()(x)
x = layers.MaxPooling3D((2, 2, 2))(x) # (30, 20, 15)
x = layers.Conv3D(32, (3, 3, 3), padding='same')(x)
x = layers.BatchNormalization()(x)
x = layers.ReLU()(x)
shortcut2 = x
x = layers.Conv3D(32, (3, 3, 3), padding='same')(x)
x = layers.BatchNormalization()(x)
x = layers.ReLU()(x)
x = layers.MaxPooling3D((2, 2, 2))(x) # (15, 10, 7)
# Global feature extraction
geom_features = layers.GlobalAveragePooling3D()(x)
geom_features = layers.Dense(64, activation='relu')(geom_features)
geom_features = layers.Dropout(0.1)(geom_features) # Regularization
# Parameter processing
param_features = layers.Dense(16, activation='relu')(param_input)
param_features = layers.Dense(32, activation='relu')(param_features)
param_features = layers.Dropout(0.1)(param_features)
# Feature fusion
fused = layers.Concatenate()([geom_features, param_features])
fused = layers.Dense(128, activation='relu')(fused)
fused = layers.Dropout(0.1)(fused)
# Decoder preparation
fused = layers.Dense(15 * 10 * 7 * 16, activation='relu')(fused)
fused = layers.Reshape((15, 10, 7, 16))(fused)
# Decoder with residual connections
x = layers.Conv3DTranspose(32, (3, 3, 3), padding='same')(fused)
x = layers.BatchNormalization()(x)
x = layers.ReLU()(x)
x = layers.UpSampling3D((2, 2, 2))(x) # (30, 20, 14)
x = layers.Conv3D(24, (3, 3, 3), padding='same')(x)
x = layers.BatchNormalization()(x)
x = layers.ReLU()(x)
x = layers.UpSampling3D((2, 2, 2))(x) # (60, 40, 28)
# Adjust to target size
x = layers.ZeroPadding3D(padding=((0, 0), (0, 0), (1, 1)))(x) # (60, 40, 30)
# Shared feature refinement
shared_features = layers.Conv3D(16, (3, 3, 3), padding='same')(x)
shared_features = layers.BatchNormalization()(shared_features)
shared_features = layers.ReLU()(shared_features)
# PHYSICS-AWARE OUTPUT BRANCHES
# Output branches with physics-aware activations (same structure as enhanced version)
velocity_output = layers.Conv3D(3, (1, 1, 1), activation='tanh', padding='same', name='velocity')(shared_features) # [-1, +1] bounded
heat_flux_output = layers.Conv3D(3, (1, 1, 1), activation='tanh', padding='same', name='heat_flux')(shared_features) # [-1, +1] bounded
density_output = layers.Conv3D(1, (1, 1, 1), activation='softplus', padding='same', name='density')(shared_features) # Always > 0
energy_output = layers.Conv3D(1, (1, 1, 1), activation='softplus', padding='same', name='energy')(shared_features) # Always > 0
temperature_output = layers.Conv3D(1, (1, 1, 1), activation='softplus', padding='same', name='temperature')(shared_features) # Always > 0
model = keras.Model(
inputs=[geom_input, param_input],
outputs=[velocity_output, heat_flux_output, density_output, energy_output, temperature_output]
)
return model
def physics_aware_loss_simple(y_true_list, y_pred_list):
"""
Simplified physics-aware loss function focusing on the critical issues
Key improvements:
1. Standard MSE loss for data fitting
2. Small positivity penalty (backup to activation functions)
NOTE: Main physics enforcement comes from activation functions:
- Softplus for density, energy, temperature (guarantees > 0)
- Tanh for velocity, heat flux (bounded range)
"""
# Extract tensors by index (TensorFlow graph-compatible)
velocity_true = y_true_list[0]
heat_flux_true = y_true_list[1]
density_true = y_true_list[2]
energy_true = y_true_list[3]
temperature_true = y_true_list[4]
velocity_pred = y_pred_list[0]
heat_flux_pred = y_pred_list[1]
density_pred = y_pred_list[2]
energy_pred = y_pred_list[3]
temperature_pred = y_pred_list[4]
# Standard MSE losses
velocity_loss = tf.reduce_mean(tf.square(velocity_true - velocity_pred))
heat_flux_loss = tf.reduce_mean(tf.square(heat_flux_true - heat_flux_pred))
density_loss = tf.reduce_mean(tf.square(density_true - density_pred))
energy_loss = tf.reduce_mean(tf.square(energy_true - energy_pred))
temperature_loss = tf.reduce_mean(tf.square(temperature_true - temperature_pred))
mse_loss = velocity_loss + heat_flux_loss + density_loss + energy_loss + temperature_loss
# Optional: Small positivity penalty as backup (should be near zero with softplus)
# This is mainly for safety and monitoring
density_positivity = tf.reduce_mean(tf.maximum(0.0, 1e-8 - density_pred)) # ρ > 0
temperature_positivity = tf.reduce_mean(tf.maximum(0.0, 1e-8 - temperature_pred)) # T > 0
energy_positivity = tf.reduce_mean(tf.maximum(0.0, 1e-8 - energy_pred)) # E > 0
positivity_loss = density_positivity + temperature_positivity + energy_positivity
# Total loss: mostly MSE with tiny positivity backup
total_loss = mse_loss + 0.01 * positivity_loss # Very small weight since softplus handles it
return total_loss
def physics_aware_loss_hybrid(is_fluid_array):
"""
Create a hybrid physics-aware loss function using is_fluid array for weighting
Key insight: is_fluid = 1 - tag, where tag comes from CSV geometry
- is_fluid = 1.0 for fluid regions (tag = 0)
- is_fluid = 0.0 for solid regions (tag = 1)
Strategy:
- Train on ALL points (like enhanced version)
- Use is_fluid directly as continuous weighting factor
- Higher weight on fluid regions, lower weight on solid regions
- Follow enhanced version's simple loss pattern but with weighting
"""
def loss_function(y_true, y_pred):
# Simplified approach: Keras calls this function once per output
# Just compute MSE for the current output + light physics penalties
# Standard MSE loss for this output (like enhanced version)
mse_loss = tf.reduce_mean(tf.square(y_true - y_pred))
# Add light physics penalties only for positive quantities (density, energy, temperature)
# We can detect these by checking if the last dimension is 1 (scalar fields)
shape = tf.shape(y_pred)
last_dim = shape[-1]
physics_penalty = tf.cond(
tf.equal(last_dim, 1), # If scalar field (density, energy, or temperature)
lambda: tf.reduce_mean(tf.maximum(0.0, 1e-8 - y_pred)), # Positivity penalty
lambda: tf.constant(0.0) # No penalty for vector fields (velocity, heat_flux)
)
# Total loss: MSE + light physics penalty
total_loss = mse_loss + 0.01 * physics_penalty
return total_loss
# Convert is_fluid array to tensor (values are 0.0 or 1.0)
is_fluid_tensor = tf.constant(is_fluid_array, dtype=tf.float32) # Shape: [nX, nY, nZ]
# Add batch dimension for broadcasting: [nX, nY, nZ] -> [1, nX, nY, nZ]
is_fluid_expanded = tf.expand_dims(is_fluid_tensor, axis=0)
# Create adaptive weighting:
# - Full weight for fluid regions (is_fluid = 1.0)
# - Reduced weight for solid regions (is_fluid = 0.0)
solid_weight = 0.2 # Give solid regions some weight to maintain structure
adaptive_weight_base = is_fluid_expanded + solid_weight * (1.0 - is_fluid_expanded)
# Result: fluid regions get weight 1.0, solid regions get weight 0.2, shape: [1, 60, 40, 30]
# Compute MSE losses - handle different output shapes correctly
# ASSUMPTION: All outputs might have 3 components based on error message
# Reduce over the last dimension first, then apply weighting
velocity_mse = tf.reduce_mean(tf.square(velocity_true - velocity_pred), axis=-1) # -> [batch, 60, 40, 30]
heat_flux_mse = tf.reduce_mean(tf.square(heat_flux_true - heat_flux_pred), axis=-1) # -> [batch, 60, 40, 30]
# If density/energy/temperature also have 3 components (as error suggests), handle them too
density_mse = tf.reduce_mean(tf.square(density_true - density_pred), axis=-1) # -> [batch, 60, 40, 30]
energy_mse = tf.reduce_mean(tf.square(energy_true - energy_pred), axis=-1) # -> [batch, 60, 40, 30]
temperature_mse = tf.reduce_mean(tf.square(temperature_true - temperature_pred), axis=-1) # -> [batch, 60, 40, 30]
# Apply adaptive weighting to each loss component (all now have compatible shapes)
velocity_loss = tf.reduce_mean(adaptive_weight_base * velocity_mse)
heat_flux_loss = tf.reduce_mean(adaptive_weight_base * heat_flux_mse)
density_loss = tf.reduce_mean(adaptive_weight_base * density_mse)
energy_loss = tf.reduce_mean(adaptive_weight_base * energy_mse)
temperature_loss = tf.reduce_mean(adaptive_weight_base * temperature_mse)
mse_loss = velocity_loss + heat_flux_loss + density_loss + energy_loss + temperature_loss
# Physics constraints: Weighted by is_fluid (naturally focuses on fluid regions)
density_positivity = tf.reduce_mean(is_fluid_tensor * tf.maximum(0.0, 1e-8 - tf.reduce_mean(density_pred, axis=-1)))
temperature_positivity = tf.reduce_mean(is_fluid_tensor * tf.maximum(0.0, 1e-8 - tf.reduce_mean(temperature_pred, axis=-1)))
energy_positivity = tf.reduce_mean(is_fluid_tensor * tf.maximum(0.0, 1e-8 - tf.reduce_mean(energy_pred, axis=-1)))
positivity_loss = density_positivity + temperature_positivity + energy_positivity
# Total loss: weighted MSE + physics penalties for fluid regions
total_loss = mse_loss + 0.05 * positivity_loss # Physics penalty weight
return total_loss
return loss_function
def create_is_fluid_from_geometry(geometry_array):
"""
Helper function to create is_fluid array from geometry tag field
Used during inference when only geometry is available
Args:
geometry_array: 3D array with tag values (1=solid, 0=fluid)
Returns:
is_fluid_array: 3D array with is_fluid values (1.0=fluid, 0.0=solid)
"""
return 1.0 - geometry_array.astype(np.float32)
class CompressibleLBMMetrics(keras.callbacks.Callback):
"""Custom callback to monitor physics violations for compressible LBM during training"""
def on_epoch_end(self, epoch, logs=None):
if hasattr(self, 'validation_data') and self.validation_data:
# Get validation predictions
x_val = self.validation_data[0]
y_val = self.validation_data[1]
predictions = self.model.predict(x_val[:4], verbose=0) # Sample first 4 for speed
velocity_pred, heat_flux_pred, density_pred, energy_pred, temperature_pred = predictions
# Count physics violations
density_violations = np.sum(density_pred <= 0)
temp_violations = np.sum(temperature_pred <= 0)
energy_violations = np.sum(energy_pred <= 0)
# Compressible flow specific checks
density_range = [np.min(density_pred), np.max(density_pred)]
temp_range = [np.min(temperature_pred), np.max(temperature_pred)]
velocity_max = np.max(np.sqrt(np.sum(velocity_pred**2, axis=-1)))
# Calculate total elements
total_elements = density_pred.size
if epoch % 10 == 0: # Report every 10 epochs
print(f"\n Compressible LBM Physics Check (Epoch {epoch}):")
print(f" Density violations: {density_violations:>7,} / {total_elements:,} ({100*density_violations/total_elements:.3f}%)")
print(f" Temperature violations: {temp_violations:>7,} / {total_elements:,} ({100*temp_violations/total_elements:.3f}%)")
print(f" Energy violations: {energy_violations:>7,} / {total_elements:,} ({100*energy_violations/total_elements:.3f}%)")
print(f" Density range: [{density_range[0]:.6f}, {density_range[1]:.6f}]")
print(f" Temperature range: [{temp_range[0]:.6f}, {temp_range[1]:.6f}]")
print(f" Max velocity magnitude: {velocity_max:.6f}")
def train_physics_aware_model(training_data, validation_split=0.2, epochs=100, batch_size=4):
"""
Train physics-aware LBM neural network model
"""
print("Setting up physics-aware neural network training...")
# Prepare input data
geometries = training_data['geometries'][..., np.newaxis]
parameters = training_data['parameters']
# Prepare output data with physics-aware normalization
velocities = training_data['velocity_fields']
heat_fluxes = training_data['heat_flux_fields']
densities = training_data['density_fields']
energies = training_data['energy_fields']
temperatures = training_data['temperature_fields']
is_fluid_arrays = training_data['fluid_masks'] # is_fluid arrays (0.0=solid, 1.0=fluid)
# PHYSICS-AWARE NORMALIZATION
print(" Applying physics-aware normalization...")
# For velocity: normalize to [-1, 1] range (will use tanh activation)
velocity_scale = np.maximum(np.abs(velocities).max(), 1e-6)
velocities_norm = velocities / velocity_scale
# For heat flux: normalize to [-1, 1] range (will use tanh activation)
heat_flux_scale = np.maximum(np.abs(heat_fluxes).max(), 1e-6)
heat_fluxes_norm = heat_fluxes / heat_flux_scale
# For positive quantities (density, energy, temperature): use log normalization
# This preserves positivity and handles wide dynamic ranges
# Density: log-normalize (ensure positive)
densities_safe = np.maximum(densities, 1e-6)
density_log_mean = np.log(densities_safe).mean()
density_log_std = np.log(densities_safe).std()
densities_norm = (np.log(densities_safe) - density_log_mean) / density_log_std
# Energy: log-normalize (ensure positive)
energies_safe = np.maximum(energies, 1e-6)
energy_log_mean = np.log(energies_safe).mean()
energy_log_std = np.log(energies_safe).std()
energies_norm = (np.log(energies_safe) - energy_log_mean) / energy_log_std
# Temperature: log-normalize (ensure positive)
temperatures_safe = np.maximum(temperatures, 1e-6)
temp_log_mean = np.log(temperatures_safe).mean()
temp_log_std = np.log(temperatures_safe).std()
temperatures_norm = (np.log(temperatures_safe) - temp_log_mean) / temp_log_std
# Store normalization parameters for later use
norm_params = {
'velocity_scale': velocity_scale,
'heat_flux_scale': heat_flux_scale,
'density_log_mean': density_log_mean,
'density_log_std': density_log_std,
'energy_log_mean': energy_log_mean,
'energy_log_std': energy_log_std,
'temp_log_mean': temp_log_mean,
'temp_log_std': temp_log_std
}
# Normalize parameters
param_scaler = StandardScaler()
parameters_scaled = param_scaler.fit_transform(parameters)
# Save fitted parameter scaler (mean and scale) for inference
try:
scaler_info = {
'mean': param_scaler.mean_.tolist(),
'scale': param_scaler.scale_.tolist()
}
with open('param_scaler.json', 'w') as sf:
json.dump(scaler_info, sf, indent=2)
print("Saved parameter scaler to 'param_scaler.json'")
except Exception as e:
print(f"Warning: could not save parameter scaler: {e}")
# Split data
indices = np.arange(len(geometries))
train_idx, val_idx = train_test_split(indices, test_size=validation_split, random_state=42)
# Create physics-aware model
model = create_physics_aware_model()
# Custom training loop with physics-informed loss
optimizer = keras.optimizers.Adam(learning_rate=0.001)
# Manual Learning Rate Scheduler - reduce every 5 epochs
def lr_schedule(epoch, lr):
"""Learning rate schedule: reduce by 50% every 5 epochs"""
if epoch > 0 and epoch % 5 == 0:
new_lr = lr * 0.5
print(f"\n Learning rate reduced at epoch {epoch}: {lr:.6f} → {new_lr:.6f}")
return new_lr
return lr
lr_scheduler = keras.callbacks.LearningRateScheduler(lr_schedule, verbose=0)
# Early stopping with shorter patience for 20-epoch training
early_stop = keras.callbacks.EarlyStopping(
monitor='val_loss', patience=8, restore_best_weights=True, verbose=1
)
# Model checkpoint to save best model
checkpoint = keras.callbacks.ModelCheckpoint(
'best_physics_aware_model.h5',
monitor='val_loss',
save_best_only=True,
verbose=1
)
physics_monitor = ActivationPhysicsMetrics()
# Prepare training data
X_train = [geometries[train_idx], parameters_scaled[train_idx]]
y_train = [velocities_norm[train_idx], heat_fluxes_norm[train_idx],
densities_norm[train_idx], energies_norm[train_idx], temperatures_norm[train_idx]]
X_val = [geometries[val_idx], parameters_scaled[val_idx]]
y_val = [velocities_norm[val_idx], heat_fluxes_norm[val_idx],
densities_norm[val_idx], energies_norm[val_idx], temperatures_norm[val_idx]]
# Set validation data for physics monitoring
physics_monitor.validation_data = (X_val, y_val)
# Create is_fluid array for loss function (use first sample as they all have same geometry)
is_fluid_array = is_fluid_arrays[0] # Shape: [nX, nY, nZ], values: 0.0=solid, 1.0=fluid
# Compile with hybrid physics-aware loss using the is_fluid array
model.compile(
optimizer=optimizer,
loss=physics_aware_loss_hybrid(is_fluid_array), # Hybrid loss for solid/fluid handling
metrics=['mae', 'mae', 'mae', 'mae', 'mae'] # One metric for each of the 5 outputs
)
print(" Starting physics-aware training...")
history = model.fit(
X_train, y_train,
validation_data=(X_val, y_val),
epochs=epochs,
batch_size=batch_size,
callbacks=[lr_scheduler, early_stop, checkpoint, physics_monitor],
verbose=1
)
return model, param_scaler, norm_params, history.history['loss']
class ActivationPhysicsMetrics(keras.callbacks.Callback):
"""Monitor physics violations with focus on activation function effectiveness"""
def on_epoch_end(self, epoch, logs=None):
if hasattr(self, 'validation_data') and self.validation_data:
# Get validation predictions
x_val = self.validation_data[0]
y_val = self.validation_data[1]
predictions = self.model.predict(x_val[:4], verbose=0) # Sample first 4 for speed
velocity_pred, heat_flux_pred, density_pred, energy_pred, temperature_pred = predictions
# Count physics violations (should be zero with proper activations)
density_violations = np.sum(density_pred <= 0)
temp_violations = np.sum(temperature_pred <= 0)
energy_violations = np.sum(energy_pred <= 0)
# Check activation function effectiveness
density_range = [np.min(density_pred), np.max(density_pred)]
temp_range = [np.min(temperature_pred), np.max(temperature_pred)]
energy_range = [np.min(energy_pred), np.max(energy_pred)]
# Velocity should be bounded by tanh activation
velocity_magnitude = np.sqrt(np.sum(velocity_pred**2, axis=-1))
velocity_range = [np.min(velocity_magnitude), np.max(velocity_magnitude)]
# Calculate total elements
total_elements = density_pred.size
if epoch % 10 == 0: # Report every 10 epochs
print(f"\n Activation Function Physics Check (Epoch {epoch}):")
print(f" Density violations: {density_violations:>7,} / {total_elements:,} ({100*density_violations/total_elements:.3f}%)")
print(f" Temperature violations: {temp_violations:>7,} / {total_elements:,} ({100*temp_violations/total_elements:.3f}%)")
print(f" Energy violations: {energy_violations:>7,} / {total_elements:,} ({100*energy_violations/total_elements:.3f}%)")
print(f" Density range: [{density_range[0]:.6f}, {density_range[1]:.6f}] (softplus )")
print(f" Temperature range: [{temp_range[0]:.6f}, {temp_range[1]:.6f}] (softplus )")
print(f" Energy range: [{energy_range[0]:.6f}, {energy_range[1]:.6f}] (softplus )")
print(f" Velocity magnitude: [{velocity_range[0]:.6f}, {velocity_range[1]:.6f}] (tanh scaling)")
def main():
"""
Main physics-aware training pipeline
"""
print(" PHYSICS-AWARE LBM Neural Network Training")
print("=" * 60)
print(" Addressing critical issues with SMART ACTIVATION FUNCTIONS:")
print(" Negative densities (~2.2M violations per case)")
print(" Negative temperatures (~2.6M violations per case)")
print(" Softplus activation for density, energy, temperature (GUARANTEES > 0)")
print(" Tanh activation for velocity, heat flux (bounded range)")
print(" Physics-aware data normalization")
print(" Improved numerical stability")
print(" NO conservation law penalties (keeping it simple!)")
print()
# Load training data with physics monitoring
training_data = load_ml_training_data()
if len(training_data['geometries']) == 0:
print(" No training data loaded!")
return
print(f"\n Training data shapes:")
for key, data in training_data.items():
print(f" {key}: {data.shape}")
# Create and train physics-aware model
print("\n Creating physics-aware model:")
model = create_physics_aware_model()
param_count = model.count_params()
model_size = param_count * 4 / (1024**2) # MB
print(f" Parameters: {param_count:,} ({model_size:.1f} MB)")
model.summary()
# Train the model with aggressive 20-epoch schedule
model, scaler, norm_params, losses = train_physics_aware_model(
training_data, epochs=20, batch_size=8 # Aggressive 20 epochs with LR reduction every 5 epochs
)
# Save model and normalization parameters
model_filename = 'lbm_flow_predictor_physics_aware.h5'
model.save(model_filename)
# Save normalization parameters
norm_filename = 'physics_aware_normalization_params.json'
with open(norm_filename, 'w') as f:
# Convert numpy types to Python types for JSON serialization
norm_params_json = {}
for key, value in norm_params.items():
if isinstance(value, np.ndarray):
norm_params_json[key] = value.tolist()
elif isinstance(value, (np.float32, np.float64)):
norm_params_json[key] = float(value)
else:
norm_params_json[key] = value
json.dump(norm_params_json, f, indent=2)
print(f"\n Physics-aware model saved as '{model_filename}'")
print(f" Normalization parameters saved as '{norm_filename}'")
# Plot training loss
plt.figure(figsize=(12, 6))
plt.plot(losses)
plt.title(f'Physics-Aware Training Loss\n({param_count:,} parameters)')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.yscale('log')
plt.grid(True)
plt.savefig('training_loss_physics_aware.png')
plt.show()
print("\n KEY IMPROVEMENTS WITH ACTIVATION FUNCTIONS:")
print(" Softplus activation for density, energy, temperature (MATHEMATICALLY > 0)")
print(" Tanh activation for velocity, heat flux (bounded range)")
print(" Batch normalization for training stability")
print(" Physics-aware data normalization (log-scale for positive quantities)")
print(" Activation function effectiveness monitoring")
print(" Simplified loss function (no complex conservation penalties)")
print(" Regularization to prevent overfitting")
print(" Focus on the CRITICAL issue: negative values elimination")
print(f"\n Next steps:")
print(f" 1. Test activation-function model with validation script")
print(f" 2. Verify ZERO physics violations (negative ρ, T, E)")
print(f" 3. Compare performance with baseline enhanced model")
print(f" 4. If successful, add conservation penalties later if needed")
if __name__ == "__main__":
main()