ddDT-GFM 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dddt_gfm/__init__.py +9 -0
- dddt_gfm/data_cleaning/__init__.py +0 -0
- dddt_gfm/data_cleaning/physics_constraint.py +358 -0
- dddt_gfm/dataloaders/__init__.py +0 -0
- dddt_gfm/dataloaders/dataloader_DIW.py +119 -0
- dddt_gfm/dataloaders/dataloader_sunsmart.py +1278 -0
- dddt_gfm/dataloaders/rwb_dataloader_sunsmart.py +1380 -0
- dddt_gfm/exemplar_DIW/__init__.py +0 -0
- dddt_gfm/exemplar_DIW/training/__init__.py +0 -0
- dddt_gfm/exemplar_DIW/training/train.py +194 -0
- dddt_gfm/exemplar_LPBF/__init__.py +0 -0
- dddt_gfm/exemplar_LPBF/training/__init__.py +0 -0
- dddt_gfm/exemplar_LPBF/training/train.py +269 -0
- dddt_gfm/exemplar_PV/__init__.py +0 -0
- dddt_gfm/exemplar_PV/training/__init__.py +0 -0
- dddt_gfm/exemplar_PV/training/train.py +262 -0
- dddt_gfm/models/__init__.py +0 -0
- dddt_gfm/models/stGAE.py +300 -0
- dddt_gfm/rwb-graph_UNET.py +161 -0
- dddt_gfm/tests/__init__.py +0 -0
- dddt_gfm/tests/test_util.py +5 -0
- dddt_gfm/util/__init__.py +0 -0
- dddt_gfm/util/callbacks.py +47 -0
- dddt_gfm/util/io.py +63 -0
- dddt_gfm-0.1.0.dist-info/METADATA +95 -0
- dddt_gfm-0.1.0.dist-info/RECORD +31 -0
- dddt_gfm-0.1.0.dist-info/WHEEL +5 -0
- dddt_gfm-0.1.0.dist-info/licenses/LICENSE +24 -0
- dddt_gfm-0.1.0.dist-info/scm_file_list.json +45 -0
- dddt_gfm-0.1.0.dist-info/scm_version.json +8 -0
- dddt_gfm-0.1.0.dist-info/top_level.txt +1 -0
dddt_gfm/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""ddDT-GFM: data-driven Digital Twin Graph Foundation Models for fleet-level
|
|
2
|
+
system performance prediction using spatiotemporal graph neural networks.
|
|
3
|
+
"""
|
|
4
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
__version__ = version("ddDT-GFM")
|
|
8
|
+
except PackageNotFoundError: # package is not installed
|
|
9
|
+
__version__ = "0.0.0"
|
|
File without changes
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Physics-Informed Constraints for Data-Driven Digital Twins
|
|
3
|
+
|
|
4
|
+
This module implements physics-based validation and constraints that can be
|
|
5
|
+
applied during data preprocessing and model training. Addresses Reviewer Concern #1.
|
|
6
|
+
|
|
7
|
+
Key Features:
|
|
8
|
+
- Domain-specific physics validation
|
|
9
|
+
- Measurement filtering based on physical limits
|
|
10
|
+
- Integration with selective loss calculation
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import torch
|
|
15
|
+
from typing import Dict, Callable, Optional, Tuple
|
|
16
|
+
from abc import ABC, abstractmethod
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PhysicsConstraint(ABC):
|
|
20
|
+
"""Base class for physics-informed constraints"""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def validate(self, data: Dict[str, np.ndarray]) -> np.ndarray:
|
|
24
|
+
"""
|
|
25
|
+
Validate data against physics constraints
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
data: Dictionary of measurement arrays
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Boolean mask where True indicates valid data
|
|
32
|
+
"""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def get_theoretical_limit(self, data: Dict[str, np.ndarray]) -> np.ndarray:
|
|
37
|
+
"""
|
|
38
|
+
Calculate theoretical limit based on physics
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
data: Dictionary of input measurements
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Array of theoretical maximum/minimum values
|
|
45
|
+
"""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PVPowerConstraint(PhysicsConstraint):
|
|
50
|
+
"""
|
|
51
|
+
Photovoltaic power constraint: Power cannot exceed theoretical maximum
|
|
52
|
+
|
|
53
|
+
Formula: max_power = irradiance * area * efficiency_limit
|
|
54
|
+
Default efficiency_limit = 0.25 (25% maximum efficiency)
|
|
55
|
+
|
|
56
|
+
References:
|
|
57
|
+
- Shockley-Queisser limit for single-junction solar cells
|
|
58
|
+
- Realistic field efficiency accounting for temperature, soiling, etc.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, area: float = 1.0, efficiency_limit: float = 0.25):
|
|
62
|
+
"""
|
|
63
|
+
Args:
|
|
64
|
+
area: Panel area in m^2
|
|
65
|
+
efficiency_limit: Maximum theoretical efficiency (default 0.25 = 25%)
|
|
66
|
+
"""
|
|
67
|
+
self.area = area
|
|
68
|
+
self.efficiency_limit = efficiency_limit
|
|
69
|
+
|
|
70
|
+
def validate(self, data: Dict[str, np.ndarray]) -> np.ndarray:
|
|
71
|
+
"""
|
|
72
|
+
Validate PV power measurements
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
data: Must contain 'irradiance' (W/m^2) and 'dc_power' (W)
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Boolean mask: True where power ≤ theoretical maximum
|
|
79
|
+
"""
|
|
80
|
+
irradiance = data['irradiance']
|
|
81
|
+
dc_power = data['dc_power']
|
|
82
|
+
|
|
83
|
+
max_power = self.get_theoretical_limit(data)
|
|
84
|
+
|
|
85
|
+
# Allow small tolerance for measurement uncertainty
|
|
86
|
+
tolerance = 1.05 # 5% tolerance
|
|
87
|
+
valid_mask = dc_power <= (max_power * tolerance)
|
|
88
|
+
|
|
89
|
+
# Also check for negative power (non-physical)
|
|
90
|
+
valid_mask &= (dc_power >= 0)
|
|
91
|
+
|
|
92
|
+
return valid_mask
|
|
93
|
+
|
|
94
|
+
def get_theoretical_limit(self, data: Dict[str, np.ndarray]) -> np.ndarray:
|
|
95
|
+
"""
|
|
96
|
+
Calculate theoretical maximum power
|
|
97
|
+
|
|
98
|
+
Formula: P_max = G * A * η_max
|
|
99
|
+
where G = irradiance, A = area, η_max = efficiency limit
|
|
100
|
+
"""
|
|
101
|
+
irradiance = data['irradiance']
|
|
102
|
+
return irradiance * self.area * self.efficiency_limit
|
|
103
|
+
|
|
104
|
+
def flag_violations(self, data: Dict[str, np.ndarray]) -> Tuple[np.ndarray, float]:
|
|
105
|
+
"""
|
|
106
|
+
Identify and quantify constraint violations
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
violation_mask: Boolean array of violations
|
|
110
|
+
violation_rate: Fraction of measurements violating constraint
|
|
111
|
+
"""
|
|
112
|
+
valid_mask = self.validate(data)
|
|
113
|
+
violation_mask = ~valid_mask
|
|
114
|
+
violation_rate = np.mean(violation_mask)
|
|
115
|
+
|
|
116
|
+
return violation_mask, violation_rate
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class LPBFEnergyDensityConstraint(PhysicsConstraint):
|
|
120
|
+
"""
|
|
121
|
+
Laser Powder Bed Fusion energy density relationship
|
|
122
|
+
|
|
123
|
+
Formula: energy_density = laser_power / scan_speed
|
|
124
|
+
|
|
125
|
+
This physics-based relationship ensures that the reported energy density
|
|
126
|
+
is consistent with the process parameters (laser power and scan speed).
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(self, tolerance: float = 0.02):
|
|
130
|
+
"""
|
|
131
|
+
Args:
|
|
132
|
+
tolerance: Relative tolerance for energy density calculation (default 2%)
|
|
133
|
+
"""
|
|
134
|
+
self.tolerance = tolerance
|
|
135
|
+
|
|
136
|
+
def validate(self, data: Dict[str, np.ndarray]) -> np.ndarray:
|
|
137
|
+
"""
|
|
138
|
+
Validate energy density calculations
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
data: Must contain 'laser_power' (W), 'scan_speed' (mm/s),
|
|
142
|
+
and 'energy_density' (J/mm)
|
|
143
|
+
"""
|
|
144
|
+
calculated_ed = self.get_theoretical_limit(data)
|
|
145
|
+
reported_ed = data['energy_density']
|
|
146
|
+
|
|
147
|
+
# Check relative error
|
|
148
|
+
relative_error = np.abs(calculated_ed - reported_ed) / (calculated_ed + 1e-10)
|
|
149
|
+
valid_mask = relative_error <= self.tolerance
|
|
150
|
+
|
|
151
|
+
# Also check for non-physical values
|
|
152
|
+
valid_mask &= (data['laser_power'] > 0)
|
|
153
|
+
valid_mask &= (data['scan_speed'] > 0)
|
|
154
|
+
valid_mask &= (data['energy_density'] > 0)
|
|
155
|
+
|
|
156
|
+
return valid_mask
|
|
157
|
+
|
|
158
|
+
def get_theoretical_limit(self, data: Dict[str, np.ndarray]) -> np.ndarray:
|
|
159
|
+
"""
|
|
160
|
+
Calculate energy density from laser power and scan speed
|
|
161
|
+
|
|
162
|
+
E = P / v
|
|
163
|
+
where E = energy density, P = laser power, v = scan speed
|
|
164
|
+
"""
|
|
165
|
+
laser_power = data['laser_power'] # W
|
|
166
|
+
scan_speed = data['scan_speed'] # mm/s
|
|
167
|
+
|
|
168
|
+
return laser_power / scan_speed # J/mm
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class DIWPositionConstraint(PhysicsConstraint):
|
|
172
|
+
"""
|
|
173
|
+
Direct Ink Write position constraints based on machine limits
|
|
174
|
+
|
|
175
|
+
Validates that position, velocity, and acceleration measurements
|
|
176
|
+
are within physical machine limits.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def __init__(self,
|
|
180
|
+
position_limits: Dict[str, Tuple[float, float]],
|
|
181
|
+
velocity_limit: float,
|
|
182
|
+
acceleration_limit: float):
|
|
183
|
+
"""
|
|
184
|
+
Args:
|
|
185
|
+
position_limits: Dict with 'x', 'y', 'z' keys, values are (min, max) tuples
|
|
186
|
+
velocity_limit: Maximum velocity magnitude (mm/s)
|
|
187
|
+
acceleration_limit: Maximum acceleration magnitude (mm/s^2)
|
|
188
|
+
"""
|
|
189
|
+
self.position_limits = position_limits
|
|
190
|
+
self.velocity_limit = velocity_limit
|
|
191
|
+
self.acceleration_limit = acceleration_limit
|
|
192
|
+
|
|
193
|
+
def validate(self, data: Dict[str, np.ndarray]) -> np.ndarray:
|
|
194
|
+
"""
|
|
195
|
+
Validate DIW mechatronics measurements
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
data: Must contain position, velocity, acceleration for x, y, z axes
|
|
199
|
+
"""
|
|
200
|
+
valid_mask = np.ones(len(data['x_pos']), dtype=bool)
|
|
201
|
+
|
|
202
|
+
# Check position limits
|
|
203
|
+
for axis in ['x', 'y', 'z']:
|
|
204
|
+
pos = data[f'{axis}_pos']
|
|
205
|
+
min_pos, max_pos = self.position_limits[axis]
|
|
206
|
+
valid_mask &= (pos >= min_pos) & (pos <= max_pos)
|
|
207
|
+
|
|
208
|
+
# Check velocity magnitude
|
|
209
|
+
velocity_mag = np.sqrt(
|
|
210
|
+
data['x_vel']**2 + data['y_vel']**2 + data['z_vel']**2
|
|
211
|
+
)
|
|
212
|
+
valid_mask &= (velocity_mag <= self.velocity_limit)
|
|
213
|
+
|
|
214
|
+
# Check acceleration magnitude
|
|
215
|
+
accel_mag = np.sqrt(
|
|
216
|
+
data['x_accel']**2 + data['y_accel']**2 + data['z_accel']**2
|
|
217
|
+
)
|
|
218
|
+
valid_mask &= (accel_mag <= self.acceleration_limit)
|
|
219
|
+
|
|
220
|
+
return valid_mask
|
|
221
|
+
|
|
222
|
+
def get_theoretical_limit(self, data: Dict[str, np.ndarray]) -> Dict[str, float]:
|
|
223
|
+
"""Return the configured limits as a dictionary"""
|
|
224
|
+
return {
|
|
225
|
+
'position': self.position_limits,
|
|
226
|
+
'velocity': self.velocity_limit,
|
|
227
|
+
'acceleration': self.acceleration_limit
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class SelectiveLossCalculator:
|
|
232
|
+
"""
|
|
233
|
+
Calculate loss only on validated/real measurements
|
|
234
|
+
|
|
235
|
+
This addresses the concern that training on imputed values can bias
|
|
236
|
+
the model. By using a mask to compute loss only on real measurements,
|
|
237
|
+
we maintain fidelity to actual system behavior.
|
|
238
|
+
|
|
239
|
+
Addresses Reviewer Concern #1 on physics-informed training.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
def __init__(self, constraint: Optional[PhysicsConstraint] = None):
|
|
243
|
+
"""
|
|
244
|
+
Args:
|
|
245
|
+
constraint: Optional physics constraint to further filter training data
|
|
246
|
+
"""
|
|
247
|
+
self.constraint = constraint
|
|
248
|
+
|
|
249
|
+
def compute_loss(self,
|
|
250
|
+
predictions: torch.Tensor,
|
|
251
|
+
targets: torch.Tensor,
|
|
252
|
+
real_data_mask: torch.Tensor,
|
|
253
|
+
data_dict: Optional[Dict[str, np.ndarray]] = None,
|
|
254
|
+
criterion: Callable = torch.nn.functional.mse_loss) -> torch.Tensor:
|
|
255
|
+
"""
|
|
256
|
+
Compute loss only on real (non-imputed) measurements that pass physics validation
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
predictions: Model predictions [batch, nodes, features, time]
|
|
260
|
+
targets: Ground truth values [batch, nodes, features, time]
|
|
261
|
+
real_data_mask: Boolean mask indicating real (non-imputed) measurements
|
|
262
|
+
data_dict: Optional dictionary for physics validation
|
|
263
|
+
criterion: Loss function (default MSE)
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Scalar loss computed only on valid measurements
|
|
267
|
+
"""
|
|
268
|
+
# Start with real data mask
|
|
269
|
+
valid_mask = real_data_mask.clone()
|
|
270
|
+
|
|
271
|
+
# Apply physics constraints if provided
|
|
272
|
+
if self.constraint is not None and data_dict is not None:
|
|
273
|
+
physics_valid = self.constraint.validate(data_dict)
|
|
274
|
+
physics_valid_tensor = torch.from_numpy(physics_valid).to(valid_mask.device)
|
|
275
|
+
valid_mask = valid_mask & physics_valid_tensor
|
|
276
|
+
|
|
277
|
+
# Select only valid measurements
|
|
278
|
+
valid_predictions = predictions[valid_mask]
|
|
279
|
+
valid_targets = targets[valid_mask]
|
|
280
|
+
|
|
281
|
+
# Compute loss
|
|
282
|
+
if len(valid_predictions) > 0:
|
|
283
|
+
loss = criterion(valid_predictions, valid_targets)
|
|
284
|
+
else:
|
|
285
|
+
# If no valid measurements, return zero loss
|
|
286
|
+
loss = torch.tensor(0.0, device=predictions.device)
|
|
287
|
+
|
|
288
|
+
return loss
|
|
289
|
+
|
|
290
|
+
def get_validation_statistics(self, real_data_mask: torch.Tensor) -> Dict[str, float]:
|
|
291
|
+
"""
|
|
292
|
+
Get statistics on data validation
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Dictionary with validation statistics
|
|
296
|
+
"""
|
|
297
|
+
total_measurements = real_data_mask.numel()
|
|
298
|
+
valid_measurements = real_data_mask.sum().item()
|
|
299
|
+
validation_rate = valid_measurements / total_measurements if total_measurements > 0 else 0.0
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
'total_measurements': total_measurements,
|
|
303
|
+
'valid_measurements': valid_measurements,
|
|
304
|
+
'validation_rate': validation_rate,
|
|
305
|
+
'rejected_measurements': total_measurements - valid_measurements
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# Example usage demonstrating physics-informed training
|
|
310
|
+
def example_pv_training():
|
|
311
|
+
"""
|
|
312
|
+
Example: Training PV model with physics-informed selective loss
|
|
313
|
+
"""
|
|
314
|
+
# Initialize physics constraint
|
|
315
|
+
pv_constraint = PVPowerConstraint(area=50.0, efficiency_limit=0.25)
|
|
316
|
+
|
|
317
|
+
# Initialize selective loss calculator
|
|
318
|
+
loss_calculator = SelectiveLossCalculator(constraint=pv_constraint)
|
|
319
|
+
|
|
320
|
+
# Simulated data
|
|
321
|
+
batch_size, num_nodes, num_features, time_steps = 8, 29, 4, 96
|
|
322
|
+
predictions = torch.randn(batch_size, num_nodes, num_features, time_steps)
|
|
323
|
+
targets = torch.randn(batch_size, num_nodes, num_features, time_steps)
|
|
324
|
+
real_data_mask = torch.rand(batch_size, num_nodes, num_features, time_steps) > 0.1 # 10% missing
|
|
325
|
+
|
|
326
|
+
# Example data dict for physics validation
|
|
327
|
+
data_dict = {
|
|
328
|
+
'irradiance': np.random.rand(batch_size * num_nodes * time_steps) * 1000,
|
|
329
|
+
'dc_power': np.random.rand(batch_size * num_nodes * time_steps) * 10000
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
# Compute selective loss
|
|
333
|
+
loss = loss_calculator.compute_loss(
|
|
334
|
+
predictions,
|
|
335
|
+
targets,
|
|
336
|
+
real_data_mask,
|
|
337
|
+
data_dict
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Get validation statistics
|
|
341
|
+
stats = loss_calculator.get_validation_statistics(real_data_mask)
|
|
342
|
+
|
|
343
|
+
print(f"Loss: {loss.item():.4f}")
|
|
344
|
+
print(f"Validation rate: {stats['validation_rate']:.2%}")
|
|
345
|
+
print(f"Valid measurements: {stats['valid_measurements']:,} / {stats['total_measurements']:,}")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
if __name__ == "__main__":
|
|
349
|
+
print("Physics-Informed Constraints Module")
|
|
350
|
+
print("=" * 50)
|
|
351
|
+
print("\nThis module addresses Reviewer Concern #1:")
|
|
352
|
+
print("Integration of physics-informed mechanisms in ddDT training")
|
|
353
|
+
print("\nKey features:")
|
|
354
|
+
print("1. Domain-specific physics validation (PV, L-PBF, DIW)")
|
|
355
|
+
print("2. Selective loss calculation on validated measurements")
|
|
356
|
+
print("3. Theoretical limit calculation based on physics")
|
|
357
|
+
print("\nRunning example...")
|
|
358
|
+
example_pv_training()
|
|
File without changes
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#%%
|
|
2
|
+
import random
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
from tsl.engines import Predictor
|
|
9
|
+
from typing import Callable, Mapping, Optional, Tuple, Union
|
|
10
|
+
from tsl.utils.casting import torch_to_numpy
|
|
11
|
+
from tsl.ops.connectivity import adj_to_edge_index
|
|
12
|
+
from tsl.metrics.torch import MaskedMSE, MaskedMAE, MaskedMAPE
|
|
13
|
+
from tsl.data.preprocessing import MinMaxScaler
|
|
14
|
+
from tsl.data.datamodule import SpatioTemporalDataModule, TemporalSplitter
|
|
15
|
+
import pytorch_lightning as pl
|
|
16
|
+
from tsl.typing import DataArray, SparseTensArray, TemporalIndex
|
|
17
|
+
from pytorch_lightning.callbacks import Callback, ModelCheckpoint
|
|
18
|
+
|
|
19
|
+
# out of the box data loader
|
|
20
|
+
from tsl.data import ImputationDataset
|
|
21
|
+
|
|
22
|
+
class DIWDataset(ImputationDataset):
|
|
23
|
+
def __init__(self,
|
|
24
|
+
target_path: str,
|
|
25
|
+
connectivity = None,
|
|
26
|
+
eval_mask = None,
|
|
27
|
+
index= None,
|
|
28
|
+
mask = None,
|
|
29
|
+
covariates= None,
|
|
30
|
+
input_map = None,
|
|
31
|
+
target_map = None,
|
|
32
|
+
auxiliary_map = None,
|
|
33
|
+
scalers = None,
|
|
34
|
+
trend = None,
|
|
35
|
+
transform = None,
|
|
36
|
+
window: int = 12,
|
|
37
|
+
stride: int = 1,
|
|
38
|
+
window_lag: int = 1,
|
|
39
|
+
precision: Union[int, str] = 32,
|
|
40
|
+
name: Optional[str] = None):
|
|
41
|
+
target, connectivity_default, scaler = self._load_from_parquet(target_path)
|
|
42
|
+
self.scaler = scaler
|
|
43
|
+
if eval_mask is None:
|
|
44
|
+
eval_mask = torch.zeros_like(torch.from_numpy(target))
|
|
45
|
+
if connectivity is None:
|
|
46
|
+
connectivity = connectivity_default
|
|
47
|
+
super(DIWDataset, self).__init__(target,
|
|
48
|
+
eval_mask,
|
|
49
|
+
index=index,
|
|
50
|
+
mask=None,
|
|
51
|
+
connectivity=connectivity,
|
|
52
|
+
covariates=covariates,
|
|
53
|
+
input_map=input_map,
|
|
54
|
+
target_map=target_map,
|
|
55
|
+
auxiliary_map=auxiliary_map,
|
|
56
|
+
trend=trend,
|
|
57
|
+
transform=transform,
|
|
58
|
+
scalers=scalers,
|
|
59
|
+
window=window,
|
|
60
|
+
stride=stride,
|
|
61
|
+
precision=precision,
|
|
62
|
+
name=name)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _load_from_parquet(self, target_path):
|
|
67
|
+
df = pd.read_parquet(target_path)
|
|
68
|
+
df['node_id'] = df['node_id'].apply(lambda x: '_'.join(x.split('_')[0:2]))
|
|
69
|
+
df = df.sort_values(by=['node_id', 'Time']).reset_index(drop=True)
|
|
70
|
+
|
|
71
|
+
target_cols = ['X_Pos_Error', 'Y_Pos_Error', 'Z_Pos_Error']
|
|
72
|
+
|
|
73
|
+
all_cols = df.columns.tolist()
|
|
74
|
+
#remove_cols = ['Time', 'node_id', 'delta_X_Pos', 'delta_Y_Pos', 'delta_Z_Pos']
|
|
75
|
+
remove_cols = ['Time', 'node_id', 'delta_X_Pos', 'delta_Y_Pos',
|
|
76
|
+
'delta_Z_Pos', 'folder.id', 'layer.num', 'shape',
|
|
77
|
+
'on.off', 'velocity', 'z_height', 'acceleration']
|
|
78
|
+
predictor_cols = [col for col in all_cols if col not in target_cols + remove_cols]
|
|
79
|
+
|
|
80
|
+
ids = df['node_id'].unique()
|
|
81
|
+
|
|
82
|
+
id_prefixes = [id.split('_')[0] for id in ids]
|
|
83
|
+
id_layer = [int(id.split('_')[1]) for id in ids]
|
|
84
|
+
|
|
85
|
+
df['Timestep'] = df.groupby('node_id').cumcount() + 1
|
|
86
|
+
|
|
87
|
+
df_max_timesteps = df.groupby('node_id')['Timestep'].max().reset_index(drop=True)
|
|
88
|
+
min_max_timestep = df_max_timesteps.min()
|
|
89
|
+
|
|
90
|
+
df = df[df['Timestep'] <= min_max_timestep]
|
|
91
|
+
df = df.drop(columns=['Time'])
|
|
92
|
+
|
|
93
|
+
all_cols = target_cols + predictor_cols
|
|
94
|
+
|
|
95
|
+
df_pivot = df.pivot_table(index='Timestep', columns='node_id', values=all_cols)
|
|
96
|
+
df_pivot = df_pivot[all_cols]
|
|
97
|
+
df_pivot = df_pivot.reorder_levels([1, 0], axis=1)
|
|
98
|
+
values = df_pivot.values
|
|
99
|
+
|
|
100
|
+
num_timesteps = df['Timestep'].nunique()
|
|
101
|
+
num_ids = df['node_id'].nunique()
|
|
102
|
+
num_features = len(all_cols)
|
|
103
|
+
|
|
104
|
+
node_ftr = values.reshape(num_timesteps, num_features, num_ids)
|
|
105
|
+
node_ftr = np.moveaxis(node_ftr, 1, 2)
|
|
106
|
+
|
|
107
|
+
adj_matrix = np.zeros((len(ids), len(ids)))
|
|
108
|
+
|
|
109
|
+
for i, id1 in enumerate(ids):
|
|
110
|
+
for j, id2 in enumerate(ids):
|
|
111
|
+
# if id_prefixes[i] == id_prefixes[j] and i != j and id_layer[i] % 2 == id_layer[j] % 2:
|
|
112
|
+
if id_prefixes[i] == id_prefixes[j] and i != j:
|
|
113
|
+
adj_matrix[i, j] = 1
|
|
114
|
+
|
|
115
|
+
scaler = MinMaxScaler(axis=(0))
|
|
116
|
+
scaler.fit(node_ftr)
|
|
117
|
+
node_ftr = scaler.transform(node_ftr)
|
|
118
|
+
|
|
119
|
+
return node_ftr, adj_matrix, scaler
|