Compare commits
No commits in common. "master" and "feature-positioning" have entirely different histories.
master
...
feature-po
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,5 +1,4 @@
|
||||
**/__pycache__/
|
||||
*.pyc
|
||||
/__pycache__
|
||||
/tck_venv
|
||||
/.venv
|
||||
/.vscode
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
{
|
||||
"act_pos_decrease": [
|
||||
5.0
|
||||
],
|
||||
"act_vel_min": [
|
||||
1900.0
|
||||
],
|
||||
"act_vel_close": [
|
||||
1700.0
|
||||
],
|
||||
"act_vel_thresh": [
|
||||
10.0
|
||||
],
|
||||
"act_vel_negative": [
|
||||
0.0
|
||||
],
|
||||
"rob_vel_thresh": [
|
||||
4.0
|
||||
],
|
||||
"act_force_close": [
|
||||
700.0
|
||||
],
|
||||
"act_force_weld": [
|
||||
1300.0
|
||||
],
|
||||
"force_increase": [
|
||||
200.0
|
||||
],
|
||||
"force_decrease": [
|
||||
80.0
|
||||
]
|
||||
}
|
||||
@ -64,36 +64,36 @@
|
||||
0.001
|
||||
],
|
||||
"distance_l_1": [
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005
|
||||
],
|
||||
"distance_l_2": [
|
||||
0.0275,
|
||||
0.0255,
|
||||
0.0242,
|
||||
0.0245,
|
||||
0.0228,
|
||||
0.0236,
|
||||
0.0229,
|
||||
0.0248,
|
||||
0.024,
|
||||
0.0235,
|
||||
0.025,
|
||||
0.0276,
|
||||
0.0234,
|
||||
0.0215
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625
|
||||
],
|
||||
"distance_h_end1": [
|
||||
0.003,
|
||||
@ -127,6 +127,22 @@
|
||||
0.005,
|
||||
0.005
|
||||
],
|
||||
"object_position": [
|
||||
0.02743,
|
||||
0.0249,
|
||||
0.024,
|
||||
0.02425,
|
||||
0.0225,
|
||||
0.0233,
|
||||
0.0226,
|
||||
0.02442,
|
||||
0.02357,
|
||||
0.02321,
|
||||
0.02464,
|
||||
0.02745,
|
||||
0.02321,
|
||||
0.021348
|
||||
],
|
||||
"time_wielding": [
|
||||
1.332,
|
||||
1.644,
|
||||
@ -160,20 +176,20 @@
|
||||
0.0
|
||||
],
|
||||
"time_robot_movement": [
|
||||
0.516,
|
||||
0.492,
|
||||
0.636,
|
||||
0.492,
|
||||
0.42,
|
||||
0.54,
|
||||
0.444,
|
||||
0.66,
|
||||
0.521,
|
||||
0.557,
|
||||
0.51,
|
||||
0.51,
|
||||
0.534,
|
||||
0.0
|
||||
0.314,
|
||||
0.331,
|
||||
0.356,
|
||||
0.428,
|
||||
0.418,
|
||||
0.454,
|
||||
0.458,
|
||||
0.44,
|
||||
0.49,
|
||||
0.47,
|
||||
0.44,
|
||||
0.425,
|
||||
0.464,
|
||||
0.5
|
||||
],
|
||||
"object_thickness": [
|
||||
0.0045,
|
||||
@ -222,5 +238,85 @@
|
||||
500.0,
|
||||
500.0,
|
||||
500.0
|
||||
],
|
||||
"Tesla closing": [
|
||||
0.216,
|
||||
0.228,
|
||||
0.252,
|
||||
0.216,
|
||||
0.228,
|
||||
0.216,
|
||||
0.228,
|
||||
0.228,
|
||||
0.228,
|
||||
0.216,
|
||||
0.228,
|
||||
0.216,
|
||||
0.216,
|
||||
0.216
|
||||
],
|
||||
"Tesla squeeze": [
|
||||
0.276,
|
||||
0.288,
|
||||
0.264,
|
||||
0.264,
|
||||
0.276,
|
||||
0.276,
|
||||
0.312,
|
||||
0.276,
|
||||
0.24,
|
||||
0.24,
|
||||
0.24,
|
||||
0.24,
|
||||
0.24,
|
||||
0.24
|
||||
],
|
||||
"Tesla welding": [
|
||||
1.332,
|
||||
1.644,
|
||||
1.644,
|
||||
1.428,
|
||||
1.284,
|
||||
1.308,
|
||||
1.272,
|
||||
1.38,
|
||||
1.416,
|
||||
1.392,
|
||||
1.38,
|
||||
1.404,
|
||||
1.452,
|
||||
1.452
|
||||
],
|
||||
"Tesla oncomming_relief": [
|
||||
0.528,
|
||||
0.528,
|
||||
0.54,
|
||||
0.636,
|
||||
0.504,
|
||||
0.468,
|
||||
0.492,
|
||||
0.54,
|
||||
0.563,
|
||||
0.588,
|
||||
0.541,
|
||||
0.564,
|
||||
0.576,
|
||||
0.507
|
||||
],
|
||||
"Tesla summary time": [
|
||||
2.34,
|
||||
2.652,
|
||||
2.796,
|
||||
2.4,
|
||||
2.208,
|
||||
2.34,
|
||||
2.256,
|
||||
2.544,
|
||||
2.405,
|
||||
2.405,
|
||||
2.358,
|
||||
2.37,
|
||||
2.442,
|
||||
1.908
|
||||
]
|
||||
}
|
||||
@ -1,325 +0,0 @@
|
||||
{
|
||||
"Online Path Scanner": {
|
||||
|
||||
"Electrode Force, N & Welding Current, kA": {
|
||||
"Settings": {
|
||||
"zoom": false,
|
||||
"stages": true,
|
||||
"performance": true,
|
||||
"ideals": true,
|
||||
"mirror ME": false,
|
||||
"workpiece": false,
|
||||
"force compensation FE": false,
|
||||
"force accuracy":true
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Electrode Force, N ME",
|
||||
"pen": "r"
|
||||
},
|
||||
{
|
||||
"name": "Electrode Force, N FE",
|
||||
"pen": "w"
|
||||
}
|
||||
],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Force",
|
||||
"pen": {"color": "g", "width":3}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
"Electrode Position, mm": {
|
||||
"Settings": {
|
||||
"zoom": false,
|
||||
"stages": true,
|
||||
"performance": false,
|
||||
"ideals": true,
|
||||
"mirror ME": true,
|
||||
"workpiece": true,
|
||||
"force compensation FE": true,
|
||||
"force accuracy":false
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Rotor Position, mm ME",
|
||||
"pen": {"color": "r", "width":2}
|
||||
},
|
||||
{
|
||||
"name": "Rotor Position, mm FE",
|
||||
"pen": {"color": "w", "width":2}
|
||||
}
|
||||
],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Position ME",
|
||||
"pen": {"color": "g", "width":4}
|
||||
},
|
||||
{
|
||||
"name": "Position FE",
|
||||
"pen": {"color": "b", "width":4}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
"Electrode Speed, mm/s": {
|
||||
"Settings": {
|
||||
"zoom": false,
|
||||
"stages": true,
|
||||
"performance": false,
|
||||
"ideals": true,
|
||||
"mirror ME": false,
|
||||
"workpiece": false,
|
||||
"force compensation FE": false,
|
||||
"force accuracy":false
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Rotor Speed, mm/s ME",
|
||||
"pen": "r",
|
||||
"zoom": false
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed, mm/s FE",
|
||||
"pen": "w",
|
||||
"zoom": false
|
||||
}
|
||||
],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Rotor Speed ME",
|
||||
"pen": {"color": "g", "width":3},
|
||||
"zoom": false
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed FE",
|
||||
"pen": {"color": "b", "width":3},
|
||||
"zoom": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Report Editor": {
|
||||
|
||||
"Electrode Force, N & Welding Current, kA": {
|
||||
"Settings": {
|
||||
"zoom": false,
|
||||
"stages": true,
|
||||
"performance": true,
|
||||
"ideals": true,
|
||||
"mirror ME": false,
|
||||
"workpiece": false,
|
||||
"force compensation FE": false,
|
||||
"force accuracy":true
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Electrode Force, N ME",
|
||||
"pen": "r"
|
||||
},
|
||||
{
|
||||
"name": "Electrode Force, N FE",
|
||||
"pen": "w"
|
||||
}
|
||||
],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Force",
|
||||
"pen": {"color": "g", "width":3}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
"Electrode Position, mm": {
|
||||
"Settings": {
|
||||
"zoom": false,
|
||||
"stages": true,
|
||||
"performance": false,
|
||||
"ideals": true,
|
||||
"mirror ME": true,
|
||||
"workpiece": true,
|
||||
"force compensation FE": true,
|
||||
"force accuracy":false
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Rotor Position, mm ME",
|
||||
"pen": {"color": "r", "width":2}
|
||||
},
|
||||
{
|
||||
"name": "Rotor Position, mm FE",
|
||||
"pen": {"color": "w", "width":2}
|
||||
}
|
||||
],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Position ME",
|
||||
"pen": {"color": "g", "width":4}
|
||||
},
|
||||
{
|
||||
"name": "Position FE",
|
||||
"pen": {"color": "b", "width":4}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
"Electrode Speed, mm/s": {
|
||||
"Settings": {
|
||||
"zoom": false,
|
||||
"stages": true,
|
||||
"performance": false,
|
||||
"ideals": true,
|
||||
"mirror ME": false,
|
||||
"workpiece": false,
|
||||
"force compensation FE": false,
|
||||
"force accuracy":false
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Rotor Speed, mm/s ME",
|
||||
"pen": "r",
|
||||
"zoom": false
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed, mm/s FE",
|
||||
"pen": "w",
|
||||
"zoom": false
|
||||
}
|
||||
],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Rotor Speed ME",
|
||||
"pen": {"color": "g", "width":3},
|
||||
"zoom": false
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed FE",
|
||||
"pen": {"color": "b", "width":3},
|
||||
"zoom": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Client Trace Watcher":{
|
||||
|
||||
"Client Trace": {
|
||||
"Settings": {
|
||||
"zoom": false,
|
||||
"stages": true
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Electrode Force, N ME",
|
||||
"pen": "r"
|
||||
},
|
||||
{
|
||||
"name": "Electrode Position, mm ME",
|
||||
"pen": "g"
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed, deg/s ME",
|
||||
"pen": "b"
|
||||
},
|
||||
{
|
||||
"name": "Cartesian Tool Speed, mm/s ME",
|
||||
"pen": "w"
|
||||
}
|
||||
],
|
||||
"Ideal_signals": []
|
||||
},
|
||||
|
||||
"TCW Trace": {
|
||||
"Settings": {
|
||||
"stages": true,
|
||||
"ideals": true,
|
||||
"mirror ME": true,
|
||||
"workpiece": true,
|
||||
"force compensation FE": true
|
||||
},
|
||||
"Real_signals": [],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Force",
|
||||
"pen": {"color": "r", "width":4}
|
||||
},
|
||||
{
|
||||
"name": "Position ME",
|
||||
"pen": {"color": "g", "width":4}
|
||||
},
|
||||
{
|
||||
"name": "Position FE",
|
||||
"pen": {"color": "b", "width":4}
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed ME",
|
||||
"pen": {"color": "w", "width":3},
|
||||
"zoom": false
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed FE",
|
||||
"pen": {"color": "w", "width":3},
|
||||
"zoom": false
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
|
||||
"Combined Trace": {
|
||||
"Settings": {
|
||||
"stages": true,
|
||||
"performance": false,
|
||||
"ideals": true,
|
||||
"workpiece": false
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Electrode Force, N ME",
|
||||
"pen": "r"
|
||||
},
|
||||
{
|
||||
"name": "Electrode Position, mm ME",
|
||||
"pen": "g"
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed, deg/s ME",
|
||||
"pen": "b"
|
||||
},
|
||||
{
|
||||
"name": "Cartesian Tool Speed, mm/s ME",
|
||||
"pen": "w"
|
||||
}
|
||||
],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Force",
|
||||
"pen": {"color": "r", "width":4}
|
||||
},
|
||||
{
|
||||
"name": "Position ME",
|
||||
"pen": {"color": "g", "width":4}
|
||||
},
|
||||
{
|
||||
"name": "Position FE",
|
||||
"pen": {"color": "b", "width":4}
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed ME",
|
||||
"pen": {"color": "w", "width":3},
|
||||
"zoom": false
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed FE",
|
||||
"pen": {"color": "w", "width":3},
|
||||
"zoom": false
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"trace_storage_path": [
|
||||
"/home/leonid/PycharmProjects/WeldingSpotPerformance/trace_samples"
|
||||
"D:/downloads/a22"
|
||||
],
|
||||
"monitor_update_period": [
|
||||
1000.0
|
||||
@ -21,7 +21,7 @@
|
||||
270.0
|
||||
],
|
||||
"mass_2": [
|
||||
1.0
|
||||
270.0
|
||||
],
|
||||
"k_hardness_1": [
|
||||
2148570.0
|
||||
@ -33,22 +33,22 @@
|
||||
13.8
|
||||
],
|
||||
"torque_max_2": [
|
||||
0.0
|
||||
13.8
|
||||
],
|
||||
"transmission_ratio_1": [
|
||||
0.00125
|
||||
],
|
||||
"transmission_ratio_2": [
|
||||
1.0
|
||||
0.00125
|
||||
],
|
||||
"contact_distance_1": [
|
||||
0.02
|
||||
0.0
|
||||
],
|
||||
"contact_distance_2": [
|
||||
0.081
|
||||
0.0
|
||||
],
|
||||
"k_prop": [
|
||||
0.075
|
||||
0.005
|
||||
],
|
||||
"time_capture": [
|
||||
1000.0
|
||||
@ -56,19 +56,7 @@
|
||||
"UML_time_scaler": [
|
||||
1000.0
|
||||
],
|
||||
"Range ME, mm": [
|
||||
115.0
|
||||
],
|
||||
"client_time": [
|
||||
35.488
|
||||
],
|
||||
"performance_mode": [
|
||||
"client"
|
||||
],
|
||||
"time_before_start": [
|
||||
0.924
|
||||
],
|
||||
"time_after_end": [
|
||||
1.14
|
||||
"gun_range": [
|
||||
112.5
|
||||
]
|
||||
}
|
||||
@ -4,7 +4,7 @@ cycler==0.12.1
|
||||
fonttools==4.54.1
|
||||
httplib2==0.22.0
|
||||
kiwisolver==1.4.7
|
||||
loguru==0.7.3
|
||||
loguru==0.7.2
|
||||
matplotlib==3.9.2
|
||||
numpy==2.1.2
|
||||
packaging==24.1
|
||||
@ -21,3 +21,4 @@ pytz==2024.2
|
||||
scipy==1.14.1
|
||||
six==1.16.0
|
||||
tzdata==2024.2
|
||||
watchdog==6.0.0
|
||||
|
||||
@ -16,12 +16,6 @@ class OptAlgorithm(AutoConfigClass):
|
||||
|
||||
calc = OptTimeCalculator(operator_config, system_config)
|
||||
|
||||
if self.distance_l_1 < 0:
|
||||
raise Exception("Ошибка - заготовка вышла за пределы маскимального раскрытия электрода FE")
|
||||
|
||||
if self.distance_l_2 < 0:
|
||||
raise Exception("Ошибка - заготовка вышла за пределы маскимального раскрытия электрода ME")
|
||||
|
||||
self.Ts = calc.T(self.distance_h_start_1,
|
||||
self.distance_h_start_2,
|
||||
self.distance_s_1,
|
||||
@ -227,8 +221,7 @@ class OptAlgorithm(AutoConfigClass):
|
||||
def FMovement(self, t: float):
|
||||
x1 = self.X1Movement(t)
|
||||
x2 = self.X2Movement(t)
|
||||
#its not true, but theres no need to track collisions at this point
|
||||
F = 0#self.k_hardness_1 * max(0, (x1 - self.x1_contact))
|
||||
F = self.k_hardness_1 * max(0, (x1 - self.x1_contact))
|
||||
return F
|
||||
|
||||
def X1Movement(self, t: float):
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import numpy as np
|
||||
from numpy import sqrt, arcsin, arccos, cos, sin, array
|
||||
from numpy import sqrt, arcsin, arccos, cos, sin
|
||||
|
||||
from OptAlgorithm.AutoConfigClass import AutoConfigClass
|
||||
from OptAlgorithm.ConstantCalculator import ConstantCalculator
|
||||
@ -22,8 +21,8 @@ class OptTimeCalculator(AutoConfigClass):
|
||||
v0q = min(sqrt(2 * self.a_max_1 * h1), self.v_max_1)
|
||||
v0 = min(v0q, sqrt(1 / (self.k_hardness_1 * self.mass_1)) * self.Ftogrow)
|
||||
t1 = v0 / self.a_max_1
|
||||
hleft = max(0, h1 - (self.a_max_1 * t1 * t1 / 2))
|
||||
t3 = min(max(0, v0q - v0)/ self.a_max_1, sqrt(self.a_max_1 * hleft))
|
||||
hleft = (h1 - (self.a_max_1 * t1 * t1 / 2))
|
||||
t3 = min((v0q - v0)/ self.a_max_1, sqrt(self.a_max_1 * hleft))
|
||||
v1 = (t1 + t3) * self.a_max_1
|
||||
hleft2 = h1 - (self.a_max_1 * (t1+t3) * (t1+t3) / 2) - v1 * t3 + t3**2 * self.a_max_1 / 2
|
||||
t2t = max(0, hleft2 / v1)
|
||||
@ -87,7 +86,7 @@ class OptTimeCalculator(AutoConfigClass):
|
||||
v1 = topen_1_acc * self.a_max_1
|
||||
if s1 > topen_1_speed * v1:
|
||||
s1 -= topen_1_speed * v1
|
||||
topen_1_mark = 2 * topen_1_acc + topen_1_speed - sqrt(max(0,topen_1_acc ** 2 - 2 * s1 / self.a_max_1))
|
||||
topen_1_mark = 2 * topen_1_acc + topen_1_speed - sqrt(topen_1_acc ** 2 - 2 * s1 / self.a_max_1)
|
||||
else:
|
||||
topen_1_mark = topen_1_acc + s1 / v1
|
||||
|
||||
@ -97,7 +96,7 @@ class OptTimeCalculator(AutoConfigClass):
|
||||
v2 = topen_2_acc * self.a_max_2
|
||||
if s2 > topen_2_speed * v2:
|
||||
s2 -= topen_2_speed * v2
|
||||
topen_2_mark = 2 * topen_2_acc + topen_2_speed - sqrt(max(0,topen_2_acc ** 2 - 2 * s2 / self.a_max_2))
|
||||
topen_2_mark = 2 * topen_2_acc + topen_2_speed - sqrt(topen_2_acc ** 2 - 2 * s2 / self.a_max_2)
|
||||
else:
|
||||
topen_2_mark = topen_2_acc + s2 / v2
|
||||
|
||||
@ -109,7 +108,7 @@ class OptTimeCalculator(AutoConfigClass):
|
||||
v0 = self.allTimes["tclose_1_acc"] * self.a_max_1
|
||||
vF0 = v0 * self.k_hardness_1
|
||||
|
||||
vFmax = min(self.v_max_1 * self.k_hardness_1, sqrt(self.k_hardness_1 / self.mass_1) * self.Ftogrow)
|
||||
vFmax = min(self.v_max_1 * self.k_hardness_1, sqrt(self.k_hardness_1 / (self.mass_1)) * self.Ftogrow)
|
||||
|
||||
L = sqrt(self.k_hardness_1 / self.mass_1 * self.eff_control ** 2 + vF0 * vF0)
|
||||
tspeed = sqrt(self.mass_1 / self.k_hardness_1) * (
|
||||
@ -160,78 +159,56 @@ class OptTimeCalculator(AutoConfigClass):
|
||||
Tfull = self.time_robot_movement
|
||||
|
||||
L = self.__dict__["distance_l_" + str(i)]
|
||||
maxL = L + x0
|
||||
maxL = contact[i - 1] - L - x0
|
||||
self.Tmovementi(i, x, Tfull, v0, maxL)
|
||||
return pos0s, v0s
|
||||
|
||||
def Tmovementi(self, i, Sfull, Tfull, v0, maxL) -> None:
|
||||
v0 = abs(v0)
|
||||
vmax = self.__dict__["v_max_" + str(i)]
|
||||
|
||||
if vmax < v0 - self.check_eps:
|
||||
raise Exception("Ошибка - скорость в начале перемещения больше макимальной - проверьте скорость")
|
||||
a = self.__dict__["a_max_" + str(i)]
|
||||
Sfull = -Sfull
|
||||
t3 = (Tfull + v0 / a) / 2
|
||||
|
||||
time_eps = 1e-8
|
||||
T = 0
|
||||
if Sfull > 0:
|
||||
S1_min = max(Sfull,v0*v0 / (2*a)) + 1e-7
|
||||
sqrtval = a ** 2 * (
|
||||
a ** 2 * (Tfull + 2 * t3) ** 2 - 8 * a * Sfull + 2 * a * v0 * (Tfull + 2 * t3) - 3 * v0 ** 2)
|
||||
if sqrtval < 0:
|
||||
raise Exception("""Невозможно с S_{i} добраться но H*_{i} за указанное время,
|
||||
проверьте distance_s_{i}, distance_h_end{i}, time_command, time_robot_movement""")
|
||||
t1max = ((Tfull + 2 * t3) + v0 / a) / (2) - sqrt(sqrtval) * sqrt(2) / (4 * a ** 2)
|
||||
t1 = min(t1max, (vmax - abs(v0)) / a)
|
||||
t1Llimit = -v0 / a + sqrt(v0 ** 2 / (a ** 2) + (abs(maxL) - v0 * v0 / a) / a)
|
||||
t1 = max(0, min(t1, t1Llimit))
|
||||
t31 = v0 / a + t1
|
||||
v1 = v0 + a * t1
|
||||
|
||||
#try to push t2 to limit S3_2-5
|
||||
maxiter = 10
|
||||
t2 = 0
|
||||
for j in range(maxiter):
|
||||
S1 = v0 * t1 + a * t1 * t1 / 2 + v1 * t31 - a * t31 * t31 / 2 + t2 * v1
|
||||
S2max = Sfull + S1
|
||||
t5max = sqrt(S2max / a)
|
||||
t5 = min(t5max, (vmax) / a)
|
||||
t32 = t5
|
||||
t3 = t31 + t32
|
||||
|
||||
|
||||
v1 = abs(v0 + t1 * a)
|
||||
v3 = abs(v0 + t1 * a - t3 * a)
|
||||
timeleft = Tfull - t1 - t5 - t3
|
||||
sq = -v0 * t1 - a * t1 ** 2 / 2 - v1 * t3 + a * t3 ** 2 / 2 + v3 * t5 - a * t5 ** 2 / 2
|
||||
Sleft = Sfull - sq
|
||||
|
||||
t2max = (timeleft - Sleft / v3) / (1 + v1 / v3)
|
||||
Smovement = -v0 * t1 - a / 2 * t1 ** 2 - v1 * t31 + a / 2 * t31 ** 2
|
||||
if v1 == 0:
|
||||
t2 = 0
|
||||
else:
|
||||
S1_min = v0*v0 / (2*a) + 1e-7
|
||||
S1_max = maxL
|
||||
if S1_min > S1_max:
|
||||
raise Exception("Ошибка - невозможно перемещение робота из-за слишком большого оффсета следующей заготовки - не хватает максимального раскрытия")
|
||||
# calc for S1
|
||||
def calc_Tmovement_for_S1(S1):
|
||||
S2 = -Sfull + S1
|
||||
if S2 < 0:
|
||||
raise Exception("Ошибка - прохождение второй части перемещения - отрицательно, неизвестное поведение")
|
||||
t2 = max(0, min(t2max, (abs(maxL) - abs(Smovement)) / v1))
|
||||
t4 = max(0, Sleft / v3 + v1 / v3 * t2)
|
||||
|
||||
t1_theory = -v0 / a + sqrt(v0 * v0 / (2 * a * a) + S1 / a)
|
||||
t1 = min(t1_theory, max(0, vmax - v0) / a)
|
||||
v2 = v0 + t1 * a
|
||||
t31 = v2 / a
|
||||
S1_fact = v0 * t1 + a * t1 ** 2 / 2 + v2 * t31 - a * t31 ** 2 / 2
|
||||
t2 = max(0, (S1 - S1_fact) / v2)
|
||||
T1 = t1 + t31 + t2
|
||||
tstay = max(0, Tfull - t1 - t2 - t3 - t4 - t5)
|
||||
|
||||
t32_theory = sqrt(S2 / a)
|
||||
t32 = min(t32_theory, (vmax) / a)
|
||||
v4 = t32 * a
|
||||
t5 = t32
|
||||
S2_fact = a * t32 ** 2 / 2 + v4 * t5 - a * t5 ** 2 / 2
|
||||
t4 = max(0, (S2 - S2_fact) / v4)
|
||||
T2 = t32 + t4 + t5
|
||||
T = T1 + T2
|
||||
return T, (t1, t2, t31, t32, t4, t5)
|
||||
|
||||
T_min, _ = calc_Tmovement_for_S1(S1_min)
|
||||
if T_min > Tfull:
|
||||
raise Exception(f"""Ошибка - время перемещения слишком мало, чтобы хотя бы закончить раскрытие, проверьте скорость, ускорение, время перемещения робота """)
|
||||
T_max, _ = calc_Tmovement_for_S1(S1_max)
|
||||
if T_max < Tfull:
|
||||
S1 = S1_max
|
||||
else:
|
||||
maxiter = 20
|
||||
cur_iter = 0
|
||||
while abs(T - Tfull) > time_eps and S1_max > S1_min and cur_iter < maxiter:
|
||||
S1_cur = (S1_min + S1_max)/2
|
||||
T, _ = calc_Tmovement_for_S1(S1_cur)
|
||||
if T > Tfull:
|
||||
S1_max = S1_cur
|
||||
else:
|
||||
S1_min = S1_cur
|
||||
cur_iter += 1
|
||||
S1 = S1_min
|
||||
T, tarray = calc_Tmovement_for_S1(S1)
|
||||
tstay = max(0, Tfull - T)
|
||||
|
||||
t1, t2, t31, t32, t4, t5 = tarray
|
||||
for j, t in enumerate(tarray):
|
||||
if t < 0:
|
||||
raise Exception(f"""Ошибка - время перехода во время фазы {j + 1} {i}-го электрода отрицательно - переход невозможен с такими параметрами,
|
||||
проверьте скорость, ускорение, смещение """)
|
||||
self.allTimes["tmovement_" + str(i) + "_acc"] = t1
|
||||
self.allTimes["tmovement_" + str(i) + "_speed"] = t2
|
||||
self.allTimes["tmovement_" + str(i) + "_slow"] = t31
|
||||
@ -249,7 +226,7 @@ class OptTimeCalculator(AutoConfigClass):
|
||||
v0 = min(v0q, sqrt(1 / (self.k_hardness_1 * self.mass_1)) * self.Ftogrow)
|
||||
t2 = T - sqrt(max(0, T ** 2 - 2 * s / self.a_max_1))
|
||||
if t2 * self.a_max_1 < v0:
|
||||
#we should wait to end with max speed but dont do it
|
||||
#we should wait to end with max speed
|
||||
t2 = v0 / self.a_max_1
|
||||
t3 = max(0, (s - self.a_max_1 * t2 ** 2 / 2) / (self.a_max_1 * t2))
|
||||
t1 = T - t2 - t3
|
||||
|
||||
BIN
src/OptAlgorithm/__pycache__/AutoConfigClass.cpython-310.pyc
Normal file
BIN
src/OptAlgorithm/__pycache__/AutoConfigClass.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/OptAlgorithm/__pycache__/ConstantCalculator.cpython-310.pyc
Normal file
BIN
src/OptAlgorithm/__pycache__/ConstantCalculator.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/OptAlgorithm/__pycache__/OptAlgorithm.cpython-310.pyc
Normal file
BIN
src/OptAlgorithm/__pycache__/OptAlgorithm.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/OptAlgorithm/__pycache__/OptTimeCalculator.cpython-310.pyc
Normal file
BIN
src/OptAlgorithm/__pycache__/OptTimeCalculator.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/OptAlgorithm/__pycache__/PhaseCalc.cpython-310.pyc
Normal file
BIN
src/OptAlgorithm/__pycache__/PhaseCalc.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/OptAlgorithm/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
src/OptAlgorithm/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
src/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/main.cpython-310.pyc
Normal file
BIN
src/__pycache__/main.cpython-310.pyc
Normal file
Binary file not shown.
751
src/base/base.py
751
src/base/base.py
@ -1,751 +0,0 @@
|
||||
from __future__ import annotations
|
||||
import os
|
||||
from typing import Optional, Union, List, Tuple, Dict, Any
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from cachetools import LRUCache
|
||||
import pandas as pd
|
||||
from PyQt5.QtCore import QObject, QTimer
|
||||
from PyQt5.QtWidgets import QWidget, QTabWidget, QMainWindow, QVBoxLayout
|
||||
|
||||
from OptAlgorithm import OptAlgorithm
|
||||
from utils.qt_settings import dark_style
|
||||
|
||||
|
||||
# ========= Дата-классы для хранения данных =========
|
||||
|
||||
@dataclass
|
||||
class KukaTXT:
|
||||
"""
|
||||
Класс для представления текстовых данных Кука.
|
||||
"""
|
||||
time: float = 0
|
||||
endtime: float = 0
|
||||
func: str = ""
|
||||
type_: str = ""
|
||||
signal: str = ""
|
||||
# Дополнительные поля (закомментированы) можно добавить при необходимости
|
||||
# module: str
|
||||
# line: int = 0
|
||||
# point_name: str = ""
|
||||
# ...
|
||||
|
||||
|
||||
@dataclass
|
||||
class KukaDataHead:
|
||||
"""
|
||||
Класс, описывающий заголовок данных Кука.
|
||||
"""
|
||||
rob_ID: int = 0
|
||||
filename: str = ""
|
||||
channels: dict = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlotItems:
|
||||
"""
|
||||
Класс для хранения элементов графика: регионы, кривые и QT-виджеты.
|
||||
"""
|
||||
regions: dict = field(default_factory=dict)
|
||||
curves: dict = field(default_factory=dict)
|
||||
qt_items: dict = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PointPassport:
|
||||
"""
|
||||
Паспорт точки, содержащий временной интервал, события,
|
||||
идеальные данные и полезную информацию для этой точки.
|
||||
"""
|
||||
timeframe: List[Any] = field(default_factory=list)
|
||||
events: Dict = field(default_factory=dict)
|
||||
ideal_data: Dict = field(default_factory=dict)
|
||||
useful_data: Dict = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PerformanceData:
|
||||
client_to_TWC: float = 0
|
||||
client_to_ideal: float = 0
|
||||
TWC_to_ideal: float = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class UsefulGraphData:
|
||||
"""
|
||||
Полезные данные для графика.
|
||||
|
||||
:param client_time: Время, рассчитанное для клиента.
|
||||
:param range_ME: Диапазон для ME.
|
||||
:param k_hardness: Коэффициент твердости.
|
||||
"""
|
||||
performance: PerformanceData = field(default_factory=PerformanceData)
|
||||
range_ME: float = 0
|
||||
k_hardness: float = 0
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class GraphicPassport:
|
||||
"""
|
||||
Графический паспорт, объединяющий DataFrame с данными,
|
||||
список паспортов точек и полезные данные для построения графика.
|
||||
"""
|
||||
dataframe: pd.DataFrame = field(default_factory=pd.DataFrame)
|
||||
points_pocket: List[PointPassport] = field(default_factory=list)
|
||||
useful_data: UsefulGraphData = field(default_factory=UsefulGraphData)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Settings:
|
||||
"""
|
||||
Настройки приложения, разделённые на категории:
|
||||
- operator: настройки, связанные с оператором,
|
||||
- system: системные настройки,
|
||||
- filter: настройки фильтрации трейса клиента для определения событий.
|
||||
"""
|
||||
operator: Dict = field(default_factory=dict)
|
||||
system: Dict = field(default_factory=dict)
|
||||
filter: Dict = field(default_factory=dict)
|
||||
|
||||
|
||||
# ========= Базовые классы для взаимодействия компонентов =========
|
||||
|
||||
class BaseMediator:
|
||||
"""
|
||||
Базовый класс медиатора для организации взаимодействия между компонентами приложения.
|
||||
|
||||
Медиатор связывает такие модули, как конвертер данных, формирователь паспортов,
|
||||
виджет построения графиков, контроллер, файловый менеджер и процессор трассировки.
|
||||
"""
|
||||
def __init__(self,
|
||||
converter: BaseDataConverter,
|
||||
passport_former: BasePointPassportFormer,
|
||||
plot: BasePlotWidget,
|
||||
controller: BaseController,
|
||||
file_manager: BaseFileManager,
|
||||
trace_processor: BaseRawTraceProcessor):
|
||||
self._converter = converter
|
||||
self._converter.mediator = self
|
||||
self._passport_former = passport_former
|
||||
self._passport_former.mediator = self
|
||||
self._plot = plot
|
||||
self._plot.mediator = self
|
||||
self._controller = controller
|
||||
self._controller.mediator = self
|
||||
self._file_manager = file_manager
|
||||
self._file_manager.mediator = self
|
||||
self._trace_processor = trace_processor
|
||||
self._trace_processor.mediator = self
|
||||
|
||||
def notify(self,
|
||||
source: Union[
|
||||
BaseDirectoryMonitor,
|
||||
BaseDataConverter,
|
||||
BasePointPassportFormer,
|
||||
BasePlotWidget,
|
||||
BaseRawTraceProcessor
|
||||
],
|
||||
data: Union[
|
||||
List[str],
|
||||
List[pd.DataFrame],
|
||||
List[list],
|
||||
List[QWidget],
|
||||
pd.DataFrame
|
||||
]) -> None:
|
||||
"""
|
||||
Получает уведомление от компонента-источника и распределяет данные по соответствующим модулям.
|
||||
"""
|
||||
...
|
||||
|
||||
def prerender_TCW(self,
|
||||
source: Union[BaseController, BaseFileManager, BaseRawTraceProcessor],
|
||||
data: Union[str, List[str], List[pd.DataFrame, dict]]) -> None:
|
||||
"""
|
||||
Выполняет операции для отображения предварительных данных клиента.
|
||||
"""
|
||||
...
|
||||
|
||||
def render_TCW(self,
|
||||
source: Union[BaseController, BaseRawTraceProcessor],
|
||||
data: Union[str, List[pd.DataFrame, dict]]) -> None:
|
||||
"""
|
||||
Выполняет операции для отображения обработанных данных клиента и ТСК.
|
||||
"""
|
||||
...
|
||||
|
||||
def set_mode(self, mode: int) -> None:
|
||||
"""
|
||||
Устанавливает режим работы.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class BaseDirectoryMonitor:
|
||||
"""
|
||||
Базовый класс для мониторинга директории.
|
||||
|
||||
Использует QTimer для периодической проверки содержимого директории.
|
||||
"""
|
||||
update_timer = QTimer()
|
||||
|
||||
def __init__(self, file_manager: Optional[BaseFileManager] = None):
|
||||
self._directory_path: Optional[str] = None
|
||||
self._update_time: Optional[float] = None
|
||||
self.isActive: bool = False
|
||||
self._files: List[str] = []
|
||||
self._file_manager = file_manager
|
||||
|
||||
@property
|
||||
def directory_path(self) -> str:
|
||||
return self._directory_path
|
||||
|
||||
@property
|
||||
def update_time(self) -> int:
|
||||
return self._update_time
|
||||
|
||||
@property
|
||||
def files(self) -> List[str]:
|
||||
return self._files
|
||||
|
||||
@property
|
||||
def file_manager(self) -> BaseFileManager:
|
||||
return self._file_manager
|
||||
|
||||
@file_manager.setter
|
||||
def file_manager(self, file_manager: BaseFileManager) -> None:
|
||||
self._file_manager = file_manager
|
||||
|
||||
def init_state(self) -> None:
|
||||
"""
|
||||
Инициализирует состояние мониторинга, считывая список файлов из директории.
|
||||
"""
|
||||
files = os.listdir(self._directory_path)
|
||||
self._files = files
|
||||
|
||||
def start(self) -> None:
|
||||
"""
|
||||
Запускает мониторинг директории, устанавливая флаг активности и запуская таймер.
|
||||
"""
|
||||
self.isActive = True
|
||||
self.update_timer.start(int(self._update_time))
|
||||
|
||||
def stop(self) -> None:
|
||||
"""
|
||||
Останавливает мониторинг директории.
|
||||
"""
|
||||
self.isActive = False
|
||||
self.update_timer.stop()
|
||||
|
||||
def pause(self) -> None:
|
||||
"""
|
||||
Приостанавливает мониторинг директории (останавливает таймер).
|
||||
"""
|
||||
self.update_timer.stop()
|
||||
|
||||
|
||||
class BaseDataConverter:
|
||||
"""
|
||||
Базовый класс для конвертации данных.
|
||||
"""
|
||||
def __init__(self, mediator: Optional[BaseMediator] = None):
|
||||
self._mediator = mediator
|
||||
|
||||
@property
|
||||
def mediator(self) -> BaseMediator:
|
||||
return self._mediator
|
||||
|
||||
@mediator.setter
|
||||
def mediator(self, mediator: BaseMediator) -> None:
|
||||
self._mediator = mediator
|
||||
|
||||
def convert_data(self, files: List[str]) -> None:
|
||||
"""
|
||||
Конвертирует данные из списка файлов.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class BasePlotWidget:
|
||||
"""
|
||||
Базовый класс для виджета построения графиков.
|
||||
"""
|
||||
def __init__(self, mediator: Optional[BaseMediator] = None, controller: BaseController = None):
|
||||
self._mediator: BaseMediator = mediator
|
||||
self._controller: BaseController = controller
|
||||
self._datalen: int = 0
|
||||
self._datastep: int = 0
|
||||
self._stage_colors = {
|
||||
"Closing": [220, 20, 60, 100], # Crimson
|
||||
"Squeeze": [30, 144, 255, 100], # Dodger Blue
|
||||
"Welding": [128, 128, 128, 100], # Gray
|
||||
"Relief": [34, 139, 34, 100], # Forest Green
|
||||
"Oncomming": [255, 165, 0, 100] # Orange
|
||||
}
|
||||
self._plt_channels = None
|
||||
|
||||
@staticmethod
|
||||
def set_style(obj: Union[QTabWidget, QWidget]) -> None:
|
||||
"""
|
||||
Устанавливает стиль для переданного объекта.
|
||||
"""
|
||||
obj.setStyleSheet(
|
||||
"""QLabel {
|
||||
color: #ffffff;
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
}"""
|
||||
)
|
||||
|
||||
@property
|
||||
def controller(self) -> BaseController:
|
||||
return self._controller
|
||||
|
||||
@property
|
||||
def mediator(self) -> BaseMediator:
|
||||
return self._mediator
|
||||
|
||||
@mediator.setter
|
||||
def mediator(self, mediator: BaseMediator) -> None:
|
||||
self._mediator = mediator
|
||||
|
||||
def build(self, data: List[pd.DataFrame]) -> List[QWidget]:
|
||||
"""
|
||||
Строит графики на основе списка DataFrame.
|
||||
"""
|
||||
...
|
||||
|
||||
def build_raw_trace(self, data: pd.DataFrame) -> None:
|
||||
"""
|
||||
Строит график трейсов клиента с предварительным разбиением на этапы.
|
||||
"""
|
||||
...
|
||||
|
||||
def set_mode(self, mode: int) -> None:
|
||||
"""
|
||||
Устанавливает режим отображения графиков.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class BaseController(QObject):
|
||||
"""
|
||||
Базовый класс контроллера, отвечающий за обновление интерфейса и взаимодействие с медиатором.
|
||||
"""
|
||||
def __init__(self, mediator: Optional[BaseMediator] = None, file_manager: Optional[BaseFileManager] = None):
|
||||
super().__init__()
|
||||
self._mediator = mediator
|
||||
self._file_manager = file_manager
|
||||
|
||||
def send_widgets(self, widgets: List[QWidget]) -> None:
|
||||
"""
|
||||
Отправляет список виджетов для дальнейшей обработки.
|
||||
"""
|
||||
...
|
||||
|
||||
def update_settings(self, settings: List[Dict]) -> None:
|
||||
"""
|
||||
Обновляет настройки приложения.
|
||||
"""
|
||||
...
|
||||
|
||||
def set_working_mode(self, mode: int) -> None:
|
||||
"""
|
||||
Устанавливает рабочий режим приложения.
|
||||
"""
|
||||
...
|
||||
|
||||
def open_file(self, filepath: str) -> None:
|
||||
"""
|
||||
Открывает файл по заданному пути.
|
||||
"""
|
||||
...
|
||||
|
||||
def open_dir(self, dirpath: str) -> None:
|
||||
"""
|
||||
Открывает директорию.
|
||||
"""
|
||||
...
|
||||
|
||||
def update_plots(self) -> None:
|
||||
"""
|
||||
Обновляет графики.
|
||||
"""
|
||||
...
|
||||
|
||||
def update_status(self, msg: str) -> None:
|
||||
"""
|
||||
Обновляет статусное сообщение в интерфейсе.
|
||||
"""
|
||||
...
|
||||
|
||||
def update_progress(self, progress: int) -> None:
|
||||
"""
|
||||
Обновляет значение прогресс-бара.
|
||||
"""
|
||||
...
|
||||
|
||||
@property
|
||||
def mediator(self) -> BaseMediator:
|
||||
return self._mediator
|
||||
|
||||
@mediator.setter
|
||||
def mediator(self, mediator: BaseMediator) -> None:
|
||||
self._mediator = mediator
|
||||
|
||||
@property
|
||||
def file_manager(self) -> BaseFileManager:
|
||||
return self._file_manager
|
||||
|
||||
@file_manager.setter
|
||||
def file_manager(self, file_manager: BaseFileManager) -> None:
|
||||
self._file_manager = file_manager
|
||||
|
||||
|
||||
class BaseFileManager:
|
||||
"""
|
||||
Базовый класс файлового менеджера.
|
||||
"""
|
||||
def __init__(self, mediator: Optional[BaseMediator] = None, monitor: Optional[BaseDirectoryMonitor] = None):
|
||||
self._mediator = mediator
|
||||
self._monitor = monitor
|
||||
self._paths_library: set = set()
|
||||
self._mode: int = 0
|
||||
|
||||
@property
|
||||
def paths_library(self) -> set:
|
||||
return self._paths_library
|
||||
|
||||
@property
|
||||
def mediator(self) -> BaseMediator:
|
||||
return self._mediator
|
||||
|
||||
@mediator.setter
|
||||
def mediator(self, mediator: BaseMediator) -> None:
|
||||
self._mediator = mediator
|
||||
|
||||
@property
|
||||
def monitor(self) -> BaseDirectoryMonitor:
|
||||
return self._monitor
|
||||
|
||||
@monitor.setter
|
||||
def monitor(self, monitor: BaseDirectoryMonitor) -> None:
|
||||
self._monitor = monitor
|
||||
|
||||
def replot_all(self) -> None:
|
||||
"""
|
||||
Переотрисовывает все данные, используя текущую библиотеку путей.
|
||||
"""
|
||||
...
|
||||
|
||||
def open_custom_file(self, path: str) -> None:
|
||||
"""
|
||||
Открывает пользовательский файл по заданному пути.
|
||||
"""
|
||||
...
|
||||
|
||||
def open_raw_traces_dir(self, path: str) -> None:
|
||||
"""
|
||||
Открывает директорию с сырыми трассировочными данными.
|
||||
"""
|
||||
...
|
||||
|
||||
def set_mode(self, num: int) -> None:
|
||||
"""
|
||||
Устанавливает режим работы файлового менеджера.
|
||||
"""
|
||||
...
|
||||
|
||||
def update_monitor_settings(self, settings: List[Dict]) -> None:
|
||||
"""
|
||||
Обновляет настройки мониторинга директории.
|
||||
"""
|
||||
...
|
||||
|
||||
def add_new_paths(self, paths: List[str]) -> None:
|
||||
"""
|
||||
Добавляет новые пути в библиотеку, если их ранее не было.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class BaseIdealDataBuilder(OptAlgorithm):
|
||||
"""
|
||||
Базовый класс для построения идеальных данных, расширяющий функциональность OptAlgorithm.
|
||||
|
||||
При инициализации на основе настроек устанавливаются множитель времени и время сварки.
|
||||
"""
|
||||
def __init__(self, settings: Settings):
|
||||
self.mul = settings.system['time_capture']
|
||||
self.welding_time = settings.operator['time_wielding']
|
||||
self._settings = settings
|
||||
super().__init__(settings.system, settings.operator)
|
||||
|
||||
def get_closingDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа закрытия.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_compressionDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа сжатия.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_openingDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа открытия.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_tmovementDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа движения.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_weldingDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа сварки.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_oncomingDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа движения (oncomming).
|
||||
"""
|
||||
...
|
||||
|
||||
def get_ideal_timings(self) -> Tuple[float, float, float, float]:
|
||||
"""
|
||||
Получает идеальные временные интервалы для этапов.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_cycle_time(self) -> float:
|
||||
"""
|
||||
Вычисляет общее время цикла как сумму идеальных временных интервалов.
|
||||
"""
|
||||
result = sum(self.get_ideal_timings())
|
||||
return result
|
||||
|
||||
|
||||
class BaseMainWindow(QMainWindow):
|
||||
"""
|
||||
Базовое главное окно приложения.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.resize(200, 200)
|
||||
# Создаем центральный виджет и устанавливаем его
|
||||
self._central_widget = QWidget()
|
||||
self.setCentralWidget(self._central_widget)
|
||||
# Устанавливаем основной вертикальный макет для центрального виджета
|
||||
self._central_layout = QVBoxLayout()
|
||||
self._central_widget.setLayout(self._central_layout)
|
||||
self.set_style(self)
|
||||
...
|
||||
|
||||
def set_style(self, obj: Union[QTabWidget, QWidget, QMainWindow]) -> None:
|
||||
"""
|
||||
Устанавливает стиль для переданного объекта.
|
||||
"""
|
||||
obj.setStyleSheet(dark_style)
|
||||
|
||||
|
||||
class BasePointPassportFormer:
|
||||
"""
|
||||
Базовый класс для формирования паспортов точек.
|
||||
|
||||
Содержит базовые настройки, список этапов, кэш для идеальных данных и набор параметров для алгоритма.
|
||||
"""
|
||||
def __init__(self, mediator: Optional[BaseMediator] = None):
|
||||
self._mediator = mediator
|
||||
self._clear_stage: str = "Welding"
|
||||
self._settings = Settings()
|
||||
self._stages: List[str] = [
|
||||
"Closing",
|
||||
"Squeeze",
|
||||
"Welding",
|
||||
"Relief",
|
||||
"Oncomming"
|
||||
]
|
||||
self._tesla_stages: List[str] = [
|
||||
"Tesla squeeze",
|
||||
"Tesla closing",
|
||||
"Tesla welding",
|
||||
"Tesla oncomming_relief"
|
||||
]
|
||||
self._ideal_data_cache = LRUCache(maxsize=1000)
|
||||
self._OptAlgorithm_operator_params = [
|
||||
"dist_open_start_1",
|
||||
"dist_open_start_2",
|
||||
"dist_open_after_1",
|
||||
"dist_open_after_2",
|
||||
"dist_open_end_1",
|
||||
"dist_open_end_2",
|
||||
"dist_close_end_1",
|
||||
"dist_close_end_2",
|
||||
"time_command",
|
||||
"time_robot_movement",
|
||||
"object_thickness",
|
||||
"force_target",
|
||||
"force_capture",
|
||||
"time_wielding"
|
||||
]
|
||||
self._OptAlgorithm_system_params = [
|
||||
"a_max_1",
|
||||
"v_max_1",
|
||||
"a_max_2",
|
||||
"v_max_2",
|
||||
"mass_1",
|
||||
"mass_2",
|
||||
"k_hardness_1",
|
||||
"k_hardness_2",
|
||||
"torque_max_1",
|
||||
"torque_max_2",
|
||||
"transmission_ratio_1",
|
||||
"transmission_ratio_2",
|
||||
"position_start_1",
|
||||
"position_start_2",
|
||||
"k_prop",
|
||||
"time_capture"
|
||||
]
|
||||
|
||||
def form_passports(self, data: List[pd.DataFrame]) -> None:
|
||||
"""
|
||||
Формирует паспорта для набора данных.
|
||||
"""
|
||||
...
|
||||
|
||||
def form_customer_passport(self, data: List[Union[pd.DataFrame, dict]]) -> None:
|
||||
"""
|
||||
Формирует паспорт заказчика на основе DataFrame и событий.
|
||||
"""
|
||||
...
|
||||
|
||||
def update_settings(self, params: List) -> None:
|
||||
"""
|
||||
Обновляет настройки для формирования паспортов.
|
||||
"""
|
||||
...
|
||||
|
||||
@property
|
||||
def opt_algorithm(self) -> BaseIdealDataBuilder:
|
||||
return self._opt_algorithm
|
||||
|
||||
@opt_algorithm.setter
|
||||
def opt_algorithm(self, opt_algorithm: BaseIdealDataBuilder) -> None:
|
||||
self._opt_algorithm = opt_algorithm
|
||||
|
||||
@property
|
||||
def mediator(self) -> BaseMediator:
|
||||
return self._mediator
|
||||
|
||||
@mediator.setter
|
||||
def mediator(self, mediator: BaseMediator) -> None:
|
||||
self._mediator = mediator
|
||||
|
||||
|
||||
class BaseRawTraceProcessor:
|
||||
"""
|
||||
Базовый класс для обработки сырых трассировочных данных.
|
||||
"""
|
||||
def __init__(self,
|
||||
dataparser: BaseKukaDataParser,
|
||||
textparser: BaseKukaTextParser,
|
||||
data_detector: BaseTraceStageDetector,
|
||||
text_detector: BaseTextStageDetector,
|
||||
mediator: Optional[BaseMediator] = None):
|
||||
self._mediator = mediator
|
||||
self._dataparser = dataparser
|
||||
self._textparser = textparser
|
||||
self._data_detector = data_detector
|
||||
self._text_detector = text_detector
|
||||
self._trace_df: Optional[pd.DataFrame] = None
|
||||
self._text_data: Optional[Any] = None
|
||||
|
||||
def prerender(self, data: List[str]) -> None:
|
||||
"""
|
||||
Выполняет предварительный рендеринг данных.
|
||||
"""
|
||||
...
|
||||
|
||||
def final_render(self) -> None:
|
||||
"""
|
||||
Выполняет финальный рендеринг данных.
|
||||
"""
|
||||
...
|
||||
|
||||
def update_settings(self, data: Settings) -> None:
|
||||
"""
|
||||
Обновляет настройки обработки трассировочных данных.
|
||||
"""
|
||||
...
|
||||
|
||||
@property
|
||||
def mediator(self) -> BaseMediator:
|
||||
return self._mediator
|
||||
|
||||
@mediator.setter
|
||||
def mediator(self, mediator: BaseMediator) -> None:
|
||||
self._mediator = mediator
|
||||
|
||||
|
||||
class BaseKukaDataParser:
|
||||
"""
|
||||
Базовый класс для парсинга данных Кука.
|
||||
"""
|
||||
def __init__(self):
|
||||
self._ch_name: Optional[str] = None
|
||||
|
||||
def parse(self, head_path: str) -> pd.DataFrame:
|
||||
"""
|
||||
Парсит файл заголовка и возвращает DataFrame с данными.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class BaseKukaTextParser:
|
||||
"""
|
||||
Базовый класс для парсинга текстовых данных Кука.
|
||||
"""
|
||||
def __init__(self):
|
||||
self._in_msg = None
|
||||
self._datapacks = None
|
||||
|
||||
def parse(self, path: str) -> List[KukaTXT]:
|
||||
"""
|
||||
Парсит текстовый файл и возвращает список объектов KukaTXT.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class BaseTraceStageDetector:
|
||||
"""
|
||||
Базовый класс для детекции стадий на основе данных трассировки.
|
||||
"""
|
||||
def __init__(self, parent: Optional[BaseRawTraceProcessor] = None):
|
||||
self._parent = parent
|
||||
|
||||
def detect_stages(self, df: pd.DataFrame) -> List:
|
||||
"""
|
||||
Детектирует стадии процесса по данным DataFrame.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class BaseTextStageDetector:
|
||||
"""
|
||||
Базовый класс для детекции сварочных стадий на основе текстовых данных.
|
||||
"""
|
||||
def __init__(self, parent: Optional[BaseRawTraceProcessor] = None):
|
||||
self._parent = parent
|
||||
|
||||
def detect_welding(self, data: List[KukaTXT]) -> List:
|
||||
"""
|
||||
Детектирует этап сварки по текстовым данным.
|
||||
"""
|
||||
...
|
||||
BIN
src/controller/__pycache__/controller.cpython-310.pyc
Normal file
BIN
src/controller/__pycache__/controller.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/controller/__pycache__/converter.cpython-310.pyc
Normal file
BIN
src/controller/__pycache__/converter.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/controller/__pycache__/ideal_data_builder.cpython-310.pyc
Normal file
BIN
src/controller/__pycache__/ideal_data_builder.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/controller/__pycache__/mediator.cpython-310.pyc
Normal file
BIN
src/controller/__pycache__/mediator.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/controller/__pycache__/monitor.cpython-310.pyc
Normal file
BIN
src/controller/__pycache__/monitor.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/controller/__pycache__/monitor.cpython-311.pyc
Normal file
BIN
src/controller/__pycache__/monitor.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/controller/__pycache__/passportFormer.cpython-310.pyc
Normal file
BIN
src/controller/__pycache__/passportFormer.cpython-310.pyc
Normal file
Binary file not shown.
@ -1,102 +1,43 @@
|
||||
from typing import Tuple
|
||||
from PyQt5.QtWidgets import QWidget, QTabWidget
|
||||
from PyQt5.QtGui import QPixmap
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from typing import Union
|
||||
|
||||
from base.base import BaseController, Settings
|
||||
from utils.base.base import BaseController
|
||||
|
||||
|
||||
class Controller(BaseController):
|
||||
"""
|
||||
Контроллер приложения.
|
||||
|
||||
Отвечает за управление сигналами для обновления интерфейса (виджеты, статус, прогресс),
|
||||
установку рабочего режима, а также делегирует действия таким модулям, как файловый менеджер,
|
||||
медиатор и т.д.
|
||||
"""
|
||||
|
||||
signal_widgets = pyqtSignal(list)
|
||||
signal_progress_bar = pyqtSignal(int)
|
||||
signal_status_text = pyqtSignal(str)
|
||||
|
||||
def set_working_mode(self, mode: int) -> None:
|
||||
"""
|
||||
Устанавливает рабочий режим приложения через медиатор.
|
||||
|
||||
:param mode: Режим работы (целое число).
|
||||
"""
|
||||
self._mediator.set_mode(mode)
|
||||
|
||||
def update_plots(self) -> None:
|
||||
"""
|
||||
Инициирует переотрисовку графиков через файловый менеджер.
|
||||
"""
|
||||
self._file_manager.replot_all()
|
||||
signal_settings = pyqtSignal(list)
|
||||
signal_open_file = pyqtSignal(str)
|
||||
signal_statusBar = pyqtSignal(int)
|
||||
signal_statusText = pyqtSignal(str)
|
||||
signal_raport_mode = pyqtSignal()
|
||||
signal_seeking_mode = pyqtSignal()
|
||||
signal_update_plots = pyqtSignal()
|
||||
|
||||
def send_widgets(self, widgets: list[QWidget]) -> None:
|
||||
"""
|
||||
Отправляет список виджетов для дальнейшего использования.
|
||||
|
||||
:param widgets: Список виджетов.
|
||||
"""
|
||||
self.signal_widgets.emit(widgets)
|
||||
|
||||
def update_settings(self, settings: Settings) -> None:
|
||||
"""
|
||||
Обновляет настройки приложения, передавая их через медиатор.
|
||||
def update_settings(self, settings: list[dict]) -> None:
|
||||
self.signal_settings.emit(settings)
|
||||
|
||||
:param settings: Объект настроек.
|
||||
"""
|
||||
self._mediator.notify(self, settings)
|
||||
def raport_mode(self) -> None:
|
||||
self.signal_raport_mode.emit()
|
||||
|
||||
def update_status(self, msg: str) -> None:
|
||||
"""
|
||||
Обновляет статусное сообщение в интерфейсе.
|
||||
|
||||
:param msg: Текст сообщения.
|
||||
"""
|
||||
self.signal_status_text.emit(msg)
|
||||
|
||||
def update_progress(self, progress: int) -> None:
|
||||
"""
|
||||
Обновляет значение прогресс-бара.
|
||||
|
||||
:param progress: Значение прогресса (от 0 до 100).
|
||||
"""
|
||||
self.signal_progress_bar.emit(progress)
|
||||
def seeking_mode(self) -> None:
|
||||
self.signal_seeking_mode.emit()
|
||||
|
||||
def open_file(self, filepath: str) -> None:
|
||||
"""
|
||||
Открывает файл по указанному пути через файловый менеджер.
|
||||
self.signal_open_file.emit(filepath)
|
||||
|
||||
:param filepath: Путь к файлу.
|
||||
"""
|
||||
self._file_manager.open_custom_file(filepath)
|
||||
def update_plots(self) -> None:
|
||||
self.signal_update_plots.emit()
|
||||
|
||||
def open_dir(self, dirpath: str) -> None:
|
||||
"""
|
||||
Инициирует предварительный рендеринг данных из директории.
|
||||
def update_status(self, msg: Union[str, float, int]) -> None:
|
||||
if type(msg) == float or type(msg) == int:
|
||||
self.signal_statusBar.emit(int(msg))
|
||||
else:
|
||||
self.signal_statusText.emit(msg)
|
||||
|
||||
:param dirpath: Путь к директории.
|
||||
"""
|
||||
self._mediator.prerender_TCW(self, dirpath)
|
||||
|
||||
def build_TCW_for_client(self) -> None:
|
||||
"""
|
||||
Выполняет финальный рендеринг TCW (Trace Control Widget) для клиента.
|
||||
"""
|
||||
self._mediator.render_TCW(self)
|
||||
|
||||
def save_file(self, data: Tuple[str, QTabWidget]) -> None:
|
||||
"""
|
||||
Сохраняет снимок содержимого вкладки в указанный файл.
|
||||
|
||||
:param data: Кортеж, содержащий путь к файлу и QTabWidget для сохранения.
|
||||
"""
|
||||
filepath, tab = data
|
||||
# Создаем QPixmap с размером, равным размеру вкладки
|
||||
pixmap = QPixmap(tab.size())
|
||||
# Рендерим содержимое вкладки в QPixmap
|
||||
tab.render(pixmap)
|
||||
# Сохраняем изображение по указанному пути
|
||||
pixmap.save(filepath)
|
||||
|
||||
@ -1,104 +1,35 @@
|
||||
import pandas as pd
|
||||
import traceback
|
||||
import sys
|
||||
from loguru import logger
|
||||
|
||||
from base.base import BaseDataConverter
|
||||
|
||||
# FIXME: костыль для выключения предупреждения "replace deprecated"
|
||||
pd.set_option('future.no_silent_downcasting', True)
|
||||
|
||||
from utils.base.base import BaseDataConverter
|
||||
|
||||
|
||||
class DataConverter(BaseDataConverter):
|
||||
"""
|
||||
Класс для преобразования данных из CSV-файлов.
|
||||
|
||||
Основные задачи:
|
||||
- Исправление заголовков столбцов по заданному шаблону.
|
||||
- Преобразование булевых значений в целочисленные.
|
||||
- Обработка ошибок при конвертации и уведомление через медиатора.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _replace_bool(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
Преобразует столбцы, содержащие булевы значения, в целочисленные.
|
||||
|
||||
:param df: DataFrame с исходными данными.
|
||||
:return: DataFrame с преобразованными булевыми столбцами или None, если произошла ошибка.
|
||||
"""
|
||||
def _replace_bool(dataframe: pd.DataFrame) -> pd.DataFrame:
|
||||
try:
|
||||
# Находим столбцы, в которых все значения являются True или False
|
||||
bool_columns = df.columns[df.isin([True, False]).all()]
|
||||
# Приводим найденные столбцы к типу int
|
||||
df = df.astype({col: int for col in bool_columns})
|
||||
return df
|
||||
except AttributeError as e:
|
||||
logger.warning(f"_replace_bool - AttributeError: Проверьте, что переданный объект является DataFrame. {e}")
|
||||
return None
|
||||
except (TypeError, ValueError) as e:
|
||||
logger.error(f"_replace_bool - Ошибка типа или значения: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"_replace_bool - Непредвиденная ошибка: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _fix_headers(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
Приводит заголовки столбцов DataFrame к корректным именам согласно заранее заданному списку.
|
||||
|
||||
:param df: DataFrame с исходными заголовками.
|
||||
:return: DataFrame с исправленными заголовками или None, если произошла ошибка.
|
||||
"""
|
||||
correct_columns = [
|
||||
"time", "Closing", "Electrode Force, N FE", "Electrode Force, N ME",
|
||||
"Force Control FE", "Force Control ME", "Hold Position ME", "Oncomming",
|
||||
"Position Control FE", "Position Control ME", "Relief",
|
||||
"Rotor Position, mm FE", "Rotor Position, mm ME",
|
||||
"Rotor Speed, mm/s FE", "Rotor Speed, mm/s ME",
|
||||
"Squeeze", "Welding", "Welding Current ME", "Welding Voltage ME"
|
||||
]
|
||||
try:
|
||||
# Создаем словарь соответствий: ключ - имя в нижнем регистре, значение - корректное имя
|
||||
correct_mapping = {name.lower(): name for name in correct_columns}
|
||||
new_columns = []
|
||||
# Для каждого столбца заменяем имя, если оно присутствует в корректном отображении
|
||||
for col in df.columns:
|
||||
fixed_col = correct_mapping.get(col.lower(), col)
|
||||
new_columns.append(fixed_col)
|
||||
df.columns = new_columns
|
||||
# Удаляем повторяющиеся столбцы, оставляя первое вхождение
|
||||
df = df.loc[:, ~df.columns.duplicated()]
|
||||
return df
|
||||
except AttributeError as e:
|
||||
logger.warning(f"_fix_headers - AttributeError: Проверьте, что переданный объект является DataFrame. {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"_fix_headers - Непредвиденная ошибка: {e}")
|
||||
bool_columns = dataframe.columns[dataframe.isin([True, False]).any()]
|
||||
dataframe[bool_columns] = dataframe[bool_columns].replace({True: 1, False: 0})
|
||||
return dataframe
|
||||
except:
|
||||
return None
|
||||
|
||||
def convert_data(self, files: list[str]) -> None:
|
||||
"""
|
||||
Считывает данные из списка CSV-файлов, исправляет заголовки и преобразует булевые столбцы,
|
||||
после чего уведомляет медиатора о завершении преобразования.
|
||||
|
||||
:param files: Список путей к CSV-файлам.
|
||||
"""
|
||||
try:
|
||||
# Считываем данные из файлов (если путь не пустой, иначе None)
|
||||
dataframes = [pd.read_csv(file) if file != '' else None for file in files]
|
||||
# Применяем исправление заголовков к каждому DataFrame
|
||||
renamed_dataframes = list(map(self._fix_headers, dataframes))
|
||||
# Преобразуем булевые значения для каждого DataFrame
|
||||
converted_dataframes = list(map(self._replace_bool, renamed_dataframes))
|
||||
except FileNotFoundError as e:
|
||||
logger.error(f"convert_data - FileNotFoundError: Файл не найден. {e}")
|
||||
converted_dataframes = [None]
|
||||
except pd.errors.EmptyDataError as e:
|
||||
logger.error(f"convert_data - EmptyDataError: Файл пустой. {e}")
|
||||
converted_dataframes = [None]
|
||||
except Exception as e:
|
||||
logger.error(f"convert_data - Непредвиденная ошибка: {e}")
|
||||
converted_dataframes = list(map(self._replace_bool, dataframes))
|
||||
except:
|
||||
# Get the traceback object
|
||||
tb = sys.exc_info()[2]
|
||||
tbinfo = traceback.format_tb(tb)[0]
|
||||
pymsg = "Traceback info:\n" + tbinfo + "\nError Info:\n" + str(sys.exc_info()[1])
|
||||
logger.error(pymsg)
|
||||
converted_dataframes = [None]
|
||||
finally:
|
||||
# Передаем результат медиатору
|
||||
self._mediator.notify(self, converted_dataframes)
|
||||
|
||||
@ -1,189 +0,0 @@
|
||||
import os
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from base.base import BaseDirectoryMonitor, BaseFileManager, Settings
|
||||
|
||||
|
||||
class FileManager(BaseFileManager):
|
||||
"""
|
||||
Менеджер файлов для работы с библиотекой путей.
|
||||
|
||||
Основные функции:
|
||||
- Переотправка (replot) списка найденных файлов.
|
||||
- Добавление нового пути (например, выбранного пользователем).
|
||||
- Переключение режимов работы (создание отчёта, онлайн-мониторинг, работа с трейсами клиента).
|
||||
- Обновление настроек мониторинга.
|
||||
- Поиск файлов в директории с трейсами: поиск файлов .dat и .txt.
|
||||
"""
|
||||
|
||||
def replot_all(self) -> None:
|
||||
"""
|
||||
Переотправляет все пути из библиотеки.
|
||||
|
||||
Если текущий режим не равен 3, уведомляет медиатора со списком путей.
|
||||
Если режим равен 3, открывает директорию с трейсами.
|
||||
"""
|
||||
if self._paths_library is not None:
|
||||
if self._mode != 3:
|
||||
self._mediator.notify(self, list(self._paths_library))
|
||||
else:
|
||||
self.open_raw_traces_dir(list(self._paths_library)[0])
|
||||
|
||||
def open_custom_file(self, path: str) -> None:
|
||||
"""
|
||||
Добавляет указанный путь в библиотеку и уведомляет медиатора.
|
||||
|
||||
:param path: Путь к файлу, выбранному пользователем.
|
||||
"""
|
||||
self._paths_library.add(path)
|
||||
# Передаем уведомление с указанным путем (если путь пустой, передаем пустую строку)
|
||||
self._mediator.notify(self, [path if path else ''])
|
||||
|
||||
def set_mode(self, num: int) -> None:
|
||||
"""
|
||||
Устанавливает режим работы менеджера файлов.
|
||||
|
||||
Режимы:
|
||||
1 - Режим создания отчёта.
|
||||
2 - Режим онлайн-мониторинга папки.
|
||||
3 - Режим работы с трейсами клиента.
|
||||
|
||||
:param num: Целое число, определяющее режим работы.
|
||||
"""
|
||||
match num:
|
||||
case 1:
|
||||
# Режим создания отчёта
|
||||
self._monitor.stop()
|
||||
self._paths_library.clear()
|
||||
self._paths_library.add('')
|
||||
self._mediator.notify(self.monitor, list(self._paths_library))
|
||||
self._mode = 1
|
||||
case 2:
|
||||
# Режим онлайн-мониторинга папки
|
||||
self._monitor.init_state()
|
||||
self._monitor.start()
|
||||
self._mode = 2
|
||||
case 3:
|
||||
# Режим работы с трейсами клиента
|
||||
self._monitor.stop()
|
||||
self._paths_library.clear()
|
||||
self._mode = 3
|
||||
|
||||
def update_monitor_settings(self, settings: Settings) -> None:
|
||||
"""
|
||||
Обновляет настройки мониторинга директории.
|
||||
|
||||
Из объекта настроек извлекается путь к директории и интервал обновления.
|
||||
Выполняется проверка существования директории и корректности интервала обновления.
|
||||
При необходимости приостанавливается или возобновляется монитор.
|
||||
|
||||
:param settings: Объект Settings с системными настройками.
|
||||
"""
|
||||
directory_path = settings.system['trace_storage_path'][0]
|
||||
update_time = settings.system['monitor_update_period'][0]
|
||||
|
||||
if not os.path.exists(directory_path):
|
||||
logger.warning(f"Путь {directory_path} не существует.")
|
||||
# Можно раскомментировать raise, если требуется прерывание работы
|
||||
# raise FileNotFoundError(f"Путь {directory_path} не существует.")
|
||||
|
||||
if update_time <= 0.01:
|
||||
logger.warning(f"Интервал между проверками папки слишком мал: {update_time}")
|
||||
|
||||
if self._monitor.isActive:
|
||||
self._monitor.pause()
|
||||
self._monitor._directory_path = directory_path
|
||||
self._monitor._update_time = update_time
|
||||
if self._monitor.isActive:
|
||||
self._monitor.start()
|
||||
|
||||
def add_new_paths(self, paths: list) -> None:
|
||||
"""
|
||||
Добавляет новые пути файлов в библиотеку, если они ранее не присутствовали,
|
||||
и уведомляет медиатора о новых путях.
|
||||
|
||||
:param paths: Список новых путей.
|
||||
"""
|
||||
paths_set = set(paths)
|
||||
new_paths = self._paths_library.difference(paths_set)
|
||||
self._paths_library.update(new_paths)
|
||||
self._mediator.notify(self, list(new_paths))
|
||||
|
||||
def open_raw_traces_dir(self, path: str) -> None:
|
||||
"""
|
||||
Открывает указанную директорию, ищет в ней файлы с расширениями .dat и .txt,
|
||||
и при их наличии инициирует предварительный рендеринг через медиатора.
|
||||
|
||||
:param path: Путь к директории с трейсами.
|
||||
"""
|
||||
self._paths_library.clear()
|
||||
self._paths_library.add(path)
|
||||
dat_file, txt_file = None, None
|
||||
|
||||
# Перебираем содержимое директории
|
||||
for entry in os.listdir(path):
|
||||
full_path = os.path.join(path, entry)
|
||||
if os.path.isfile(full_path):
|
||||
_, ext = os.path.splitext(entry)
|
||||
ext = ext.lower()
|
||||
if ext == '.dat' and dat_file is None:
|
||||
dat_file = full_path
|
||||
elif ext == '.txt' and txt_file is None:
|
||||
txt_file = full_path
|
||||
if dat_file and txt_file:
|
||||
break
|
||||
|
||||
if dat_file and txt_file:
|
||||
# Уведомляем медиатора для запуска предварительного рендеринга трейс-контента
|
||||
self._mediator.prerender_TCW(self, [dat_file, txt_file])
|
||||
|
||||
|
||||
class DirectoryMonitor(BaseDirectoryMonitor):
|
||||
"""
|
||||
Мониторинг директории для поиска CSV-файлов.
|
||||
|
||||
Инициализирует состояние, подключает таймер обновления и отслеживает появление новых файлов.
|
||||
При обнаружении новых файлов уведомляет менеджер файлов.
|
||||
"""
|
||||
|
||||
def init_state(self):
|
||||
"""
|
||||
Инициализирует состояние мониторинга директории.
|
||||
|
||||
Проверяет, существует ли директория. Если директория не существует, генерирует ошибку.
|
||||
Собирает список файлов с расширением .csv и подключает метод мониторинга к таймеру.
|
||||
"""
|
||||
if not os.path.exists(self._directory_path):
|
||||
logger.error(f"Путь {self._directory_path} не существует.")
|
||||
raise FileNotFoundError(f"Путь {self._directory_path} не существует.")
|
||||
|
||||
self._files = [
|
||||
os.path.join(self._directory_path, file)
|
||||
for file in os.listdir(self._directory_path)
|
||||
if file.lower().endswith('.csv')
|
||||
]
|
||||
|
||||
self.update_timer.timeout.connect(self._monitor)
|
||||
logger.info("Monitor initiated!")
|
||||
|
||||
def _monitor(self):
|
||||
"""
|
||||
Метод, вызываемый по таймеру, для проверки появления новых CSV-файлов.
|
||||
|
||||
Если обнаружены новые файлы, уведомляет менеджер файлов и обновляет список файлов.
|
||||
Если в директории отсутствуют файлы, сбрасывает список.
|
||||
"""
|
||||
current_files = [
|
||||
os.path.join(self._directory_path, file)
|
||||
for file in os.listdir(self._directory_path)
|
||||
if file.lower().endswith('.csv')
|
||||
]
|
||||
# Определяем файлы, которых еще не было в предыдущем списке
|
||||
new_files = sorted(list(filter(lambda x: x not in self._files, current_files)))
|
||||
if new_files:
|
||||
logger.info(f"New files detected: {new_files}")
|
||||
self._file_manager.add_new_paths(new_files)
|
||||
self._files = current_files
|
||||
if not current_files:
|
||||
self._files = []
|
||||
@ -1,127 +1,41 @@
|
||||
from typing import Union
|
||||
|
||||
import pandas as pd
|
||||
import time
|
||||
|
||||
from typing import Union
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
|
||||
from base.base import (
|
||||
BaseMediator,
|
||||
BaseFileManager,
|
||||
BaseDataConverter,
|
||||
BasePlotWidget,
|
||||
BasePointPassportFormer,
|
||||
BaseController,
|
||||
GraphicPassport,
|
||||
Settings,
|
||||
BaseRawTraceProcessor
|
||||
)
|
||||
from utils.base.base import (BaseMediator, BaseDirectoryMonitor,
|
||||
BaseDataConverter, BasePlotWidget,
|
||||
BasePointPassportFormer)
|
||||
|
||||
|
||||
class Mediator(BaseMediator):
|
||||
"""
|
||||
Медиатор для организации взаимодействия между различными компонентами приложения.
|
||||
|
||||
Компоненты (менеджеры, конвертеры, контроллер, виджеты, процессоры) регистрируются
|
||||
и вызываются через медиатора, который распределяет уведомления и передаваемые данные.
|
||||
"""
|
||||
def notify(self,
|
||||
source: Union[BaseDirectoryMonitor, BaseDataConverter, BasePointPassportFormer, BasePlotWidget],
|
||||
data: Union[list[str], list[pd.DataFrame], list[list], list[QWidget]]):
|
||||
|
||||
def notify(
|
||||
self,
|
||||
source: Union[
|
||||
BaseFileManager,
|
||||
BaseDataConverter,
|
||||
BasePointPassportFormer,
|
||||
BasePlotWidget,
|
||||
BaseController,
|
||||
BaseRawTraceProcessor
|
||||
],
|
||||
data: Union[
|
||||
list[str],
|
||||
list[pd.DataFrame],
|
||||
list[GraphicPassport],
|
||||
list[QWidget],
|
||||
Settings,
|
||||
pd.DataFrame
|
||||
]
|
||||
) -> None:
|
||||
"""
|
||||
Принимает уведомление от компонента-источника и направляет данные в соответствующий модуль.
|
||||
|
||||
:param source: Компонент, вызвавший уведомление.
|
||||
:param data: Передаваемые данные (могут быть строками, DataFrame, графическими паспортами, виджетами, Settings).
|
||||
"""
|
||||
# Если источник – менеджер файлов: обновляем статус и запускаем конвертацию CSV
|
||||
if isinstance(source, BaseFileManager):
|
||||
self._controller.update_status("CSV found! Calculating...")
|
||||
if issubclass(source.__class__, BaseDirectoryMonitor):
|
||||
self.update_status("CSV found! Calculating...")
|
||||
self._converter.convert_data(data)
|
||||
|
||||
# Если источник – конвертер данных: обновляем прогресс и формируем паспорта
|
||||
if isinstance(source, BaseDataConverter):
|
||||
self._controller.update_progress(1)
|
||||
self._passport_former.form_passports(data)
|
||||
if issubclass(source.__class__, BaseDataConverter):
|
||||
self.update_status(0.5)
|
||||
self._passportFormer.form_passports(data)
|
||||
|
||||
# Если источник – формирователь паспортов точек: обновляем прогресс и строим графический паспорт
|
||||
if isinstance(source, BasePointPassportFormer):
|
||||
self._controller.update_progress(10)
|
||||
if issubclass(source.__class__, BasePointPassportFormer):
|
||||
self.update_status(1)
|
||||
self._plot.build(data)
|
||||
|
||||
# Если источник – виджет для построения графиков: завершаем построение и передаём виджеты контроллеру
|
||||
if isinstance(source, BasePlotWidget):
|
||||
self._controller.update_progress(100)
|
||||
if issubclass(source.__class__, BasePlotWidget):
|
||||
self.update_status(100)
|
||||
self._controller.send_widgets(data)
|
||||
|
||||
# Если источник – контроллер: обновляем настройки у различных модулей
|
||||
if isinstance(source, BaseController):
|
||||
self._file_manager.update_monitor_settings(data)
|
||||
self._passport_former.update_settings(data)
|
||||
self._trace_processor.update_settings(data)
|
||||
def update_settings(self, settings: list[dict]):
|
||||
self._monitor.update_settings(settings)
|
||||
self._passportFormer.update_settings(settings)
|
||||
|
||||
def prerender_TCW(
|
||||
self,
|
||||
source: Union[BaseController, BaseFileManager, BaseRawTraceProcessor],
|
||||
data: Union[str, list[str], list[pd.DataFrame], dict]
|
||||
) -> None:
|
||||
"""
|
||||
Выполняет предварительный рендеринг TCW (trace control widget) в зависимости от источника уведомления.
|
||||
def update_status(self, msg: Union[str, float]) -> None:
|
||||
self._controller.update_status(msg)
|
||||
|
||||
:param source: Компонент, инициирующий предварительный рендеринг.
|
||||
:param data: Передаваемые данные, могут быть строкой или списком путей/DataFrame и словарём.
|
||||
"""
|
||||
if isinstance(source, BaseController):
|
||||
self._controller.update_progress(5)
|
||||
self._file_manager.open_raw_traces_dir(data)
|
||||
|
||||
if isinstance(source, BaseFileManager):
|
||||
self._controller.update_progress(15)
|
||||
self._trace_processor.prerender(data)
|
||||
|
||||
if isinstance(source, BaseRawTraceProcessor):
|
||||
self._controller.update_progress(40)
|
||||
self._plot.build_raw_trace(data)
|
||||
|
||||
def render_TCW(
|
||||
self,
|
||||
source: Union[BaseController, BaseRawTraceProcessor],
|
||||
data: Union[str, list[pd.DataFrame], dict] = None
|
||||
) -> None:
|
||||
"""
|
||||
Выполняет финальный рендеринг TCW.
|
||||
|
||||
:param source: Компонент, инициирующий финальный рендеринг.
|
||||
:param data: Передаваемые данные (опционально).
|
||||
"""
|
||||
if isinstance(source, BaseController):
|
||||
self._controller.update_progress(5)
|
||||
self._trace_processor.final_render()
|
||||
|
||||
if isinstance(source, BaseRawTraceProcessor):
|
||||
self._controller.update_progress(5)
|
||||
self._passport_former.form_customer_passport(data)
|
||||
|
||||
def set_mode(self, mode: int) -> None:
|
||||
"""
|
||||
Устанавливает режим работы для графиков и файлового менеджера.
|
||||
|
||||
:param mode: Целое число, определяющее режим работы.
|
||||
"""
|
||||
self._plot.set_mode(mode)
|
||||
self._file_manager.set_mode(mode)
|
||||
|
||||
72
src/controller/monitor.py
Normal file
72
src/controller/monitor.py
Normal file
@ -0,0 +1,72 @@
|
||||
import os
|
||||
from loguru import logger
|
||||
|
||||
from utils.base.base import BaseDirectoryMonitor
|
||||
|
||||
|
||||
class DirectoryMonitor(BaseDirectoryMonitor):
|
||||
|
||||
def _init_state(self):
|
||||
|
||||
self._files = [
|
||||
os.path.join(self._directory_path, file)
|
||||
for file in os.listdir(self._directory_path)
|
||||
if file.lower().endswith('.csv')
|
||||
]
|
||||
|
||||
self.update_timer.timeout.connect(self._monitor)
|
||||
logger.info("Monitor initiated!")
|
||||
|
||||
def _monitor(self):
|
||||
current_files = [
|
||||
os.path.join(self._directory_path, file)
|
||||
for file in os.listdir(self._directory_path)
|
||||
if file.lower().endswith('.csv')
|
||||
]
|
||||
new_files = sorted(list(filter(lambda x: x not in self._files, current_files)))
|
||||
if new_files:
|
||||
logger.info(f"New files detected: {new_files}")
|
||||
self._mediator.notify(self, new_files)
|
||||
self._files = current_files
|
||||
if not current_files:
|
||||
self._files = []
|
||||
|
||||
def update_settings(self, data: list[dict]) -> None:
|
||||
if self.isActive:
|
||||
self.stop()
|
||||
_, system_params = data
|
||||
self._directory_path = system_params['trace_storage_path'][0]
|
||||
self._update_time = system_params['monitor_update_period'][0]
|
||||
|
||||
if not os.path.exists(self._directory_path):
|
||||
logger.error(f"Путь {self._directory_path} не существует.")
|
||||
raise FileNotFoundError(f"Путь {self._directory_path} не существует.")
|
||||
else:
|
||||
self._init_state()
|
||||
self.start()
|
||||
else:
|
||||
_, system_params = data
|
||||
self._directory_path = system_params['trace_storage_path'][0]
|
||||
self._update_time = system_params['monitor_update_period'][0]
|
||||
|
||||
def update_plots(self):
|
||||
if self._files is not None:
|
||||
self._mediator.notify(self, self._files)
|
||||
|
||||
def custom_csv_extract_only(self, path: str):
|
||||
self._files.append(path)
|
||||
if path is not None:
|
||||
self._mediator.notify(self, [path])
|
||||
else:
|
||||
self._mediator.notify(self, [''])
|
||||
|
||||
def start_raport(self) -> None:
|
||||
self.stop()
|
||||
self._files = ['']
|
||||
self._mediator.notify(self, [''])
|
||||
|
||||
|
||||
def start_seeking(self) -> None:
|
||||
self._init_state()
|
||||
self.start()
|
||||
|
||||
126
src/controller/passportFormer.py
Normal file
126
src/controller/passportFormer.py
Normal file
@ -0,0 +1,126 @@
|
||||
import pandas as pd
|
||||
import traceback
|
||||
import sys
|
||||
from loguru import logger
|
||||
|
||||
from utils.base.base import BasePointPassportFormer, BaseIdealDataBuilder
|
||||
|
||||
|
||||
class idealDataBuilder(BaseIdealDataBuilder):
|
||||
def get_closingDF(self) -> pd.DataFrame:
|
||||
return self._get_data(self.Ts['tclose'], self.calcPhaseClose)
|
||||
|
||||
def get_compressionDF(self) -> pd.DataFrame:
|
||||
return self._get_data(self.Ts['tgrow'], self.calcPhaseGrow)
|
||||
|
||||
def get_openingDF(self) -> pd.DataFrame:
|
||||
return self._get_data(self.getMarkOpen(), self.calcPhaseOpen)
|
||||
|
||||
def get_oncomingDF(self) -> pd.DataFrame:
|
||||
return self._get_data(self.Ts['tmovement'], self.calcPhaseMovement)
|
||||
|
||||
def get_weldingDF(self) -> pd.DataFrame:
|
||||
data = []
|
||||
X1, X2, V1, V2, F = self.calcPhaseGrow(self.Ts['tgrow']-0.0001)
|
||||
X1, X2, V1, V2 = X1*1000, X2*1000, V1*1000, V2*1000
|
||||
data.append({"time":0, "Position FE":X1,"Position ME":X2, "Rotor Speed FE":V1, "Rotor Speed ME":V2, "Force":F})
|
||||
data.append({"time":self.welding_time, "Position FE":X1,"Position ME":X2, "Rotor Speed FE":V1, "Rotor Speed ME":V2, "Force":F})
|
||||
return pd.DataFrame(data)
|
||||
|
||||
def get_ideal_timings(self) -> list[float]:
|
||||
data = self.Ts
|
||||
ideal_timings = [data['tclose'], data['tgrow'], self.welding_time, self.getMarkOpen(), data['tmovement']]
|
||||
return ideal_timings
|
||||
|
||||
class PassportFormer(BasePointPassportFormer):
|
||||
|
||||
def form_passports(self, data: list[pd.DataFrame]) -> list[list[pd.DataFrame, dict, int]]:
|
||||
try:
|
||||
return_data = [self._build_passports_pocket(dataframe) for dataframe in data]
|
||||
except:
|
||||
tb = sys.exc_info()[2]
|
||||
tbinfo = traceback.format_tb(tb)[0]
|
||||
pymsg = "Traceback info:\n" + tbinfo + "\nError Info:\n" + str(sys.exc_info()[1])
|
||||
logger.error(pymsg)
|
||||
return_data = []
|
||||
finally:
|
||||
self._mediator.notify(self, return_data)
|
||||
|
||||
|
||||
def _build_passports_pocket(self, dataframe: pd.DataFrame) -> list[pd.DataFrame, dict, int]:
|
||||
|
||||
if dataframe is not None:
|
||||
events, point_quantity = self._filter_events(dataframe["time"], dataframe)
|
||||
if point_quantity == 0:
|
||||
return []
|
||||
idx_shift = True if events[self._stages[-1]][0][0] == 0 else False
|
||||
else:
|
||||
events = None
|
||||
key = list(self._params[0].keys())[0]
|
||||
point_quantity = len(self._params[0][key])
|
||||
|
||||
points_pocket = []
|
||||
system_settings = {key: value[0] for key, value in self._params[1].items()}
|
||||
tesla_time = sum(self._params[0].get("Tesla summary time", []))
|
||||
useful_data = {
|
||||
"tesla_time": tesla_time,
|
||||
"range": system_settings["gun_range"],
|
||||
"k_hardness": system_settings["k_hardness_1"]
|
||||
}
|
||||
|
||||
for i in range(point_quantity):
|
||||
operator_settings = {
|
||||
key: (value[i] if i < len(value) else value[0])
|
||||
for key, value in self._params[0].items()
|
||||
}
|
||||
|
||||
next_i = i + 1
|
||||
|
||||
P1 = operator_settings["object_position"] + 0.5 * operator_settings["object_thickness"]
|
||||
P2 = operator_settings["object_position"] - 0.5 * operator_settings["object_thickness"]
|
||||
|
||||
if next_i < point_quantity:
|
||||
next_operator_settings = {
|
||||
key: (value[next_i] if i < len(value) else value[0])
|
||||
for key, value in self._params[0].items()
|
||||
}
|
||||
|
||||
next_P1 = next_operator_settings["object_position"] + 0.5*next_operator_settings["object_thickness"]
|
||||
next_P2 = next_operator_settings["object_position"] - 0.5*next_operator_settings["object_thickness"]
|
||||
|
||||
displacement_me = next_P1 - P1
|
||||
displacement_fe = next_P2 - P2
|
||||
|
||||
operator_settings["distance_h_end1"] -= displacement_fe
|
||||
operator_settings["distance_h_end2"] += displacement_me
|
||||
|
||||
LFEAbs = operator_settings["distance_l_1"]
|
||||
LMEAbs = operator_settings["distance_l_2"]
|
||||
|
||||
operator_settings["distance_l_1"] = P2 - LFEAbs
|
||||
operator_settings["distance_l_2"] = LMEAbs - P1
|
||||
|
||||
params_list = [operator_settings, system_settings]
|
||||
cache_key = self._generate_cache_key(params_list)
|
||||
if cache_key in self._ideal_data_cashe:
|
||||
ideal_data = self._ideal_data_cashe[cache_key]
|
||||
else:
|
||||
ideal_data = self._build_ideal_data(idealDataBuilder=idealDataBuilder, params=params_list)
|
||||
self._ideal_data_cashe[cache_key] = ideal_data
|
||||
if events is not None:
|
||||
idx = i+1 if idx_shift else i
|
||||
point_timeframe = [events[self._stages[0]][0][i], events[self._stages[-1]][1][idx]]
|
||||
point_events = {key: [value[0][i], value[1][i]] for key, value in events.items()}
|
||||
else:
|
||||
point_timeframe, point_events = None, None
|
||||
useful_p_data = {"thickness": operator_settings["object_thickness"],
|
||||
"position": operator_settings["object_position"]-0.5*operator_settings["object_thickness"],
|
||||
"force": operator_settings["force_target"],
|
||||
"P1": operator_settings["object_position"] + 0.5*operator_settings["object_thickness"],
|
||||
"P2": operator_settings["object_position"] - 0.5*operator_settings["object_thickness"]}
|
||||
|
||||
points_pocket.append([point_timeframe, ideal_data, point_events, useful_p_data])
|
||||
return dataframe, points_pocket, useful_data
|
||||
|
||||
def update_settings(self, params: list[dict, dict]):
|
||||
self._params = params
|
||||
@ -1,544 +0,0 @@
|
||||
from typing import Optional, Any, Tuple, List, Dict
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from loguru import logger
|
||||
|
||||
from base.base import (
|
||||
BasePointPassportFormer,
|
||||
BaseIdealDataBuilder,
|
||||
PointPassport,
|
||||
GraphicPassport,
|
||||
Settings,
|
||||
UsefulGraphData,
|
||||
PerformanceData
|
||||
)
|
||||
|
||||
|
||||
class PassportFormer(BasePointPassportFormer):
|
||||
"""
|
||||
Класс для формирования паспортов (графических и точечных) по данным трассировки.
|
||||
|
||||
Основные возможности:
|
||||
- Формирование паспортов для каждого DataFrame.
|
||||
- Формирование паспорта заказчика на основе DataFrame и событий.
|
||||
- Генерация событий по DataFrame.
|
||||
- Построение идеальных данных для каждой точки.
|
||||
- Формирование графического паспорта, включающего полезные данные и перечень паспортов точек.
|
||||
|
||||
Внимание: многие атрибуты (например, self._mediator, self._stages, self._clear_stage,
|
||||
self._settings, self._ideal_data_cache, self._OptAlgorithm_operator_params,
|
||||
self._OptAlgorithm_system_params) предполагается задавать извне (например, в базовом классе).
|
||||
"""
|
||||
|
||||
def form_passports(self, data: List[pd.DataFrame]) -> None:
|
||||
"""
|
||||
Формирует паспорта для каждого DataFrame из списка.
|
||||
В случае ошибки логируется сообщение, а медиатору отправляется пустой список.
|
||||
|
||||
:param data: Список DataFrame с данными трассировки.
|
||||
"""
|
||||
try:
|
||||
passports = [self._build_from_df_only(df) for df in data]
|
||||
except Exception as e:
|
||||
logger.error(f"form_passports - Непредвиденная ошибка при формировании паспортов: {e}")
|
||||
passports = []
|
||||
finally:
|
||||
self._mediator.notify(self, passports)
|
||||
|
||||
def update_settings(self, settings: Settings) -> None:
|
||||
"""
|
||||
Обновляет настройки формирования паспортов.
|
||||
|
||||
:param settings: Объект настроек.
|
||||
"""
|
||||
self._settings = settings
|
||||
|
||||
def form_customer_passport(self, data: Tuple[pd.DataFrame, Dict, Tuple]) -> None:
|
||||
"""
|
||||
Формирует паспорт заказчика на основе DataFrame и словаря событий.
|
||||
Ожидается, что data содержит:
|
||||
- DataFrame с данными,
|
||||
- словарь событий,
|
||||
- кортеж координат ME (например, (part_pos, open_pos)).
|
||||
|
||||
В случае ошибки логируется сообщение, а медиатору отправляется пустой список.
|
||||
|
||||
:param data: Кортеж из (DataFrame, события, ME_coords).
|
||||
"""
|
||||
try:
|
||||
dataframe, events, ME_coords = data
|
||||
point_quantity = len(events["Squeeze"][0])
|
||||
self._modify_coord_settings(ME_coords)
|
||||
customer_passport = self._form_graphic_passport(dataframe, events, point_quantity)
|
||||
result = [customer_passport]
|
||||
except Exception as e:
|
||||
logger.error(f"form_customer_passport - Непредвиденная ошибка при формировании паспорта заказчика: {e}")
|
||||
result = []
|
||||
finally:
|
||||
self._mediator.notify(self, result)
|
||||
|
||||
def _modify_coord_settings(self, ME_coords: Tuple[List[float], List[float]]) -> None:
|
||||
"""
|
||||
Модифицирует настройки координат на основе переданных данных ME.
|
||||
|
||||
:param ME_coords: Кортеж из двух списков: (part_pos, open_pos).
|
||||
"""
|
||||
part_pos, open_pos = ME_coords
|
||||
self._settings.operator["distance_l_2"] = open_pos
|
||||
l1 = self._settings.operator["distance_l_1"]
|
||||
self._settings.operator["part_pos"] = [part_pos[i] + l1[i] for i in range(len(part_pos))]
|
||||
|
||||
@staticmethod
|
||||
def _find_indexes(signal: str, df: pd.DataFrame) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Находит индексы начала и окончания этапа для указанного сигнала.
|
||||
|
||||
:param signal: Имя столбца-сигнала.
|
||||
:param df: DataFrame с данными.
|
||||
:return: Кортеж из массивов индексов начала и окончания этапа.
|
||||
"""
|
||||
stage_diff = np.diff(df[signal])
|
||||
start_idx = np.where(stage_diff == 1)
|
||||
finish_idx = np.where(stage_diff == -1)
|
||||
return start_idx[0], finish_idx[0]
|
||||
|
||||
@staticmethod
|
||||
def _find_events(signal: str, times: pd.Series, df: pd.DataFrame) -> Tuple[List[float], List[float]]:
|
||||
"""
|
||||
Формирует списки времён начала и окончания событий для указанного сигнала.
|
||||
|
||||
Если у первого события не определено время начала, оно принимается за 0.
|
||||
Если число стартов события больше числа финишей, последним финишем считается конец времён.
|
||||
|
||||
:param signal: Имя столбца-сигнала.
|
||||
:param times: Series с временными метками.
|
||||
:param df: DataFrame с данными.
|
||||
:return: Кортеж из двух списков: (список времён начала, список времён окончания).
|
||||
"""
|
||||
start_idx, finish_idx = PassportFormer._find_indexes(signal, df)
|
||||
start_list, end_list = [], []
|
||||
if len(start_idx) > 0 and len(finish_idx) > 0:
|
||||
if start_idx[0] > finish_idx[0]:
|
||||
logger.debug(f"_find_events - не найдено начало события {signal} с окончанием в {times.iloc[finish_idx[0]]} секунд, принято равным 0")
|
||||
start_idx = np.insert(start_idx, 0, 0)
|
||||
if start_idx[-1] > finish_idx[-1]:
|
||||
logger.debug(f"_find_events - не найден конец события {signal} с началом в {times.iloc[start_idx[-1]]} секунд, принято равным {times.iloc[-1]} секунд")
|
||||
finish_idx = np.append(finish_idx, -1)
|
||||
start_list = times.iloc[start_idx].tolist()
|
||||
end_list = times.iloc[finish_idx].tolist()
|
||||
return (start_list, end_list)
|
||||
|
||||
def _generate_events(self, times: pd.Series, df: pd.DataFrame) -> Tuple[Dict[str, List[List[float]]], int]:
|
||||
"""
|
||||
Генерирует словарь событий для каждого этапа, используя временные метки и данные.
|
||||
Также определяет общее количество точек (на основе количества событий основного этапа).
|
||||
|
||||
Если для основного этапа (self._clear_stage) не найдено ни одного события, возвращается пустой словарь и 0 точек.
|
||||
|
||||
:param times: Серия временных меток.
|
||||
:param df: DataFrame с данными.
|
||||
:return: Кортеж (словарь событий, количество точек).
|
||||
"""
|
||||
events = {}
|
||||
point_quantity = 0
|
||||
if self._clear_stage in self._stages:
|
||||
start_list, _ = self._find_events(self._clear_stage, times, df)
|
||||
point_quantity = len(start_list)
|
||||
if point_quantity == 0:
|
||||
logger.error(f"_generate_events - Не найдены события для этапа '{self._clear_stage}'.")
|
||||
return {}, 0
|
||||
for stage in self._stages:
|
||||
s_list, e_list = self._find_events(stage, times, df)
|
||||
temp = max(len(s_list), len(e_list))
|
||||
if temp < point_quantity:
|
||||
logger.warning(f"_generate_events - Недостаточное количество событий для этапа '{stage}'. "
|
||||
f"Ожидается {point_quantity}, получено {temp}.")
|
||||
events[stage] = [s_list, e_list]
|
||||
|
||||
if self._settings.system["performance_mode"][0] == 'client':
|
||||
client_rob = self._settings.operator["time_robot_movement"]
|
||||
move_start = events["Oncomming"][0]
|
||||
if len(move_start) > len(client_rob):
|
||||
move_start = move_start[1:]
|
||||
events["Oncomming"] = [move_start, [client_rob[i]+ move_start[i] for i in range(len(move_start))]]
|
||||
return (events, point_quantity)
|
||||
|
||||
def _build_ideal_data(self, idealDataBuilder: Optional[BaseIdealDataBuilder] = None,
|
||||
point_settings: Optional[Settings] = None) -> Dict:
|
||||
"""
|
||||
Строит идеальные данные с использованием билдера.
|
||||
|
||||
:param idealDataBuilder: Класс-билдер для генерации идеальных данных.
|
||||
:param point_settings: Настройки для точки.
|
||||
:return: Словарь с идеальными данными для этапов.
|
||||
"""
|
||||
try:
|
||||
self._opt_algorithm = idealDataBuilder(point_settings)
|
||||
stage_ideals = {
|
||||
"Closing": self.opt_algorithm.get_closingDF(),
|
||||
"Squeeze": self.opt_algorithm.get_compressionDF(),
|
||||
"Welding": self.opt_algorithm.get_weldingDF(),
|
||||
"Relief": self.opt_algorithm.get_openingDF(),
|
||||
"Oncomming": self.opt_algorithm.get_oncomingDF(),
|
||||
"Ideal cycle": self.opt_algorithm.get_cycle_time(),
|
||||
"Ideal timings": self.opt_algorithm.get_ideal_timings()
|
||||
}
|
||||
return stage_ideals
|
||||
except Exception as e:
|
||||
logger.error(f"_build_ideal_data - Ошибка при построении идеальных данных: {e}")
|
||||
return {}
|
||||
|
||||
def _generate_cache_key(self, point_settings: Settings) -> Tuple[Tuple[Tuple[str, Any], ...], Tuple[Tuple[str, Any], ...]]:
|
||||
"""
|
||||
Преобразует настройки точки в хешируемый ключ для кэша.
|
||||
|
||||
Использует только те параметры оператора и системы, которые присутствуют в
|
||||
соответствующих наборах (self._OptAlgorithm_operator_params и self._OptAlgorithm_system_params).
|
||||
|
||||
:param point_settings: Объект настроек для точки.
|
||||
:return: Кортеж из двух frozenset с параметрами.
|
||||
"""
|
||||
try:
|
||||
operator_tuple = frozenset(
|
||||
(key, value)
|
||||
for key, value in point_settings.operator.items()
|
||||
if str(key) in self._OptAlgorithm_operator_params
|
||||
)
|
||||
system_tuple = frozenset(
|
||||
(key, value)
|
||||
for key, value in point_settings.system.items()
|
||||
if str(key) in self._OptAlgorithm_system_params
|
||||
)
|
||||
return (operator_tuple, system_tuple)
|
||||
except Exception as e:
|
||||
logger.error(f"_generate_cache_key - Ошибка при генерации ключа кэша: {e}")
|
||||
return ((), ())
|
||||
|
||||
def _build_from_df_only(self, df: pd.DataFrame) -> Optional[GraphicPassport]:
|
||||
"""
|
||||
Строит GraphicPassport на основе одного DataFrame.
|
||||
|
||||
Если DataFrame содержит необходимые столбцы (определяемые в self._stages),
|
||||
генерируются события; иначе используется альтернативное определение количества точек.
|
||||
Если количество точек равно нулю, логируется ошибка и возвращается None.
|
||||
|
||||
:param df: DataFrame с данными.
|
||||
:return: Объект GraphicPassport или None в случае ошибки.
|
||||
"""
|
||||
try:
|
||||
if df is not None and set(self._stages).issubset(set(df.columns.tolist())):
|
||||
events, point_quantity = self._generate_events(df["time"], df)
|
||||
if point_quantity == 0:
|
||||
logger.error("_build_from_df_only - Не найдено ни одного события в DataFrame.")
|
||||
return None
|
||||
else:
|
||||
events = None
|
||||
key = list(self._settings.operator.keys())[0]
|
||||
point_quantity = len(self._settings.operator[key])
|
||||
self._settings.operator["part_pos"] = self._settings.operator["distance_l_2"]
|
||||
passport = self._form_graphic_passport(df, events, point_quantity)
|
||||
return passport
|
||||
except Exception as e:
|
||||
logger.error(f"_build_from_df_only - Непредвиденная ошибка: {e}")
|
||||
return None
|
||||
|
||||
def _form_graphic_passport(self, df: pd.DataFrame, events: Dict, point_quantity: int) -> Optional[GraphicPassport]:
|
||||
"""
|
||||
Формирует графический паспорт, включающий полезные данные и список PointPassport.
|
||||
|
||||
Для каждой из точек:
|
||||
- Извлекаются настройки оператора для данной точки.
|
||||
- Формируется временной интервал и события.
|
||||
- Рассчитываются идеальные и полезные данные.
|
||||
- Создаётся объект PointPassport, который добавляется в список.
|
||||
|
||||
:param df: DataFrame с данными.
|
||||
:param events: Словарь событий, сгенерированных для этапов.
|
||||
:param point_quantity: Количество точек.
|
||||
:return: Объект GraphicPassport или None в случае ошибки.
|
||||
"""
|
||||
try:
|
||||
system_settings = {key: value[0] for key, value in self._settings.system.items()}
|
||||
graphic_passport = GraphicPassport()
|
||||
graphic_passport.dataframe = df
|
||||
|
||||
for i in range(point_quantity):
|
||||
point_settings = Settings(self._get_operator_settings_part(i), system_settings)
|
||||
timeframe, po_events = self._form_point_events(events, i)
|
||||
if timeframe and po_events:
|
||||
point_settings.operator["time_robot_movement"] = po_events["Oncomming"][1] - po_events["Oncomming"][0]
|
||||
ideal_data = self._form_point_ideal_data(point_settings)
|
||||
useful_data = self._form_point_useful_data(point_settings.operator)
|
||||
point_passport = PointPassport(timeframe, po_events, ideal_data, useful_data)
|
||||
graphic_passport.points_pocket.append(point_passport)
|
||||
graphic_passport.useful_data = self._form_graphic_useful_data(system_settings, graphic_passport.points_pocket)
|
||||
return graphic_passport
|
||||
except Exception as e:
|
||||
logger.error(f"_form_graphic_passport - Ошибка при формировании графического паспорта: {e}")
|
||||
return None
|
||||
|
||||
def _form_graphic_useful_data(self, system_settings: Dict, points:List[PointPassport]) -> UsefulGraphData:
|
||||
"""
|
||||
Формирует словарь полезных данных для графического паспорта.
|
||||
|
||||
:param system_settings: Словарь системных настроек.
|
||||
:return: Объект UsefulGraphData.
|
||||
"""
|
||||
try:
|
||||
performance_data = PerformanceData()
|
||||
if points[0].timeframe is not None and points[0].events is not None:
|
||||
client_time = system_settings["client_time"]
|
||||
ideal_time = sum([sum(point.ideal_data["Ideal timings"])for point in points])
|
||||
TWC_time = (
|
||||
system_settings["time_before_start"] +
|
||||
points[0].timeframe[1] - points[0].events["Closing"][0] +
|
||||
sum([sum([item[1]-item[0] for key, item in point.events.items()]) for point in points[1:-1]]) +
|
||||
points[-1].events["Relief"][1] - points[-1].timeframe[0] +
|
||||
system_settings["time_after_end"]
|
||||
)
|
||||
performance_data.client_to_TWC = round((1 - TWC_time / client_time) * 100, 2) if client_time else 0.0
|
||||
performance_data.client_to_ideal = round((1 - ideal_time / client_time) * 100, 2) if client_time else 0.0
|
||||
performance_data.TWC_to_ideal = round((ideal_time / TWC_time) * 100, 2) if TWC_time else 0.0
|
||||
|
||||
useful_data = UsefulGraphData(
|
||||
performance_data,
|
||||
system_settings["Range ME, mm"],
|
||||
system_settings["k_hardness_1"]
|
||||
)
|
||||
return useful_data
|
||||
except Exception as e:
|
||||
logger.error(f"_form_graphic_useful_data - Ошибка при формировании полезных данных: {e}")
|
||||
return UsefulGraphData()
|
||||
|
||||
def _form_point_useful_data(self, operator_settings: Dict) -> Dict:
|
||||
"""
|
||||
Формирует полезные данные для отдельной точки.
|
||||
|
||||
:param operator_settings: Словарь настроек оператора для точки.
|
||||
:return: Словарь с полезными данными (толщина, позиция детали, сила) или пустой словарь.
|
||||
"""
|
||||
try:
|
||||
useful_data = {
|
||||
"thickness": operator_settings["object_thickness"],
|
||||
"part_pos": operator_settings["part_pos"],
|
||||
"force": operator_settings["force_target"]
|
||||
}
|
||||
return useful_data
|
||||
except Exception as e:
|
||||
logger.error(f"_form_point_useful_data - Ошибка при формировании полезных данных точки: {e}")
|
||||
return {}
|
||||
|
||||
def _form_point_ideal_data(self, point_settings: Settings) -> Dict:
|
||||
"""
|
||||
Формирует идеальные данные для отдельной точки с использованием кэша.
|
||||
|
||||
Генерируется кэш-ключ из настроек точки, затем либо извлекаются ранее рассчитанные
|
||||
данные, либо вычисляются новые с помощью билдера.
|
||||
|
||||
:param point_settings: Настройки точки.
|
||||
:return: Словарь с идеальными данными или пустой словарь в случае ошибки.
|
||||
"""
|
||||
try:
|
||||
cache_key = self._generate_cache_key(point_settings)
|
||||
ideal_data = self._ideal_data_cache.get(
|
||||
cache_key,
|
||||
self._build_ideal_data(idealDataBuilder=IdealDataBuilder, point_settings=point_settings)
|
||||
)
|
||||
self._ideal_data_cache[cache_key] = ideal_data
|
||||
return ideal_data
|
||||
except Exception as e:
|
||||
logger.error(f"_form_point_ideal_data - Ошибка при формировании идеальных данных точки: {e}")
|
||||
return {}
|
||||
|
||||
def _get_operator_settings_part(self, idx: int) -> Dict:
|
||||
"""
|
||||
Извлекает часть настроек оператора для конкретного индекса.
|
||||
|
||||
Если индекс выходит за пределы списка, используется первый элемент.
|
||||
|
||||
:param idx: Индекс точки.
|
||||
:return: Словарь настроек оператора для данной точки.
|
||||
"""
|
||||
try:
|
||||
operator_settings = {
|
||||
key: (value[idx] if idx < len(value) else value[0])
|
||||
for key, value in self._settings.operator.items()
|
||||
}
|
||||
return operator_settings
|
||||
except Exception as e:
|
||||
logger.error(f"_get_operator_settings_part - Ошибка при получении настроек оператора для индекса {idx}: {e}")
|
||||
return {}
|
||||
|
||||
def _form_point_events(self, events: Dict, idx: int) -> Tuple[Optional[List[float]], Optional[Dict[str, List[float]]]]:
|
||||
"""
|
||||
Формирует временной интервал и события для отдельной точки.
|
||||
|
||||
Если событий нет, возвращает (None, None).
|
||||
|
||||
:param events: Словарь с событиями для всех этапов.
|
||||
:param idx: Индекс точки.
|
||||
:return: Кортеж (timeframe, point_events) или (None, None) в случае ошибки.
|
||||
"""
|
||||
try:
|
||||
timeframe, point_events = None, None
|
||||
start, end = 0, 0
|
||||
if events is not None:
|
||||
# Если первое событие основного этапа начинается с 0, сдвигаем индекс
|
||||
start = events[self._stages[0]][0][idx]
|
||||
end = 0
|
||||
point_events = {}
|
||||
for key, value in events.items():
|
||||
if len(value[0]) > idx and value[0][idx] >= start:
|
||||
point_events[key] = [value[0][idx], value[1][idx]]
|
||||
if value[1][idx] > end: end = value[1][idx]
|
||||
else:
|
||||
logger.warning(f"_form_point_events - Обнаружен аномальный порядок событий для точки {idx}")
|
||||
for i in range(len(value[0])):
|
||||
if value[0][i] >= start:
|
||||
logger.info(f"_form_point_events - Найдено событе (вхождение {i}), соответствующее временным рамкам точки")
|
||||
point_events[key] = [value[0][i], value[1][i]]
|
||||
if value[1][i] > end: end = value[1][i]
|
||||
break
|
||||
point_events[key] = [end, end+0.01]
|
||||
end += 0.01
|
||||
if end != 0: timeframe = [start, end]
|
||||
return timeframe, point_events
|
||||
except Exception as e:
|
||||
logger.error(f"_form_point_events - Ошибка при формировании событий для точки {idx}: {e}")
|
||||
return None, None
|
||||
|
||||
|
||||
class IdealDataBuilder(BaseIdealDataBuilder):
|
||||
"""
|
||||
Класс для построения идеальных данных по этапам.
|
||||
|
||||
Реализует методы получения DataFrame для различных этапов:
|
||||
закрытия, сжатия, открытия, движения, сварки,
|
||||
а также метод получения идеальных временных интервалов.
|
||||
"""
|
||||
|
||||
def get_closingDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа закрытия.
|
||||
"""
|
||||
try:
|
||||
return self._get_data(self.Ts['tclose'], self.calcPhaseClose)
|
||||
except Exception as e:
|
||||
logger.error(f"get_closingDF - Ошибка при получении данных для этапа закрытия: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_compressionDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа сжатия.
|
||||
"""
|
||||
try:
|
||||
return self._get_data(self.Ts['tgrow'], self.calcPhaseGrow)
|
||||
except Exception as e:
|
||||
logger.error(f"get_compressionDF - Ошибка при получении данных для этапа сжатия: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_openingDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа открытия.
|
||||
"""
|
||||
try:
|
||||
return self._get_data(self.getMarkOpen(), self.calcPhaseOpen)
|
||||
except Exception as e:
|
||||
logger.error(f"get_openingDF - Ошибка при получении данных для этапа открытия: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_oncomingDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа движения.
|
||||
"""
|
||||
try:
|
||||
return self._get_data(self.Ts['tmovement'], self.calcPhaseMovement)
|
||||
except Exception as e:
|
||||
logger.error(f"get_oncomingDF - Ошибка при получении данных для этапа движения: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_weldingDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа сварки.
|
||||
|
||||
Используется функция calcPhaseGrow с небольшим сдвигом времени.
|
||||
Значения масштабируются (умножаются на 1000) для перевода в нужные единицы.
|
||||
"""
|
||||
try:
|
||||
data = []
|
||||
# Используем небольшое смещение для расчёта сварки
|
||||
X1, X2, V1, V2, F = self.calcPhaseGrow(self.Ts['tgrow'] - 0.0001)
|
||||
X1, X2, V1, V2 = X1 * 1000, X2 * 1000, V1 * 1000, V2 * 1000
|
||||
points_num = 5
|
||||
for i in range(points_num + 1):
|
||||
data.append({
|
||||
"time": self.welding_time * i / points_num,
|
||||
"Position FE": X1,
|
||||
"Position ME": X2,
|
||||
"Rotor Speed FE": V1,
|
||||
"Rotor Speed ME": V2,
|
||||
"Force": F
|
||||
})
|
||||
return pd.DataFrame(data)
|
||||
except Exception as e:
|
||||
logger.error(f"get_weldingDF - Ошибка при получении данных для этапа сварки: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_ideal_timings(self) -> List[float]:
|
||||
"""
|
||||
Получает список идеальных временных интервалов для этапов.
|
||||
Список включает: tclose, tgrow, welding_time, getMarkOpen(), tmovement.
|
||||
"""
|
||||
try:
|
||||
data = self.Ts
|
||||
ideal_timings = [
|
||||
data['tclose'],
|
||||
data['tgrow'],
|
||||
self.welding_time,
|
||||
self.getMarkOpen(),
|
||||
data['tmovement']
|
||||
]
|
||||
return ideal_timings
|
||||
except Exception as e:
|
||||
logger.error(f"get_ideal_timings - Ошибка при получении идеальных временных интервалов: {e}")
|
||||
return []
|
||||
|
||||
def _get_data(self, end_timestamp: float, func) -> pd.DataFrame:
|
||||
"""
|
||||
Получает данные до указанного времени (end_timestamp) с шагом, определяемым параметром mul.
|
||||
|
||||
Для каждого шага рассчитываются значения с использованием переданной функции func.
|
||||
В конце добавляется строка с точным значением end_timestamp.
|
||||
|
||||
:param end_timestamp: Время окончания получения данных.
|
||||
:param func: Функция для расчёта значений в зависимости от времени.
|
||||
:return: DataFrame с рассчитанными данными.
|
||||
"""
|
||||
try:
|
||||
data = []
|
||||
# Генерируем данные с шагом 1/mul
|
||||
for i in range(0, int(end_timestamp * self.mul) + 1):
|
||||
time_val = i / self.mul
|
||||
X1, X2, V1, V2, F = func(time_val)
|
||||
data.append({
|
||||
"time": time_val,
|
||||
"Position FE": X1 * 1000,
|
||||
"Position ME": X2 * 1000,
|
||||
"Rotor Speed FE": V1 * 1000,
|
||||
"Rotor Speed ME": V2 * 1000,
|
||||
"Force": F
|
||||
})
|
||||
# Добавляем финальную строку с end_timestamp
|
||||
X1, X2, V1, V2, F = func(end_timestamp)
|
||||
data.append({
|
||||
"time": end_timestamp,
|
||||
"Position FE": X1 * 1000,
|
||||
"Position ME": X2 * 1000,
|
||||
"Rotor Speed FE": V1 * 1000,
|
||||
"Rotor Speed ME": V2 * 1000,
|
||||
"Force": F
|
||||
})
|
||||
return pd.DataFrame(data)
|
||||
except Exception as e:
|
||||
logger.error(f"_get_data - Ошибка при получении данных с end_timestamp={end_timestamp}: {e}")
|
||||
return pd.DataFrame()
|
||||
@ -1,2 +1,2 @@
|
||||
from .plotter import PlotWidget
|
||||
from .settings_window import SettingsWindow
|
||||
from .settings_window import settingsWindow
|
||||
|
||||
BIN
src/gui/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
src/gui/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/gui/__pycache__/app.cpython-310.pyc
Normal file
BIN
src/gui/__pycache__/app.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/gui/__pycache__/mainGui.cpython-310.pyc
Normal file
BIN
src/gui/__pycache__/mainGui.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/gui/__pycache__/plot_window.cpython-310.pyc
Normal file
BIN
src/gui/__pycache__/plot_window.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/gui/__pycache__/plotter.cpython-310.pyc
Normal file
BIN
src/gui/__pycache__/plotter.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/gui/__pycache__/qt_settings.cpython-310.pyc
Normal file
BIN
src/gui/__pycache__/qt_settings.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/gui/__pycache__/reportGui.cpython-310.pyc
Normal file
BIN
src/gui/__pycache__/reportGui.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/gui/__pycache__/settings_window.cpython-310.pyc
Normal file
BIN
src/gui/__pycache__/settings_window.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/gui/__pycache__/settings_windows.cpython-310.pyc
Normal file
BIN
src/gui/__pycache__/settings_windows.cpython-310.pyc
Normal file
Binary file not shown.
335
src/gui/mainGui.py
Normal file
335
src/gui/mainGui.py
Normal file
@ -0,0 +1,335 @@
|
||||
from datetime import datetime as dt
|
||||
from typing import Optional
|
||||
from PyQt5 import QtWidgets
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QPixmap, QIcon
|
||||
from PyQt5.QtWidgets import QSizePolicy as QSP
|
||||
|
||||
from utils.base.base import BaseMainWindow, BaseController
|
||||
from gui.settings_window import SystemSettings, OperatorSettings
|
||||
from gui.reportGui import ReportSettings
|
||||
|
||||
|
||||
# FIXME: При отркытии RaportMode все прочитанные трейсы должны удаляться, но они открываются...
|
||||
class MainWindow(BaseMainWindow):
|
||||
def __init__(self,
|
||||
controller: Optional[BaseController] = None) -> None:
|
||||
super().__init__()
|
||||
self._controller = controller
|
||||
self._init_startUI()
|
||||
self._init_dock_widgets()
|
||||
self._init_menu()
|
||||
# self.setLocale()
|
||||
|
||||
|
||||
def _init_TabWidget(self) -> None:
|
||||
self.tabWidget = QtWidgets.QTabWidget()
|
||||
self.tabWidget.setTabsClosable(True)
|
||||
self.tabWidget.tabCloseRequested.connect(self._close_tab)
|
||||
self.tabWidget.currentChanged.connect(self._on_tab_changed)
|
||||
|
||||
def _init_startUI(self) -> None:
|
||||
self.operSettings = OperatorSettings("params/operator_params.json", 'Operator', self._upd_settings)
|
||||
self.sysSettings = SystemSettings("params/system_params.json", 'System', self._upd_settings)
|
||||
self.repSettings = ReportSettings()
|
||||
|
||||
|
||||
self._clear()
|
||||
self.resize(800,800)
|
||||
seeking_mode_btn = QtWidgets.QPushButton("Real time folder scanning")
|
||||
seeking_mode_btn.setFixedWidth(300)
|
||||
seeking_mode_btn.clicked.connect(self._init_seekingUI)
|
||||
raport_mode_btn = QtWidgets.QPushButton("Raport editor")
|
||||
raport_mode_btn.setFixedWidth(300)
|
||||
raport_mode_btn.clicked.connect(self._init_raportUI)
|
||||
|
||||
button_layout = QtWidgets.QHBoxLayout()
|
||||
button_layout.setSpacing(2)
|
||||
button_layout.addWidget(seeking_mode_btn)
|
||||
button_layout.addWidget(raport_mode_btn)
|
||||
button_widget = QtWidgets.QWidget()
|
||||
button_widget.setLayout(button_layout)
|
||||
|
||||
mainLayout = self._central_layout
|
||||
label = QtWidgets.QLabel("Select work mode")
|
||||
label.setStyleSheet(
|
||||
"""QLabel{
|
||||
color: #ffffff;
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
}"""
|
||||
)
|
||||
mainLayout.addWidget(label, alignment=Qt.AlignCenter)
|
||||
mainLayout.addWidget(button_widget)
|
||||
|
||||
self._init_statusBar()
|
||||
|
||||
def _init_dock_widgets(self) -> None:
|
||||
"""
|
||||
Инициализация док-виджетов для настроек.
|
||||
"""
|
||||
|
||||
# Создаем док-виджет для OperatorSettings
|
||||
self.operator_dock = QtWidgets.QDockWidget("Operator Settings", self)
|
||||
self.operator_dock.setWidget(self.operSettings)
|
||||
self.operator_dock.setObjectName("OperatorSettings")
|
||||
self.addDockWidget(Qt.RightDockWidgetArea, self.operator_dock)
|
||||
self.operator_dock.hide() # Скрываем по умолчанию
|
||||
|
||||
# Создаем док-виджет для SystemSettings
|
||||
self.system_dock = QtWidgets.QDockWidget("System Settings", self)
|
||||
self.system_dock.setWidget(self.sysSettings)
|
||||
self.system_dock.setObjectName("SystemSettings")
|
||||
self.addDockWidget(Qt.RightDockWidgetArea, self.system_dock)
|
||||
self.system_dock.hide() # Скрываем по умолчанию
|
||||
|
||||
# Создаем док-виджет для ReportSettings
|
||||
self.report_dock = QtWidgets.QDockWidget("View settings", self)
|
||||
self.report_dock.setWidget(self.repSettings)
|
||||
self.report_dock.setObjectName("ReportSettings")
|
||||
self.addDockWidget(Qt.RightDockWidgetArea, self.report_dock)
|
||||
self.report_dock.hide() # Скрываем по умолчанию
|
||||
|
||||
# Настройка док-виджетов
|
||||
self._set_dock_features(self.operator_dock)
|
||||
self._set_dock_features(self.system_dock)
|
||||
self._set_dock_features(self.report_dock)
|
||||
|
||||
def _init_menu(self) -> None:
|
||||
"""
|
||||
Инициализация главного меню.
|
||||
"""
|
||||
# Создаем главное меню
|
||||
menu_bar = self.menuBar()
|
||||
|
||||
# Создаем меню "Режимы"
|
||||
modes_menu = menu_bar.addMenu("Mode")
|
||||
settings_menu = menu_bar.addMenu("Settings")
|
||||
|
||||
# Создаем действия для меню
|
||||
seeking_action = QtWidgets.QAction("Real time folder scanning", self)
|
||||
seeking_action.triggered.connect(self._init_seekingUI)
|
||||
|
||||
raport_action = QtWidgets.QAction("Raport editor", self)
|
||||
raport_action.triggered.connect(self._init_raportUI)
|
||||
|
||||
system_settings = QtWidgets.QAction("System settings", self)
|
||||
system_settings.setIcon(QIcon('resources/system_ico.png'))
|
||||
system_settings.triggered.connect(lambda: self._toggle_visibility(self.system_dock))
|
||||
|
||||
operator_settings = QtWidgets.QAction("Operator settings ", self)
|
||||
operator_settings.setIcon(QIcon('resources/operator_ico.png'))
|
||||
operator_settings.triggered.connect(lambda: self._toggle_visibility(self.operator_dock))
|
||||
|
||||
view_settings = QtWidgets.QAction("View settings", self)
|
||||
view_settings.setIcon(QIcon('resources/view_ico.png'))
|
||||
view_settings.triggered.connect(lambda: self._toggle_visibility(self.report_dock))
|
||||
view_settings.triggered.connect(lambda: self._on_tab_changed(0))
|
||||
|
||||
|
||||
# Добавляем действия в меню "Режимы"
|
||||
modes_menu.addAction(seeking_action)
|
||||
modes_menu.addAction(raport_action)
|
||||
|
||||
settings_menu.addAction(system_settings)
|
||||
settings_menu.addAction(operator_settings)
|
||||
settings_menu.addAction(view_settings)
|
||||
|
||||
def _init_statusBar(self) -> None:
|
||||
# Создание пользовательского виджета для StatusBar
|
||||
note_widget = QtWidgets.QWidget()
|
||||
note_layout = QtWidgets.QHBoxLayout(note_widget)
|
||||
note_layout.setContentsMargins(10, 1, 10, 1)
|
||||
note_layout.setSpacing(15) # Устанавливаем расстояние между элементами
|
||||
|
||||
# Создание QLabel и QProgressBar
|
||||
self.mode_label = QtWidgets.QLabel()
|
||||
self.note_label = QtWidgets.QLabel()
|
||||
self.progress_bar = QtWidgets.QProgressBar()
|
||||
self.progress_bar.setRange(0, 100)
|
||||
self.progress_bar.setValue(0)
|
||||
self.progress_bar.setMinimumWidth(250)
|
||||
self.progress_bar.setMaximumHeight(10)
|
||||
self.progress_bar.setTextVisible(False)
|
||||
|
||||
# Создание QSpacerItem
|
||||
|
||||
# Установка политики размеров
|
||||
self.mode_label.setSizePolicy(QSP.Policy.Preferred, QSP.Policy.Preferred)
|
||||
self.note_label.setSizePolicy(QSP.Policy.MinimumExpanding, QSP.Policy.Preferred)
|
||||
self.progress_bar.setSizePolicy(QSP.Policy.Fixed, QSP.Policy.Preferred)
|
||||
|
||||
# Добавление виджетов в макет
|
||||
note_layout.addWidget(self.progress_bar)
|
||||
note_layout.addWidget(self.note_label)
|
||||
note_layout.addStretch(1)
|
||||
note_layout.addWidget(self.mode_label)
|
||||
|
||||
# Установка политики размеров для note_widget
|
||||
note_widget.setSizePolicy(QSP.Policy.Expanding, QSP.Policy.Preferred)
|
||||
|
||||
# Добавление пользовательского виджета в StatusBar как Permanent Widget
|
||||
self.statusBar().addPermanentWidget(note_widget, 1)
|
||||
|
||||
def _toggle_visibility(self, body:QtWidgets.QDockWidget = None) -> None:
|
||||
"""
|
||||
Переключение видимости док-виджета.
|
||||
"""
|
||||
is_visible = body.isVisible()
|
||||
body.setVisible(not is_visible)
|
||||
|
||||
def _set_dock_features(self, body:QtWidgets.QDockWidget = None) -> None:
|
||||
"""
|
||||
Настройка флагов док-виджета.
|
||||
"""
|
||||
flag_move = QtWidgets.QDockWidget.DockWidgetMovable
|
||||
flag_close = QtWidgets.QDockWidget.DockWidgetClosable
|
||||
flag_floating = QtWidgets.QDockWidget.DockWidgetFloatable
|
||||
body.setFeatures(flag_move | flag_close | flag_floating)
|
||||
|
||||
def _clear(self) -> None:
|
||||
if self._central_layout is not None:
|
||||
while self._central_layout.count():
|
||||
child = self._central_layout.takeAt(0)
|
||||
if child.widget() is not None:
|
||||
child.widget().deleteLater()
|
||||
|
||||
def _init_seekingUI(self) -> None:
|
||||
self._clear()
|
||||
self._init_TabWidget()
|
||||
self._transfer_settings()
|
||||
|
||||
button_layout = QtWidgets.QHBoxLayout()
|
||||
button_layout.setSpacing(2)
|
||||
button_widget = QtWidgets.QWidget()
|
||||
button_widget.setLayout(button_layout)
|
||||
|
||||
self.mode_label.setText("online mode")
|
||||
self._central_layout.addWidget(self.tabWidget)
|
||||
self._central_layout.addWidget(button_widget)
|
||||
self._controller.seeking_mode()
|
||||
|
||||
def _init_raportUI(self) -> None:
|
||||
self._clear()
|
||||
self._init_TabWidget()
|
||||
self._transfer_settings()
|
||||
|
||||
save_screen_btn = QtWidgets.QPushButton("Save state")
|
||||
save_screen_btn.setFixedWidth(185)
|
||||
save_screen_btn.clicked.connect(self._save_plots)
|
||||
open_file_btn = QtWidgets.QPushButton("Open file")
|
||||
open_file_btn.setFixedWidth(185)
|
||||
open_file_btn.clicked.connect(self._open_file)
|
||||
|
||||
button_layout = QtWidgets.QHBoxLayout()
|
||||
button_layout.setSpacing(2)
|
||||
|
||||
button_layout.addWidget(save_screen_btn)
|
||||
button_layout.addWidget(open_file_btn)
|
||||
button_widget = QtWidgets.QWidget()
|
||||
button_widget.setLayout(button_layout)
|
||||
|
||||
self.mode_label.setText("raport mode")
|
||||
self._central_layout.addWidget(self.tabWidget)
|
||||
self._central_layout.addWidget(button_widget)
|
||||
|
||||
self._controller.raport_mode()
|
||||
|
||||
def show_plot_tabs(self, plot_widgets: list[QtWidgets.QWidget]) -> None:
|
||||
for plot_widget in plot_widgets:
|
||||
widget, reg_items, curve_items, qt_items = plot_widget
|
||||
|
||||
tab = QtWidgets.QWidget()
|
||||
tab.setProperty("reg_items", reg_items)
|
||||
tab.setProperty("curve_items", curve_items)
|
||||
tab.setProperty("qt_items", qt_items)
|
||||
grid = QtWidgets.QGridLayout()
|
||||
grid.addWidget(widget)
|
||||
tab.setLayout(grid)
|
||||
self.tabWidget.addTab(tab, "SF_trace_" + dt.now().strftime('%Y_%m_%d-%H_%M_%S'))
|
||||
self.tabWidget.setCurrentWidget(tab)
|
||||
|
||||
tab_count = self.tabWidget.count()
|
||||
if tab_count > 10:
|
||||
for i in range(0, tab_count-10):
|
||||
self._close_tab(i)
|
||||
|
||||
self.update_stateLabel("Done!")
|
||||
|
||||
def keyPressEvent(self, a0) -> None:
|
||||
if a0.key() == Qt.Key_F5:
|
||||
tab_count = self.tabWidget.count()
|
||||
for i in range(0, tab_count):
|
||||
self._close_tab(i)
|
||||
|
||||
def closeEvent(self, a0):
|
||||
self.operSettings.close()
|
||||
self.sysSettings.close()
|
||||
self.repSettings.close()
|
||||
super().closeEvent(a0)
|
||||
|
||||
def update_progressBar(self, percent:int) -> None:
|
||||
if percent > 100: percent = 100
|
||||
self.progress_bar.setValue(percent)
|
||||
|
||||
def update_stateLabel(self, msg: str = None) -> None:
|
||||
self.note_label.setText(msg)
|
||||
self.note_label.adjustSize()
|
||||
|
||||
def _transfer_settings(self) -> None:
|
||||
operator_params = self.operSettings.getParams()
|
||||
system_params = self.sysSettings.getParams()
|
||||
self._controller.update_settings([operator_params, system_params])
|
||||
|
||||
def _upd_settings(self) -> None:
|
||||
self._transfer_settings()
|
||||
if self.mode_label.text():
|
||||
self.tabWidget.clear()
|
||||
self._controller.update_plots()
|
||||
|
||||
|
||||
|
||||
def _close_tab(self, index:int) -> None:
|
||||
self.tabWidget.removeTab(index)
|
||||
|
||||
def _open_file(self) -> None:
|
||||
path = self._select_csv()
|
||||
if path is not None:
|
||||
self._controller.open_file(path)
|
||||
|
||||
def _select_csv(self) -> Optional[str]:
|
||||
CSV_path, _ = QtWidgets.QFileDialog.getOpenFileName(self,"Select csv file", "", "CSV Files (*.csv)")
|
||||
if CSV_path:
|
||||
print(CSV_path)
|
||||
return CSV_path
|
||||
return None
|
||||
|
||||
def _on_tab_changed(self, index):
|
||||
try:
|
||||
tab = self.tabWidget.currentWidget()
|
||||
except:
|
||||
tab = None
|
||||
if tab is not None and self.report_dock.isVisible():
|
||||
reg_items = tab.property("reg_items")
|
||||
curve_items = tab.property("curve_items")
|
||||
qt_items = tab.property("qt_items")
|
||||
self.repSettings.build(index, reg_items, curve_items, qt_items)
|
||||
|
||||
def _save_plots(self) -> None:
|
||||
filepath, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save file", "", "Image Files (*.png *.jpeg)")
|
||||
tab = self.tabWidget.currentWidget()
|
||||
if tab is not None:
|
||||
pixmap = QPixmap(tab.size())
|
||||
tab.render(pixmap)
|
||||
pixmap.save(filepath)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,185 +0,0 @@
|
||||
from PyQt5 import QtWidgets
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
|
||||
from base.base import BaseMainWindow, Settings
|
||||
from gui.start_widget import (CustomMenuBar, CustomStatusBar,
|
||||
StartWidget, CustomTabWidget,
|
||||
RaportWidget, SeekingWidget,
|
||||
ClientAnalyzerWidget)
|
||||
from gui.settings_window import SystemSettings, OperatorSettings, FilterSettings
|
||||
from gui.report_gui import ReportSettings
|
||||
|
||||
|
||||
# FIXME: При отркытии RaportMode все прочитанные трейсы должны удаляться, но они открываются...
|
||||
class MainWindow(BaseMainWindow):
|
||||
|
||||
signal_mode = pyqtSignal(int)
|
||||
signal_settings = pyqtSignal(Settings)
|
||||
signal_replot_all = pyqtSignal()
|
||||
signal_open_file = pyqtSignal(str)
|
||||
signal_save_file = pyqtSignal(list)
|
||||
signal_open_dir = pyqtSignal(str)
|
||||
signal_TCW_for_client = pyqtSignal()
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._init_startUI()
|
||||
|
||||
def show_plot_tabs(self, plot_widgets: list[QtWidgets.QWidget]) -> None:
|
||||
for plot_widget in plot_widgets:
|
||||
self._tab_widget.create_tab(plot_widget)
|
||||
|
||||
tab_count = self._tab_widget.count()
|
||||
if tab_count > 10:
|
||||
for i in range(0, tab_count-10):
|
||||
self._tab_widget.close_tab(i)
|
||||
|
||||
self.status_widget.set_note("Done!")
|
||||
self.status_widget.set_progress(100)
|
||||
|
||||
def keyPressEvent(self, a0) -> None:
|
||||
if a0.key() == Qt.Key_F5:
|
||||
tab_count = self._tab_widget.count()
|
||||
for i in range(0, tab_count):
|
||||
self._tab_widget.close_tab(i)
|
||||
|
||||
def closeEvent(self, a0):
|
||||
self.operSettings.close()
|
||||
self.sysSettings.close()
|
||||
self.repSettings.close()
|
||||
super().closeEvent(a0)
|
||||
|
||||
def _init_startUI(self) -> None:
|
||||
self._init_settings()
|
||||
self._init_menu()
|
||||
self._init_status_bar()
|
||||
start_widget = StartWidget()
|
||||
start_widget.btn_scanning.clicked.connect(self._init_seekingUI)
|
||||
start_widget.btn_report.clicked.connect(self._init_raportUI)
|
||||
start_widget.btn_client.clicked.connect(self._init_client_UI)
|
||||
self.setCentralWidget(start_widget.get_widget())
|
||||
|
||||
def _init_settings(self) -> None:
|
||||
self.sysSettings = SystemSettings("params/system_params.json", 'System', self._upd_settings)
|
||||
self.repSettings = ReportSettings()
|
||||
self.operSettings = OperatorSettings("params/operator_params.json", 'Operator', self._upd_settings)
|
||||
self.filterSettings = FilterSettings("params/filter_params.json", "Client filter", self._upd_settings)
|
||||
|
||||
def _init_menu(self) -> None:
|
||||
self.menu = CustomMenuBar(self.sysSettings, self.repSettings, self.operSettings, self.filterSettings)
|
||||
self.menu.action_scanning.triggered.connect(self._init_seekingUI)
|
||||
self.menu.action_report.triggered.connect(self._init_raportUI)
|
||||
self.menu.action_client.triggered.connect(self._init_client_UI)
|
||||
self.menu.action_view_settings.triggered.connect(lambda: self._on_tab_changed)
|
||||
self.menu.setup(self)
|
||||
|
||||
def _init_status_bar(self) -> None:
|
||||
self.status_widget = CustomStatusBar()
|
||||
self.statusBar().addPermanentWidget(self.status_widget.get_widget(), 1)
|
||||
|
||||
def _init_tab_widget(self) -> None:
|
||||
self._tab_widget = CustomTabWidget()
|
||||
self._tab_widget.currentChanged.connect(self._on_tab_changed)
|
||||
|
||||
def _on_tab_changed(self):
|
||||
tab = self._tab_widget.currentWidget()
|
||||
if tab:
|
||||
reg_items = tab.property("reg_items")
|
||||
curve_items = tab.property("curve_items")
|
||||
qt_items = tab.property("qt_items")
|
||||
self.repSettings.build(reg_items, curve_items, qt_items)
|
||||
|
||||
def _clear(self) -> None:
|
||||
if self.layout() is not None:
|
||||
while self.layout().count():
|
||||
child = self.layout().takeAt(0)
|
||||
if child.widget() is not None:
|
||||
child.widget().deleteLater()
|
||||
self._init_tab_widget()
|
||||
self._transfer_settings()
|
||||
|
||||
def _init_raportUI(self) -> None:
|
||||
self._clear()
|
||||
self._set_mode(1)
|
||||
raport_widget = RaportWidget(self._tab_widget)
|
||||
raport_widget.btn_open_file.clicked.connect(self._open_file)
|
||||
raport_widget.btn_save_state.clicked.connect(self._save_plots)
|
||||
self.setCentralWidget(raport_widget.get_widget())
|
||||
|
||||
def _init_seekingUI(self) -> None:
|
||||
self._clear()
|
||||
self._set_mode(2)
|
||||
seeking_widget = SeekingWidget(self._tab_widget)
|
||||
self.setCentralWidget(seeking_widget.get_widget())
|
||||
|
||||
def _init_client_UI(self) -> None:
|
||||
self._clear()
|
||||
self._set_mode(3)
|
||||
client_widget = ClientAnalyzerWidget(self._tab_widget)
|
||||
#TODO: привязать действия к кнопкам
|
||||
client_widget.btn_open_folder.clicked.connect(self._open_folder)
|
||||
client_widget.btn_save_state.clicked.connect(self._save_plots)
|
||||
client_widget.btn_calc_tcw.clicked.connect(self._build_TCW_for_client)
|
||||
self.setCentralWidget(client_widget.get_widget())
|
||||
|
||||
def _build_TCW_for_client(self):
|
||||
self.signal_TCW_for_client.emit()
|
||||
|
||||
def _set_mode(self, num:int) -> None:
|
||||
match num:
|
||||
case 1:
|
||||
self.status_widget.set_mode("raport mode")
|
||||
self.signal_mode.emit(1)
|
||||
case 2:
|
||||
self.status_widget.set_mode("online mode")
|
||||
self.signal_mode.emit(2)
|
||||
case 3:
|
||||
self.status_widget.set_mode("client processor mode")
|
||||
self.signal_mode.emit(3)
|
||||
|
||||
def _transfer_settings(self) -> None:
|
||||
settings = Settings()
|
||||
settings.system = self.sysSettings.get_params()
|
||||
settings.operator = self.operSettings.get_params()
|
||||
settings.filter = self.filterSettings.get_params()
|
||||
self.signal_settings.emit(settings)
|
||||
|
||||
def _upd_settings(self) -> None:
|
||||
self._transfer_settings()
|
||||
if self.status_widget._mode_label.text():
|
||||
self._tab_widget.clear()
|
||||
self.signal_replot_all.emit()
|
||||
|
||||
def _open_file(self) -> None:
|
||||
CSV_path, _ = QtWidgets.QFileDialog.getOpenFileName(self,"Select csv file", "", "CSV Files (*.csv)")
|
||||
if CSV_path:
|
||||
self.signal_open_file.emit(CSV_path)
|
||||
|
||||
def _open_folder(self) -> None:
|
||||
dir_path = QtWidgets.QFileDialog.getExistingDirectory(self,"Select folder with traces","",
|
||||
QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontResolveSymlinks)
|
||||
if dir_path:
|
||||
self.signal_open_dir.emit(dir_path)
|
||||
|
||||
def _save_plots(self) -> None:
|
||||
filepath, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save file", "", "Image Files (*.png *.jpeg)")
|
||||
tab = self._tab_widget.currentWidget()
|
||||
if tab: self.signal_save_file.emit([filepath, tab])
|
||||
|
||||
# TODO: эта реализация работает, но для пользователя будет больно каждый раз писать имена и расширения
|
||||
# сохраняемых файлов, не кажется? Можно сделать, как показано ниже, тогда файлам будет назначаться имя
|
||||
# по умолчанию и с расширением. ХЗ правда, как эта форма на винде будет выглядеть - надо проверить.
|
||||
# ========================================================
|
||||
# dialog = QFileDialog()
|
||||
# dialog.setOptions(QFileDialog.DontUseNativeDialog)
|
||||
# dialog.setFileMode(QFileDialog.AnyFile)
|
||||
# dialog.setAcceptMode(QFileDialog.AcceptSave)
|
||||
# dialog.setDirectory(os.getcwd())
|
||||
# dialog.selectFile("untitled.txt")
|
||||
# dialog.exec_()
|
||||
# if dialog.accepted:
|
||||
# # emit signal to save file
|
||||
# ...
|
||||
# ========================================================
|
||||
|
||||
|
||||
@ -1,486 +1,143 @@
|
||||
from PyQt5.QtWidgets import (QWidget, QVBoxLayout,
|
||||
QHBoxLayout, QLabel,
|
||||
QGraphicsRectItem, QSpacerItem,
|
||||
QSizePolicy)
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
import copy
|
||||
import traceback
|
||||
import sys
|
||||
from typing import Optional, Tuple, Callable, List, Any, Dict
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QGraphicsRectItem, QSpacerItem, QSizePolicy
|
||||
)
|
||||
from PyQt5.QtCore import Qt
|
||||
from loguru import logger
|
||||
import pyqtgraph as pg
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from typing import Optional, Any
|
||||
|
||||
from base.base import (
|
||||
BasePlotWidget, GraphicPassport, PlotItems, PointPassport,
|
||||
UsefulGraphData, BaseController, PerformanceData
|
||||
)
|
||||
from utils.json_tools import read_json
|
||||
from utils import qt_settings as qts
|
||||
from utils.base.base import BasePlotWidget
|
||||
|
||||
class ProcessStage():
|
||||
mean_value:int
|
||||
start_index:int
|
||||
finish_index:int
|
||||
|
||||
# =============================================================================
|
||||
# Класс PlotWidget – построение графических виджетов на основе графических паспортов
|
||||
# =============================================================================
|
||||
class PlotWidget(BasePlotWidget):
|
||||
|
||||
def _create_curve_ideal(self,
|
||||
signal: dict[str, Any],
|
||||
ideal_data: pd.DataFrame,
|
||||
start_timestamp: float,
|
||||
finish_timestamp: float) -> Optional[pg.PlotDataItem]:
|
||||
"""
|
||||
Виджет построения графиков. На основе полученных графических паспортов
|
||||
создаёт набор виджетов (для отображения нескольких наборов данных),
|
||||
Создаёт идеальную кривую для сигнала, если заданы корректные временные рамки.
|
||||
"""
|
||||
if start_timestamp is not None and finish_timestamp is not None:
|
||||
return pg.PlotDataItem(
|
||||
x=start_timestamp + ideal_data["time"],
|
||||
y=ideal_data[signal["name"]],
|
||||
pen=signal["pen"]
|
||||
)
|
||||
return None
|
||||
|
||||
def __init__(self, controller: BaseController):
|
||||
"""
|
||||
Инициализирует PlotWidget, читая параметры структуры графиков из JSON.
|
||||
"""
|
||||
super().__init__(controller=controller)
|
||||
self._plt_structures = read_json("params/plot_structure_params.json")
|
||||
self._plt_channels = None # Будет установлен в зависимости от режима
|
||||
|
||||
def build(self, data: List[GraphicPassport]) -> None:
|
||||
"""
|
||||
Создает набор виджетов на основе списка графических паспортов.
|
||||
При ошибке выводится информационная метка об ошибке.
|
||||
|
||||
:param data: Список объектов GraphicPassport с данными для построения графиков.
|
||||
"""
|
||||
try:
|
||||
self._datalen = len(data)
|
||||
widgets_datapack = []
|
||||
# Проходим по каждому графическому паспорту и обновляем текущий индекс
|
||||
for idx, passport in enumerate(data):
|
||||
self._datastep = idx # Обновляем текущий шаг для обновления статуса
|
||||
widget = self._build_widget(passport)
|
||||
widgets_datapack.append(widget)
|
||||
except (KeyError, ValueError, TypeError) as e:
|
||||
logger.error(f"Ошибка при сборке данных графика: {e}")
|
||||
logger.debug(traceback.format_exc())
|
||||
error_label = QLabel("Произошла ошибка при формировании графиков. Проверьте корректность данных.")
|
||||
widgets_datapack = [error_label]
|
||||
except Exception as e:
|
||||
logger.error(f"Непредвиденная ошибка при формировании графиков: {e}")
|
||||
logger.debug(traceback.format_exc())
|
||||
error_label = QLabel("Непредвиденная ошибка при формировании графиков.")
|
||||
widgets_datapack = [error_label]
|
||||
finally:
|
||||
self._mediator.notify(self, widgets_datapack)
|
||||
|
||||
def build_raw_trace(self, data: Tuple[pd.DataFrame, dict]) -> None:
|
||||
"""
|
||||
Создаёт виджет с одним графиком, отображающим все данные сырой трассировки.
|
||||
|
||||
:param data: Кортеж из DataFrame с трассировочными данными и словаря событий.
|
||||
"""
|
||||
container_widget, container_layout, pyqt_container = self._generate_widget_container()
|
||||
plot = self._build_raw_plotitem(data, pyqt_container)
|
||||
container_layout.addWidget(plot)
|
||||
container_widget.setProperty("pyqt_container", pyqt_container)
|
||||
self._mediator.notify(self, [container_widget])
|
||||
|
||||
def set_mode(self, mode: int) -> None:
|
||||
"""
|
||||
Устанавливает режим построения графиков, выбирая соответствующий набор каналов.
|
||||
|
||||
:param mode: Режим работы (1 – Online Path Scanner, 2 – Report Editor, 3 – Client Trace Watcher).
|
||||
"""
|
||||
match mode:
|
||||
case 1:
|
||||
self._plt_channels = self._plt_structures["Online Path Scanner"]
|
||||
case 2:
|
||||
self._plt_channels = self._plt_structures["Report Editor"]
|
||||
case 3:
|
||||
self._plt_channels = self._plt_structures["Client Trace Watcher"]
|
||||
|
||||
def _build_raw_plotitem(self, data: Tuple[pd.DataFrame, dict],
|
||||
pyqt_container: PlotItems) -> pg.GraphicsLayoutWidget:
|
||||
"""
|
||||
Строит график необработанных трейсов клиента. Добавляет линии для каждого канала и области для событий.
|
||||
|
||||
:param data: Кортеж (dataframe, events)
|
||||
:param pyqt_container: Контейнер для хранения ссылок на QT-объекты графиков.
|
||||
:return: Виджет GraphicsLayoutWidget с построенным графиком.
|
||||
"""
|
||||
plot_item, legend = PlotItemGenerator._init_plot_item("Customer data")
|
||||
dataframe, events = data
|
||||
channels = dataframe.columns.tolist()
|
||||
# Добавляем линии для каждого канала
|
||||
for i, channel in enumerate(channels):
|
||||
plot = plot_item.plot(dataframe["time"], dataframe[channel], pen=qts.colors[i], fast=True)
|
||||
legend.addItem(plot, channel)
|
||||
pyqt_container.curves.setdefault("real", {})[channel] = plot
|
||||
|
||||
# Для каждого события этапа Squeeze добавляем регионы
|
||||
for i in range(len(events.get("Squeeze", [[], []])[0])):
|
||||
point_events = {}
|
||||
for key, item in events.items():
|
||||
point_events[key] = [item[0][i], item[1][i]]
|
||||
PlotItemGenerator._add_stage_regions(self._stage_colors, plot_item, point_events, pyqt_container.regions, 20)
|
||||
|
||||
plot_layout = pg.GraphicsLayoutWidget()
|
||||
plot_layout.addItem(plot_item)
|
||||
return plot_layout
|
||||
|
||||
def _build_performance_label(self, performance:PerformanceData, qt_items: Dict) -> QWidget:
|
||||
"""
|
||||
Создает QLabel с информацией о производительности (сокращение длительности, идеальное значение, КДИП).
|
||||
|
||||
:param qt_items: Словарь для сохранения ссылок на созданные виджеты.
|
||||
:return: Виджет с меткой производительности.
|
||||
"""
|
||||
|
||||
label_widget = QWidget()
|
||||
label_layout = QHBoxLayout(label_widget)
|
||||
start_label = QLabel("Сокращение длительности: ")
|
||||
real_label = QLabel(f"фактическое = {performance.client_to_TWC} % ")
|
||||
if performance.client_to_TWC == 0:
|
||||
real_label.setVisible(False)
|
||||
ideal_label = QLabel(f"идеальное = {performance.client_to_ideal} % ")
|
||||
if performance.client_to_ideal == 0:
|
||||
ideal_label.setVisible(False)
|
||||
kdip_label = QLabel(f"КДИП = {performance.TWC_to_ideal}% ")
|
||||
if performance.TWC_to_ideal == 0:
|
||||
kdip_label.setVisible(False)
|
||||
|
||||
label_layout.addWidget(start_label, alignment=Qt.AlignLeft)
|
||||
label_layout.addWidget(real_label, alignment=Qt.AlignLeft)
|
||||
label_layout.addWidget(ideal_label, alignment=Qt.AlignLeft)
|
||||
label_layout.addWidget(kdip_label, alignment=Qt.AlignLeft)
|
||||
spacer = QSpacerItem(1, 1, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||
label_layout.addSpacerItem(spacer)
|
||||
self.set_style(label_widget)
|
||||
|
||||
qt_items["performance label"] = label_widget
|
||||
qt_items["real performance"] = real_label
|
||||
qt_items["ideal performance"] = ideal_label
|
||||
qt_items["real to ideal performance"] = kdip_label
|
||||
return label_widget
|
||||
|
||||
def _build_widget(self, graphic_passport: GraphicPassport) -> QWidget:
|
||||
"""
|
||||
Собирает графический виджет для одного набора данных.
|
||||
|
||||
Генерирует контейнер, создаёт CustomPlotLayout для построения графика,
|
||||
добавляет, при необходимости, метку производительности.
|
||||
|
||||
:param graphic_passport: Объект GraphicPassport с данными для построения графика.
|
||||
:return: Готовый QWidget для отображения.
|
||||
"""
|
||||
container_widget, container_layout, pyqt_container = self._generate_widget_container()
|
||||
plot_layout = CustomPlotLayout(graphic_passport, len(self._plt_channels), self._stage_colors, self)
|
||||
plot_layout.build(pyqt_container, self._plt_channels)
|
||||
|
||||
perf_widget = self._build_performance_label(graphic_passport.useful_data.performance, pyqt_container.qt_items)
|
||||
container_layout.addWidget(perf_widget)
|
||||
|
||||
container_layout.addWidget(plot_layout)
|
||||
container_widget.setProperty("pyqt_container", pyqt_container)
|
||||
return container_widget
|
||||
|
||||
@staticmethod
|
||||
def _generate_widget_container() -> Tuple[QWidget, QVBoxLayout, PlotItems]:
|
||||
"""
|
||||
Создает контейнер для виджета графика, его макет и объект PlotItems для хранения ссылок.
|
||||
|
||||
:return: Кортеж (контейнер, вертикальный макет, PlotItems).
|
||||
"""
|
||||
container_widget = QWidget()
|
||||
container_layout = QVBoxLayout(container_widget)
|
||||
pyqt_container = PlotItems({"real": {}, "ideal": {}}, {"real": {}, "ideal": {}}, {})
|
||||
return container_widget, container_layout, pyqt_container
|
||||
|
||||
def _update_status(self, widget_steps: int, point_steps: int, cur_widget: int, cur_point: int) -> None:
|
||||
"""
|
||||
Вычисляет текущее значение прогресса и обновляет его через контроллер.
|
||||
|
||||
:param widget_steps: Общее число шагов по виджетам.
|
||||
:param point_steps: Общее число точек в виджете.
|
||||
:param cur_widget: Текущий номер виджета.
|
||||
:param cur_point: Текущий номер точки внутри виджета.
|
||||
"""
|
||||
if self._datalen:
|
||||
cycle_progress = self._datastep / self._datalen * 100 + 1
|
||||
period1 = 99 / self._datalen
|
||||
else:
|
||||
cycle_progress = 1
|
||||
period1 = 100
|
||||
|
||||
period2 = period1 / widget_steps if widget_steps != 0 else period1
|
||||
period3 = period2 / point_steps if point_steps != 0 else period2
|
||||
|
||||
progress = int(cycle_progress + period2 * cur_widget + period3 * cur_point)
|
||||
self._controller.update_progress(progress)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Класс CustomPlotLayout – кастомное размещение графических элементов
|
||||
# =============================================================================
|
||||
class CustomPlotLayout(pg.GraphicsLayoutWidget):
|
||||
"""
|
||||
Кастомный виджет для размещения графиков, построенных с помощью PlotItemGenerator.
|
||||
|
||||
Он формирует графики по каналам, связывает их между собой (XLink)
|
||||
и добавляет навигатор для общего обзора.
|
||||
"""
|
||||
|
||||
def __init__(self, graphic_passport: GraphicPassport, widget_steps: int, colors: Dict, parent: PlotWidget = None) -> None:
|
||||
super().__init__()
|
||||
self._plotter = PlotItemGenerator(graphic_passport, widget_steps, colors, parent)
|
||||
self.setProperty("performance", None)
|
||||
|
||||
def build(self, pyqt_container: PlotItems, plt_channels: Dict) -> None:
|
||||
"""
|
||||
Строит графики для каждого канала на основе параметров из plt_channels.
|
||||
Затем добавляет навигатор (NavigatorPlot) для синхронизации обзора.
|
||||
|
||||
:param pyqt_container: Контейнер для хранения ссылок на графические объекты.
|
||||
:param plt_channels: Словарь с описаниями каналов и их настройками.
|
||||
"""
|
||||
main_plot = None
|
||||
for widget_num, (channel, description) in enumerate(plt_channels.items()):
|
||||
plot_item = self._plotter.generate_plot_item(widget_num, channel, description, pyqt_container)
|
||||
if widget_num == 0:
|
||||
main_plot = plot_item
|
||||
else:
|
||||
plot_item.setXLink(main_plot)
|
||||
self.addItem(plot_item, widget_num, 0)
|
||||
navigator = NavigatorPlot([0, 10], main_plot)
|
||||
if navigator is not None:
|
||||
self.addItem(navigator, widget_num + 1, 0)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Класс PlotItemGenerator – генерация элементов графика и добавление регионов
|
||||
# =============================================================================
|
||||
class PlotItemGenerator:
|
||||
"""
|
||||
Генератор графических элементов (PlotItem) для построения графиков.
|
||||
|
||||
На основе графического паспорта создаются линии, регионы для этапов, добавляются
|
||||
идеальные сигналы и рассчитываются показатели производительности.
|
||||
"""
|
||||
|
||||
def __init__(self, graphic_passport: GraphicPassport, widget_steps: int, colors: Dict, parent: PlotWidget = None) -> None:
|
||||
self._stage_colors = colors
|
||||
self._ideal_mode = graphic_passport.dataframe is None
|
||||
self._parent = parent
|
||||
|
||||
if not self._ideal_mode:
|
||||
dataframe_headers = graphic_passport.dataframe.columns.tolist()
|
||||
point_steps = len(graphic_passport.points_pocket)
|
||||
else:
|
||||
dataframe_headers = []
|
||||
point_steps = 1
|
||||
|
||||
# Используем словарь для более понятной организации данных
|
||||
self._datapack: Dict[str, Any] = {
|
||||
"dataframe": graphic_passport.dataframe,
|
||||
"headers": dataframe_headers,
|
||||
"useful_data": graphic_passport.useful_data,
|
||||
"points_pocket": graphic_passport.points_pocket,
|
||||
"widget_steps": widget_steps,
|
||||
"point_steps": point_steps
|
||||
}
|
||||
|
||||
def generate_plot_item(self, widget_num: int, channel: str, description: Dict[str, Any],
|
||||
pyqt_container: PlotItems) -> pg.PlotItem:
|
||||
"""
|
||||
Генерирует PlotItem для заданного канала с добавлением регионов, компенсаций и идеальных данных.
|
||||
|
||||
:param widget_num: Номер текущего виджета.
|
||||
:param channel: Имя канала.
|
||||
:param description: Словарь настроек для данного канала.
|
||||
:param pyqt_container: Контейнер для хранения ссылок на объекты графиков.
|
||||
:return: PlotItem
|
||||
"""
|
||||
dp = self._datapack
|
||||
dataframe = dp["dataframe"]
|
||||
useful_data:UsefulGraphData = dp["useful_data"]
|
||||
points_pocket = dp["points_pocket"]
|
||||
widget_steps = dp["widget_steps"]
|
||||
point_steps = dp["point_steps"]
|
||||
|
||||
# Инициализируем PlotItem и легенду
|
||||
plot_item, legend = self._init_plot_item(title=channel)
|
||||
settings: Dict = description["Settings"]
|
||||
global_shift = 0
|
||||
ideal_df = pd.DataFrame({})
|
||||
|
||||
# При необходимости – зеркальное отражение данных для ME
|
||||
if settings.get("mirror ME", False) and not self._ideal_mode:
|
||||
dataframe = self._shift_data("ME", description["Real_signals"], dataframe,
|
||||
lambda x: useful_data.range_ME - x)
|
||||
|
||||
# Итерация по точкам паспорта
|
||||
for cur_point, point_data in enumerate(points_pocket):
|
||||
ideal_data = copy.deepcopy(point_data.ideal_data)
|
||||
if self._ideal_mode:
|
||||
global_shift, point_data.events, point_data.timeframe = self._generate_synthetic_events(global_shift, ideal_data)
|
||||
else:
|
||||
if settings.get("force compensation FE", False):
|
||||
force = point_data.useful_data["force"]
|
||||
k_hardness = useful_data.k_hardness
|
||||
signals = description["Real_signals"]
|
||||
dataframe = self._apply_force_compensation(force, k_hardness, dataframe, point_data.timeframe, signals)
|
||||
if settings.get("stages", False):
|
||||
self._add_stage_regions(self._stage_colors, plot_item, point_data.events, pyqt_container.regions, 75)
|
||||
if settings.get("force accuracy", False):
|
||||
force = point_data.useful_data["force"]
|
||||
self._add_force_accuracy_region(point_data.events["Welding"], force, plot_item)
|
||||
if settings.get("ideals", False) and settings.get("mirror ME", False):
|
||||
for stage in point_data.events.keys():
|
||||
ideal_data[stage] = self._shift_data("ME", description["Ideal_signals"], ideal_data[stage],
|
||||
lambda x: useful_data.range_ME - x)
|
||||
if settings.get("workpiece", False):
|
||||
self._add_workpiece(point_data, plot_item)
|
||||
if settings.get("ideals", False):
|
||||
self._add_ideal_stage_regions(self._stage_colors, plot_item, ideal_data, point_data.events,
|
||||
pyqt_container.regions, 100)
|
||||
ideal_df = self._modify_ideal_df(ideal_df, ideal_data, point_data.events)
|
||||
# Обновляем статус через родительский PlotWidget
|
||||
self._parent._update_status(widget_steps, point_steps, widget_num, cur_point)
|
||||
|
||||
# Добавляем идеальные сигналы (если указано в настройках)
|
||||
if settings.get("ideals", False):
|
||||
self._add_signals(plot_item, ideal_df, description["Ideal_signals"], legend, pyqt_container.curves["ideal"])
|
||||
# Добавляем реальные сигналы, если не включен режим идеала
|
||||
if not self._ideal_mode:
|
||||
self._add_signals(plot_item, dataframe, description["Real_signals"], legend, pyqt_container.curves["real"])
|
||||
return plot_item
|
||||
|
||||
@staticmethod
|
||||
def _shift_data(valid_str: str, signals: List[Dict], dataframe: pd.DataFrame, func: Callable) -> pd.DataFrame:
|
||||
"""
|
||||
Применяет заданную функцию (например, смещение) к столбцам, удовлетворяющим условию.
|
||||
|
||||
:param valid_str: Подстрока, по которой определяется, к каким сигналам применять функцию.
|
||||
:param signals: Список описаний сигналов.
|
||||
:param dataframe: DataFrame с данными.
|
||||
:param func: Функция, применяемая к значениям.
|
||||
:return: Измененный DataFrame.
|
||||
"""
|
||||
keys = dataframe.keys()
|
||||
for signal in signals:
|
||||
if valid_str in signal["name"] and signal["name"] in keys:
|
||||
dataframe[signal["name"]] = dataframe[signal["name"]].apply(func)
|
||||
return dataframe
|
||||
|
||||
@staticmethod
|
||||
def _init_plot_item(title: str) -> Tuple[pg.PlotItem, pg.LegendItem]:
|
||||
"""
|
||||
Инициализирует PlotItem с заданным заголовком, включает сетку и легенду.
|
||||
|
||||
:param title: Заголовок графика.
|
||||
:return: Кортеж (PlotItem, LegendItem).
|
||||
"""
|
||||
plot_item = pg.PlotItem(title=title)
|
||||
# Подписываемся на изменение диапазона оси X для автоматической даунсэмплинга
|
||||
plot_item.sigXRangeChanged.connect(lambda: PlotItemGenerator._update_plots_downsample(plot_item))
|
||||
plot_item.showGrid(x=True, y=True)
|
||||
plot_item.setClipToView(True)
|
||||
legend = plot_item.addLegend(offset=(70, 20))
|
||||
return plot_item, legend
|
||||
|
||||
@staticmethod
|
||||
def _create_stage_region(colors: Dict, stage: str, start_timestamp: float, finish_timestamp: float,
|
||||
def _create_stage_region(self,
|
||||
stage: str,
|
||||
start_timestamp: float,
|
||||
finish_timestamp: float,
|
||||
transparency: int) -> Optional[pg.LinearRegionItem]:
|
||||
"""
|
||||
Создает регион (LinearRegionItem) для заданного этапа, если заданы границы.
|
||||
|
||||
:param colors: Словарь с цветами для этапов.
|
||||
:param stage: Имя этапа.
|
||||
:param start_timestamp: Время начала этапа.
|
||||
:param finish_timestamp: Время окончания этапа.
|
||||
:param transparency: Значение прозрачности (alpha).
|
||||
:return: Объект LinearRegionItem или None.
|
||||
Создает регион для определённого этапа, если заданы временные рамки.
|
||||
"""
|
||||
if start_timestamp is not None and finish_timestamp is not None:
|
||||
region = pg.LinearRegionItem([start_timestamp, finish_timestamp], movable=False)
|
||||
color = colors.get(stage, [100, 100, 100, 100])
|
||||
# Устанавливаем кисть с заданной прозрачностью
|
||||
color = self._stage_colors.get(stage, [100, 100, 100, 100])
|
||||
region.setBrush(pg.mkBrush(color[:3] + [transparency]))
|
||||
return region
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _add_stage_regions(colors: Dict, plot_item: pg.PlotItem, point_events: Dict, reg_items: Dict, transparency: int = 75) -> None:
|
||||
"""
|
||||
Добавляет регионы для реальных этапов на PlotItem.
|
||||
def _init_plot_item(title: str) -> tuple[pg.PlotItem, pg.LegendItem]:
|
||||
plot_item = pg.PlotItem(title=title)
|
||||
# Оптимизация отображения графиков
|
||||
plot_item.setDownsampling(auto=True, mode='peak')
|
||||
plot_item.showGrid(x=True, y=True)
|
||||
plot_item.setClipToView(True)
|
||||
legend = plot_item.addLegend(offset=(70, 20))
|
||||
return plot_item, legend
|
||||
|
||||
:param colors: Словарь с цветами для этапов.
|
||||
:param plot_item: PlotItem, на который добавляются регионы.
|
||||
:param point_events: Словарь событий с временными рамками.
|
||||
:param reg_items: Словарь для хранения созданных регионов.
|
||||
:param transparency: Прозрачность регионов.
|
||||
def _add_stage_regions(self,
|
||||
plot_item: pg.PlotItem,
|
||||
point_events: dict[str, list[float]],
|
||||
dataframe_headers: list[str],
|
||||
reg_items: dict,
|
||||
transparency: int = 75) -> None:
|
||||
"""
|
||||
for stage, times in point_events.items():
|
||||
start_t, end_t = times
|
||||
region = PlotItemGenerator._create_stage_region(colors, stage, start_t, end_t, transparency)
|
||||
Добавляет регионы для реальных этапов, если все стадии есть в заголовках датафрейма.
|
||||
"""
|
||||
stages = point_events.keys()
|
||||
if all(stage in dataframe_headers for stage in stages):
|
||||
for stage in stages:
|
||||
start_t, end_t = point_events[stage]
|
||||
region = self._create_stage_region(stage, start_t, end_t, transparency)
|
||||
if region is not None:
|
||||
region.setZValue(-20)
|
||||
plot_item.addItem(region)
|
||||
reg_items.setdefault("real", {}).setdefault(stage, []).append(region)
|
||||
reg_items["real"].setdefault(stage, [])
|
||||
reg_items["real"][stage].append(region)
|
||||
|
||||
@staticmethod
|
||||
def _add_ideal_stage_regions(colors: Dict, plot_item: pg.PlotItem, ideal_data: Dict[str, Any],
|
||||
point_events: Dict[str, List[float]], reg_items: Dict, transparency: int = 125) -> None:
|
||||
def _add_ideal_stage_regions(self,
|
||||
plot_item: pg.PlotItem,
|
||||
ideal_data: dict[str, Any],
|
||||
point_events: dict[str, list[float]],
|
||||
reg_items: dict,
|
||||
transparency: int = 125) -> None:
|
||||
"""
|
||||
Добавляет регионы для идеальных этапов.
|
||||
|
||||
:param colors: Словарь с цветами.
|
||||
:param plot_item: PlotItem для добавления регионов.
|
||||
:param ideal_data: Словарь с идеальными данными, включая "Ideal timings".
|
||||
:param point_events: События с временными рамками.
|
||||
:param reg_items: Словарь для хранения созданных регионов.
|
||||
:param transparency: Прозрачность регионов.
|
||||
"""
|
||||
ideal_timings = ideal_data["Ideal timings"]
|
||||
stages = list(point_events.keys())
|
||||
for i, stage in enumerate(stages):
|
||||
start_t = point_events[stage][0]
|
||||
end_t = start_t + ideal_timings[i]
|
||||
region = PlotItemGenerator._create_stage_region(colors, stage, start_t, end_t, transparency)
|
||||
region = self._create_stage_region(stage, start_t, end_t, transparency)
|
||||
if region:
|
||||
region.setZValue(-10)
|
||||
plot_item.addItem(region)
|
||||
reg_items.setdefault("ideal", {}).setdefault(stage, []).append(region)
|
||||
reg_items["ideal"].setdefault(stage, [])
|
||||
reg_items["ideal"][stage].append(region)
|
||||
|
||||
@staticmethod
|
||||
def _modify_ideal_df(ideal_df: pd.DataFrame, ideal_data: Dict[str, Any], point_events: Dict[str, List[float]]) -> pd.DataFrame:
|
||||
def _add_ideal_signals(self,
|
||||
plot_item: pg.PlotItem,
|
||||
legend_item: pg.LegendItem,
|
||||
ideal_data: dict[str, Any],
|
||||
point_events: dict[str, list[float]],
|
||||
ideal_signals: list[dict[str, Any]],
|
||||
curve_items: dict,
|
||||
is_last: bool) -> None:
|
||||
"""
|
||||
Добавляет идеальные сигналы для каждого этапа к DataFrame.
|
||||
|
||||
Если ideal_df не пуст, добавляет разделитель между сигналами.
|
||||
|
||||
:param ideal_df: DataFrame с идеальными данными.
|
||||
:param ideal_data: Словарь с идеальными данными для этапов.
|
||||
:param point_events: События с временными рамками.
|
||||
:return: Обновленный DataFrame с идеальными сигналами.
|
||||
Добавляет идеальные сигналы для каждого этапа.
|
||||
"""
|
||||
for signal in ideal_signals:
|
||||
for stage in point_events.keys():
|
||||
if not ideal_df.empty:
|
||||
last_time = ideal_df['time'].iloc[-1]
|
||||
separator_row = {col: np.nan if col != "time" else last_time + 0.01 for col in ideal_df.columns}
|
||||
separator_df = pd.DataFrame([separator_row])
|
||||
worker_df = ideal_data[stage].copy(deep=True)
|
||||
worker_df["time"] = worker_df["time"] + point_events[stage][0]
|
||||
ideal_df = pd.concat([ideal_df, separator_df, worker_df], ignore_index=True)
|
||||
else:
|
||||
ideal_df = ideal_data[stage].copy()
|
||||
return ideal_df
|
||||
curve = self._create_curve_ideal(
|
||||
signal,
|
||||
ideal_data[stage],
|
||||
point_events[stage][0],
|
||||
point_events[stage][1]
|
||||
)
|
||||
if curve:
|
||||
curve.setZValue(50)
|
||||
plot_item.addItem(curve)
|
||||
curve_items["ideal"].setdefault(signal["name"], {})
|
||||
curve_items["ideal"][signal["name"]].setdefault(stage, [])
|
||||
curve_items["ideal"][signal["name"]][stage].append(curve)
|
||||
if is_last: legend_item.addItem(curve, "Ideal " + signal["name"])
|
||||
|
||||
@staticmethod
|
||||
def _add_signals(plot_item: pg.PlotItem, dataframe: pd.DataFrame, real_signals: List[Dict[str, Any]],
|
||||
legend: pg.LegendItem, curve_items: Dict) -> None:
|
||||
def _add_real_signals(self,
|
||||
plot_item: pg.PlotItem,
|
||||
dataframe: pd.DataFrame,
|
||||
real_signals: list[dict[str, Any]],
|
||||
legend: pg.LegendItem,
|
||||
curve_items: dict) -> None:
|
||||
"""
|
||||
Добавляет сигналы из DataFrame на PlotItem.
|
||||
|
||||
:param plot_item: PlotItem для добавления сигналов.
|
||||
:param dataframe: DataFrame с данными.
|
||||
:param real_signals: Список описаний сигналов, содержащих имя, ручку (pen) и т.д.
|
||||
:param legend: Легенда для PlotItem.
|
||||
:param curve_items: Словарь для хранения созданных объектов графиков.
|
||||
Добавляет реальные сигналы из dataframe на виджет.
|
||||
"""
|
||||
dataframe_headers = dataframe.columns.tolist()
|
||||
for signal in real_signals:
|
||||
@ -488,60 +145,137 @@ class PlotItemGenerator:
|
||||
plot = plot_item.plot(dataframe["time"], dataframe[signal["name"]], pen=signal["pen"], fast=True)
|
||||
plot.setZValue(0)
|
||||
legend.addItem(plot, signal["name"])
|
||||
curve_items.setdefault(signal["name"], {}) # гарантируем, что ключ существует
|
||||
curve_items[signal["name"]] = plot
|
||||
curve_items["real"].setdefault(signal["name"], {})
|
||||
curve_items["real"][signal["name"]] = plot
|
||||
|
||||
@staticmethod
|
||||
def _update_plots_downsample(plot_item: pg.PlotItem):
|
||||
def _add_performance_label(self,
|
||||
layout: QVBoxLayout,
|
||||
TWC_time: float,
|
||||
ideal_time: float,
|
||||
tesla_time: float,
|
||||
qt_items: dict) -> None:
|
||||
"""
|
||||
Настраивает даунсэмплинг данных на PlotItem в зависимости от видимого диапазона по оси X.
|
||||
|
||||
:param plot_item: PlotItem, для которого устанавливается даунсэмплинг.
|
||||
Добавляет QLabel с информацией о производительности.
|
||||
"""
|
||||
visible_range = plot_item.getViewBox().viewRange()[0]
|
||||
diapason = visible_range[1] - visible_range[0]
|
||||
if diapason >= 30:
|
||||
plot_item.setDownsampling(ds=50, auto=True, mode='peak')
|
||||
elif diapason >= 10:
|
||||
plot_item.setDownsampling(ds=20, auto=True, mode='peak')
|
||||
elif diapason >= 4:
|
||||
plot_item.setDownsampling(ds=10, auto=True, mode='peak')
|
||||
else:
|
||||
plot_item.setDownsampling(ds=1, auto=True, mode='peak')
|
||||
tesla_TWC = round((1 - TWC_time/tesla_time)*100, 2) if tesla_time else 0.0
|
||||
tesla_ideal = round((1 - ideal_time/tesla_time)*100, 2) if tesla_time else 0.0
|
||||
TWC_ideal = round((ideal_time/TWC_time)*100, 2) if TWC_time else 0.0
|
||||
|
||||
@staticmethod
|
||||
def _add_force_accuracy_region(event: List[float], force: float, plot_item: pg.PlotItem) -> None:
|
||||
label_widget = QWidget()
|
||||
label_layout = QHBoxLayout(label_widget)
|
||||
start_label = QLabel("Сокращение длительности: ")
|
||||
real_label = QLabel(f"фактическое = {tesla_TWC} % ")
|
||||
if not tesla_TWC or not TWC_time: real_label.setVisible(False)
|
||||
ideal_label = QLabel(f"идеальное = {tesla_ideal} % ")
|
||||
if not tesla_ideal: ideal_label.setVisible(False)
|
||||
kdip_label = QLabel(f"КДИП = {TWC_ideal}% ")
|
||||
if not TWC_ideal: kdip_label.setVisible(False)
|
||||
label_layout.addWidget(start_label, alignment=Qt.AlignLeft)
|
||||
label_layout.addWidget(real_label, alignment=Qt.AlignLeft)
|
||||
label_layout.addWidget(ideal_label, alignment=Qt.AlignLeft)
|
||||
label_layout.addWidget(kdip_label, alignment=Qt.AlignLeft)
|
||||
spacer = QSpacerItem(1, 1, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||
label_layout.addSpacerItem(spacer)
|
||||
|
||||
self.set_style(label_widget)
|
||||
layout.addWidget(label_widget)
|
||||
|
||||
qt_items["performance label"] = label_widget
|
||||
qt_items["real performance"] = real_label
|
||||
qt_items["ideal performance"] = ideal_label
|
||||
qt_items["real to ideal performance"] = kdip_label
|
||||
|
||||
|
||||
def _build_widget(self, data: list[Any]) -> QWidget:
|
||||
"""
|
||||
Добавляет прямоугольную область для отображения точности силы на PlotItem.
|
||||
|
||||
:param event: Список [начало, конец] события.
|
||||
:param force: Значение силы.
|
||||
:param plot_item: PlotItem для добавления прямоугольника.
|
||||
Собирает графический виджет для одного набора данных.
|
||||
Параметр `data` предполагается списком: [dataframe, points_pocket, useful_data].
|
||||
"""
|
||||
modifier = 0.05
|
||||
x1 = event[0]
|
||||
dx = event[1] - x1
|
||||
y1 = force * (1 - modifier)
|
||||
dy = force * (2 * modifier)
|
||||
result_widget = QWidget()
|
||||
result_layout = QVBoxLayout(result_widget)
|
||||
plot_layout = pg.GraphicsLayoutWidget()
|
||||
reg_items = {"real":{}, "ideal":{}}
|
||||
curve_items = {"real":{}, "ideal":{}}
|
||||
qt_items = {}
|
||||
|
||||
rect_item = QGraphicsRectItem(x1, y1, dx, dy)
|
||||
rect_item.setZValue(-5)
|
||||
rect_item.setBrush(pg.mkBrush((0, 255, 0, 50)))
|
||||
rect_item.setPen(pg.mkPen('black', width=0))
|
||||
plot_item.addItem(rect_item)
|
||||
dataframe, points_pocket, useful_data = data
|
||||
tesla_time = useful_data["tesla_time"]
|
||||
gun_range = useful_data["range"]
|
||||
|
||||
@staticmethod
|
||||
def _add_workpiece(point_data: PointPassport, plot_item: pg.PlotItem) -> None:
|
||||
"""
|
||||
Добавляет область, обозначающую заготовку, на PlotItem.
|
||||
k_hardness = useful_data["k_hardness"]
|
||||
|
||||
:param point_data: Объект PointPassport с данными точки.
|
||||
:param plot_item: PlotItem для добавления области.
|
||||
"""
|
||||
x1 = point_data.events["Closing"][0]
|
||||
dx = point_data.events["Relief"][1] - x1
|
||||
y1 = point_data.useful_data["part_pos"] * 1000
|
||||
dy = point_data.useful_data["thickness"] * 1000
|
||||
dat_is_none = dataframe is None
|
||||
|
||||
widget_steps = len(self._plt_channels)
|
||||
|
||||
if not dat_is_none:
|
||||
dataframe_headers = dataframe.columns.tolist()
|
||||
point_steps = len(points_pocket)
|
||||
else: point_steps = 1
|
||||
|
||||
for widget_num, (channel, description) in enumerate(self._plt_channels.items()):
|
||||
plot_item, legend = self._init_plot_item(title=channel)
|
||||
settings = description["Settings"]
|
||||
global_shift = 0
|
||||
TWC_time = 0.0
|
||||
ideal_time = 0.0
|
||||
worst_perf = 2
|
||||
|
||||
# TODO: рассчитать корректный параметр range
|
||||
if settings["mirror ME"] and not dat_is_none:
|
||||
dataframe = self._mirror_shift_data(
|
||||
"ME",
|
||||
description["Real_signals"],
|
||||
dataframe,
|
||||
gun_range
|
||||
)
|
||||
|
||||
# Итерация по точкам
|
||||
for cur_point, point_data in enumerate(points_pocket):
|
||||
# point_data структура: [point_timeframe, ideal_data, point_events, useful_p_data]
|
||||
point_timeframe, ideal_dat, point_events, useful_p_data = point_data
|
||||
ideal_data = copy.deepcopy(ideal_dat)
|
||||
|
||||
if dat_is_none:
|
||||
worst_timeframe = point_timeframe = [global_shift, global_shift+ ideal_data["Ideal cycle"]]
|
||||
point_events = {}
|
||||
keys = list(ideal_data.keys())
|
||||
shift = 0
|
||||
for i, time in enumerate(ideal_data["Ideal timings"]):
|
||||
point_events[keys[i]] = [global_shift+shift, global_shift+time+shift]
|
||||
shift += time
|
||||
global_shift +=ideal_data["Ideal cycle"]
|
||||
|
||||
# TODO: проверить корректность расчетов
|
||||
if False and settings["force compensation FE"] and not dat_is_none:
|
||||
force = useful_p_data["force"]
|
||||
F_comp = - force/k_hardness
|
||||
point_idxs = dataframe[(dataframe["time"] >= point_timeframe[0]) & (dataframe["time"] <= point_timeframe[1])].index
|
||||
|
||||
dataframe.loc[point_idxs] = self._shift_data("FE", description["Real_signals"], dataframe.loc[point_idxs], F_comp)
|
||||
|
||||
# Модифицируем данные для отображения гарфика
|
||||
if settings["ideals"] and settings["mirror ME"]:
|
||||
for stage in point_events.keys():
|
||||
ideal_data[stage] = (
|
||||
self._mirror_shift_ideal("ME", "FE", description["Ideal_signals"],
|
||||
ideal_data[stage],
|
||||
gun_range, useful_p_data["P1"]*1000, useful_p_data["P2"]*1000)
|
||||
)
|
||||
|
||||
# Добавляем реальные стадии
|
||||
if settings["stages"] and not dat_is_none:
|
||||
self._add_stage_regions(plot_item, point_events, dataframe_headers, reg_items, 75)
|
||||
|
||||
if settings["workpiece"]:
|
||||
#x1 = point_timeframe[0]
|
||||
#dx = point_timeframe[1] - x1
|
||||
|
||||
x1 = point_events["Closing"][0]
|
||||
dx = point_events["Relief"][1] - x1
|
||||
|
||||
y1 = useful_p_data["position"]*1000
|
||||
dy = useful_p_data["thickness"]*1000
|
||||
|
||||
rect_item = QGraphicsRectItem(x1, y1, dx, dy)
|
||||
rect_item.setZValue(-5)
|
||||
@ -549,139 +283,110 @@ class PlotItemGenerator:
|
||||
rect_item.setPen(pg.mkPen('black', width=3))
|
||||
plot_item.addItem(rect_item)
|
||||
|
||||
if settings["force accuracy"]and not dat_is_none:
|
||||
modifier = 0.05
|
||||
|
||||
@staticmethod
|
||||
def _generate_synthetic_events(global_shift:float, ideal_data: Dict) -> Tuple[float, Dict, List[float]]:
|
||||
"""
|
||||
Генерирует синтетические события для случая, когда данные отсутствуют.
|
||||
x1 = point_events["Welding"][0]
|
||||
dx = point_events["Welding"][1] - x1
|
||||
force = useful_p_data["force"]
|
||||
y1 = force*(1-modifier)
|
||||
dy = force*(2*modifier)
|
||||
|
||||
:param global_shift: Смещение точки по линии времени относительно нуля.
|
||||
:param ideal_data: Словарь с идеальными данными, содержащий "Ideal cycle" и "Ideal timings".
|
||||
:return: Кортеж (обновленные global_shift, сгенерированные события, временной интервал точки).
|
||||
"""
|
||||
point_timeframe = [global_shift, global_shift + ideal_data["Ideal cycle"]]
|
||||
point_events = {}
|
||||
keys = list(ideal_data.keys())
|
||||
shift = 0
|
||||
for i, time in enumerate(ideal_data["Ideal timings"]):
|
||||
point_events[keys[i]] = [global_shift + shift, global_shift + time + shift]
|
||||
shift += time
|
||||
global_shift += ideal_data["Ideal cycle"]
|
||||
return global_shift, point_events, point_timeframe
|
||||
rect_item = QGraphicsRectItem(x1, y1, dx, dy)
|
||||
rect_item.setZValue(-5)
|
||||
rect_item.setBrush(pg.mkBrush((0,255,0, 50)))
|
||||
rect_item.setPen(pg.mkPen('black', width=0))
|
||||
plot_item.addItem(rect_item)
|
||||
|
||||
@staticmethod
|
||||
def _apply_force_compensation(force: float, k_hardness: float, dataframe: pd.DataFrame,
|
||||
point_timeframe: List[float], real_signals: List[Dict]) -> pd.DataFrame:
|
||||
"""
|
||||
Применяет компенсацию силы к данным в заданном интервале времени.
|
||||
# Добавляем идеальные стадии и идеальные сигналы
|
||||
if settings["ideals"]:
|
||||
is_last_point = (cur_point == len(points_pocket) - 1)
|
||||
self._add_ideal_stage_regions(plot_item, ideal_data, point_events, reg_items, 100)
|
||||
self._add_ideal_signals(plot_item, legend, ideal_data, point_events, description["Ideal_signals"], curve_items, is_last_point)
|
||||
|
||||
:param force: Значение силы.
|
||||
:param k_hardness: Коэффициент твердости.
|
||||
:param dataframe: DataFrame с данными.
|
||||
:param point_timeframe: Временной интервал, для которого производится компенсация.
|
||||
:param real_signals: Список описаний реальных сигналов.
|
||||
:return: Измененный DataFrame с примененной компенсацией.
|
||||
"""
|
||||
F_comp = -force / k_hardness
|
||||
point_idxs = dataframe[(dataframe["time"] >= point_timeframe[0]) & (dataframe["time"] <= point_timeframe[1])].index
|
||||
dataframe.loc[point_idxs] = PlotItemGenerator._shift_data("FE", real_signals, dataframe.loc[point_idxs],
|
||||
lambda x: x + F_comp)
|
||||
return dataframe
|
||||
# Подсчёт производительности
|
||||
if settings["performance"]:
|
||||
is_last_point = (cur_point == len(points_pocket) - 1)
|
||||
if is_last_point:
|
||||
if not dat_is_none: TWC_delta = sum([point_events[stage][1] - point_events[stage][0]
|
||||
for stage in ["Closing", "Squeeze", "Welding"]])
|
||||
else: TWC_delta = 0
|
||||
ideal_delta = sum(ideal_data["Ideal timings"][0:3])
|
||||
else:
|
||||
if not dat_is_none: TWC_delta = point_timeframe[1] - point_timeframe[0]
|
||||
else: TWC_delta = 0
|
||||
ideal_delta = ideal_data["Ideal cycle"]
|
||||
|
||||
@staticmethod
|
||||
def _parent_ideal_mode() -> bool:
|
||||
"""
|
||||
Вспомогательный метод-заглушка для определения режима идеальных данных.
|
||||
(При необходимости можно реализовать проверку из родительского класса)
|
||||
"""
|
||||
# Здесь можно вернуть значение из родительского контекста, если требуется
|
||||
return False
|
||||
TWC_time += TWC_delta
|
||||
ideal_time += ideal_delta
|
||||
curr_perf = ideal_delta/TWC_delta if TWC_delta != 0 else 1
|
||||
|
||||
if curr_perf < worst_perf:
|
||||
worst_perf = curr_perf
|
||||
worst_timeframe = point_timeframe
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Класс NavigatorPlot – навигатор для графика, позволяющий управлять областью просмотра
|
||||
# =============================================================================
|
||||
class NavigatorPlot(pg.PlotItem):
|
||||
"""
|
||||
График-навигатор, отображающий уменьшенную копию данных основного графика,
|
||||
позволяющий синхронизировать область просмотра основного графика с регионом навигатора.
|
||||
"""
|
||||
# Считаем прогресс
|
||||
self._update_status(widget_steps, point_steps, widget_num, cur_point)
|
||||
|
||||
def __init__(self, time_region: Tuple[float, float], main_plot: pg.PlotItem):
|
||||
"""
|
||||
Инициализирует NavigatorPlot, синхронизируя его с основным графиком.
|
||||
# Добавляем реальные сигналы
|
||||
if not dat_is_none:
|
||||
self._add_real_signals(plot_item, dataframe, description["Real_signals"], legend, curve_items)
|
||||
if widget_num == 0:
|
||||
main_plot = plot_item
|
||||
else:
|
||||
# Связываем остальные графики с основным графиком
|
||||
plot_item.setXLink(main_plot)
|
||||
|
||||
:param time_region: Временной интервал, который должен быть выделен.
|
||||
:param main_plot: Основной PlotItem, с которым будет синхронизация.
|
||||
"""
|
||||
super().__init__()
|
||||
self._init_navigator(time_region, main_plot)
|
||||
self._init_syncranisation(main_plot)
|
||||
if settings["performance"]:
|
||||
self._add_performance_label(result_layout, TWC_time, ideal_time, tesla_time, qt_items)
|
||||
|
||||
@staticmethod
|
||||
def _sync_main_plot_with_navigator(main_plot: pg.PlotItem, region: pg.LinearRegionItem) -> None:
|
||||
"""
|
||||
Синхронизирует область просмотра основного графика с регионом навигатора.
|
||||
plot_layout.addItem(plot_item, widget_num, 0)
|
||||
|
||||
:param main_plot: Основной график.
|
||||
:param region: Регион навигатора.
|
||||
"""
|
||||
x_min, x_max = region.getRegion()
|
||||
if main_plot:
|
||||
main_plot.blockSignals(True)
|
||||
main_plot.setXRange(x_min, x_max, padding=0)
|
||||
main_plot.blockSignals(False)
|
||||
navigator, ROI_region = self._create_navigator(worst_timeframe, main_plot)
|
||||
if navigator is not None:
|
||||
plot_layout.addItem(navigator, widget_num+1, 0)
|
||||
self._sync_main_plot_with_navigator(main_plot, ROI_region)
|
||||
main_plot.sigXRangeChanged.connect(lambda _, plot=main_plot, region=ROI_region: self._sync_navigator_with_main(main_plot=plot, region=region))
|
||||
|
||||
@staticmethod
|
||||
def _sync_navigator_with_main(main_plot: pg.PlotItem, region: pg.LinearRegionItem):
|
||||
"""
|
||||
Синхронизирует регион навигатора с текущей областью просмотра основного графика.
|
||||
result_layout.addWidget(plot_layout)
|
||||
return result_widget, reg_items, curve_items, qt_items
|
||||
|
||||
:param main_plot: Основной график.
|
||||
:param region: Регион навигатора.
|
||||
def build(self, data: list[list[Any]]) -> None:
|
||||
"""
|
||||
if region:
|
||||
x_min, x_max = main_plot.getViewBox().viewRange()[0]
|
||||
region.blockSignals(True)
|
||||
region.setRegion([x_min, x_max])
|
||||
region.blockSignals(False)
|
||||
Создает набор виджетов по предоставленному списку данных.
|
||||
Предполагается, что data — это список элементов вида:
|
||||
[
|
||||
[dataframe, points_pocket, useful_data],
|
||||
[dataframe, points_pocket, useful_data],
|
||||
...
|
||||
]
|
||||
"""
|
||||
try:
|
||||
self._datalen = len(data)
|
||||
widgets_datapack = [self._build_widget(data_sample) for self._datastep, data_sample in enumerate(data)]
|
||||
except:
|
||||
tb = sys.exc_info()[2]
|
||||
tbinfo = traceback.format_tb(tb)[0]
|
||||
pymsg = "Traceback info:\n" + tbinfo + "\nError Info:\n" + str(sys.exc_info()[1])
|
||||
logger.error(pymsg)
|
||||
widgets_datapack = [QLabel(pymsg)]
|
||||
finally:
|
||||
self._mediator.notify(self, widgets_datapack)
|
||||
|
||||
def _update_status(self, widgsteps:int, pointsteps:int, cur_widg:int, cur_point:int):
|
||||
if self._datalen != 0:
|
||||
sycle_start = self._datastep/self._datalen*100 + 1
|
||||
period1 = 100/self._datalen
|
||||
else:
|
||||
sycle_start = 1
|
||||
period1 = 100
|
||||
|
||||
period2 = period1/widgsteps if widgsteps != 0 else period1
|
||||
period3 = period2/pointsteps if pointsteps != 0 else period2
|
||||
|
||||
progress = sycle_start + period2*cur_widg + period3*cur_point
|
||||
self._mediator.update_status(progress)
|
||||
|
||||
def _init_syncranisation(self, main_plot: pg.PlotItem) -> None:
|
||||
"""
|
||||
Настраивает синхронизацию между навигатором и основным графиком.
|
||||
"""
|
||||
self._sync_main_plot_with_navigator(main_plot, self.ROI_region)
|
||||
self.ROI_region.sigRegionChanged.connect(
|
||||
lambda: self._sync_main_plot_with_navigator(main_plot, self.ROI_region)
|
||||
)
|
||||
main_plot.sigXRangeChanged.connect(
|
||||
lambda: self._sync_navigator_with_main(main_plot, self.ROI_region)
|
||||
)
|
||||
|
||||
def _init_navigator(self, time_region: Tuple[float, float], main_plot: pg.PlotItem) -> None:
|
||||
"""
|
||||
Создает график-навигатор, отображающий уменьшенную копию данных.
|
||||
|
||||
:param time_region: Временной интервал, который необходимо выделить.
|
||||
:param main_plot: Основной график, из которого будут извлекаться данные.
|
||||
"""
|
||||
self.setTitle("Navigator")
|
||||
self.setFixedHeight(100)
|
||||
# Извлекаем данные из каждого элемента основного графика и даунсэмплируем их
|
||||
for curve in main_plot.listDataItems():
|
||||
x, y = curve.getData()
|
||||
curve_name = curve.opts.get("name", None)
|
||||
signal_pen = curve.opts.get("pen", None)
|
||||
plot = self.plot(x, y, pen=signal_pen, name=curve_name)
|
||||
#plot.setDownsampling(ds = 10, auto = True, method = 'peak')
|
||||
# Создаем регион для выделения области
|
||||
self.ROI_region = pg.LinearRegionItem(
|
||||
movable=True,
|
||||
brush=pg.mkBrush(0, 0, 255, 100),
|
||||
pen=pg.mkPen(width=4)
|
||||
)
|
||||
max_bound = x[-1] if x.any() else 50
|
||||
self.ROI_region.setBounds([0, max_bound])
|
||||
self.ROI_region.setRegion(time_region)
|
||||
self.addItem(self.ROI_region)
|
||||
self.getViewBox().setLimits(xMin=0, xMax=max_bound)
|
||||
|
||||
@ -1,33 +1,42 @@
|
||||
import traceback
|
||||
import sys
|
||||
from typing import Union
|
||||
|
||||
import pyqtgraph as pg
|
||||
from pyqtgraph.parametertree import Parameter, ParameterTree
|
||||
from typing import Union
|
||||
from PyQt5 import QtWidgets
|
||||
import traceback
|
||||
import sys
|
||||
from loguru import logger
|
||||
|
||||
|
||||
|
||||
class ReportSettings(QtWidgets.QWidget):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
#self._tab_cashe = LRUCache(maxsize=1000)
|
||||
|
||||
def build(self, reg_items: dict, curve_items: dict, qt_items: dict) -> None:
|
||||
def build(self, index, reg_items: dict, curve_items: dict, qt_items: dict) -> None:
|
||||
"""Создает ParameterTree для элементов всех графиков выбранной вкладки"""
|
||||
try:
|
||||
self._clear()
|
||||
param_tree = ParameterTree()
|
||||
layout = self.layout()
|
||||
layout.addWidget(param_tree)
|
||||
"""if index in self._tab_cashe:
|
||||
body = self._tab_cashe[index]
|
||||
else:
|
||||
body= [
|
||||
ReportSettings._generate_reg_params(reg_items),
|
||||
ReportSettings._generate_curve_params(curve_items),
|
||||
ReportSettings._generate_qt_params(qt_items)
|
||||
self._generate_reg_params(reg_items),
|
||||
self._generate_curve_params(curve_items)
|
||||
]
|
||||
self._tab_cashe[index] = body"""
|
||||
body= [
|
||||
self._generate_reg_params(reg_items),
|
||||
self._generate_curve_params(curve_items),
|
||||
self._generate_qt_params(qt_items)
|
||||
]
|
||||
# Добавляем параметры в дерево
|
||||
params = Parameter.create(name='params', type='group', children=body)
|
||||
params.sigTreeStateChanged.connect(
|
||||
lambda: ReportSettings._update_settings(reg_items, curve_items, qt_items, params)
|
||||
lambda: self._update_settings(reg_items, curve_items, qt_items, params)
|
||||
)
|
||||
param_tree.setParameters(params, showTop=False)
|
||||
except:
|
||||
@ -50,52 +59,69 @@ class ReportSettings(QtWidgets.QWidget):
|
||||
else:
|
||||
self.setLayout(QtWidgets.QVBoxLayout())
|
||||
|
||||
@staticmethod
|
||||
def _generate_qt_params(qt_items: dict) -> dict:
|
||||
def _generate_qt_params(self, qt_items: dict) -> dict:
|
||||
"""Создает qt элементы"""
|
||||
res = {'name': 'Qt elements', 'type': 'group', 'children':[
|
||||
{'name': key, 'type': 'group', 'children': ReportSettings._create_qt_samples(item)} for key, item in qt_items.items()
|
||||
{'name': key, 'type': 'group', 'children': self._create_qt_samples(item)} for key, item in qt_items.items()
|
||||
]}
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def _create_qt_samples(item: Union[QtWidgets.QWidget, QtWidgets.QLabel]) -> dict:
|
||||
def _create_qt_samples(self, item: Union[QtWidgets.QWidget, QtWidgets.QLabel]) -> dict:
|
||||
visibility = item.isVisible()
|
||||
return [
|
||||
{'name': 'Visibility', 'type': 'bool', 'value': visibility}
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _generate_reg_params(reg_items: dict) -> dict:
|
||||
def _generate_reg_params(self,
|
||||
reg_items: dict) -> dict:
|
||||
|
||||
"""Созадет реальные и идеальные секторы"""
|
||||
|
||||
res = {'name': 'Sectors', 'type': 'group', 'children': [
|
||||
{'name': 'Real sectors', 'type': 'group', 'children': ReportSettings._create_samples(reg_items["real"])},
|
||||
{'name': 'Ideal sectors', 'type': 'group', 'children': ReportSettings._create_samples(reg_items["ideal"])},
|
||||
{'name': 'Real sectors', 'type': 'group', 'children': self._create_samples(reg_items["real"])},
|
||||
{'name': 'Ideal sectors', 'type': 'group', 'children': self._create_samples(reg_items["ideal"])},
|
||||
]}
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def _generate_curve_params(curve_items: dict) -> dict:
|
||||
def _generate_curve_params(self,
|
||||
curve_items: dict) -> dict:
|
||||
|
||||
"""Создает реальные и идеальные линии графиков"""
|
||||
|
||||
res = {'name': 'Plots', 'type': 'group', 'children': [
|
||||
{'name': 'Real plots', 'type': 'group', 'children': ReportSettings._create_samples(curve_items["real"])},
|
||||
{'name': 'Ideal plots', 'type': 'group', 'children': ReportSettings._create_samples(curve_items["ideal"])},
|
||||
{'name': 'Real plots', 'type': 'group', 'children': self._create_samples(curve_items["real"])},
|
||||
{'name': 'Ideal plots', 'type': 'group', 'children': self._create_ideal_curves(curve_items["ideal"])},
|
||||
]}
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def _create_samples(sector: dict) -> list[dict]:
|
||||
"""Создает список представленных элементов с их параметрами"""
|
||||
def _create_ideal_curves(self,
|
||||
curve: dict) -> list[dict]:
|
||||
|
||||
"""Создает секторы с этапами циклограммы"""
|
||||
|
||||
res = []
|
||||
for key, item in sector.items():
|
||||
sample = item[0] if type(item) == list else item
|
||||
param = {'name': key, 'type': 'group', 'children': ReportSettings._create_settings(sample)}
|
||||
for key, item in curve.items():
|
||||
param = {'name': key, 'type': 'group', 'children': self._create_samples(item)}
|
||||
res.append(param)
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def _create_settings(item: Union[pg.LinearRegionItem, pg.PlotDataItem]) -> list[dict]:
|
||||
def _create_samples(self,
|
||||
sector: dict) -> list[dict]:
|
||||
|
||||
"""Создает список представленных элементов с их параметрами"""
|
||||
|
||||
res = []
|
||||
for key, item in sector.items():
|
||||
sample = item[0] if type(item) == list else item
|
||||
param = {'name': key, 'type': 'group', 'children': self._create_settings(sample)}
|
||||
res.append(param)
|
||||
return res
|
||||
|
||||
def _create_settings(self,
|
||||
item: Union[pg.LinearRegionItem, pg.PlotDataItem]) -> list[dict]:
|
||||
|
||||
"""Получает настройки для элемента"""
|
||||
|
||||
if type(item) == pg.LinearRegionItem:
|
||||
pen = item.lines[0].pen
|
||||
brush = item.brush
|
||||
@ -114,12 +140,14 @@ class ReportSettings(QtWidgets.QWidget):
|
||||
{'name': 'Fill color', 'type': 'color', 'value': fill_color},
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _update_settings(reg_items: dict,
|
||||
def _update_settings(self,
|
||||
reg_items: dict,
|
||||
curve_items: dict,
|
||||
qt_items: dict,
|
||||
params: Parameter) -> None:
|
||||
|
||||
"""Задает параметры элементов в соответствии с paramTree"""
|
||||
|
||||
real_sectors = params.child("Sectors").child("Real sectors")
|
||||
ideal_sectors = params.child("Sectors").child("Ideal sectors")
|
||||
|
||||
@ -128,17 +156,21 @@ class ReportSettings(QtWidgets.QWidget):
|
||||
|
||||
qt_settings = params.child("Qt elements")
|
||||
|
||||
ReportSettings._set_sector_settings(reg_items["real"], real_sectors)
|
||||
ReportSettings._set_sector_settings(reg_items["ideal"], ideal_sectors)
|
||||
self._set_sector_settings(reg_items["real"], real_sectors)
|
||||
self._set_sector_settings(reg_items["ideal"], ideal_sectors)
|
||||
|
||||
ReportSettings._set_plot_settings(curve_items["real"], real_plots)
|
||||
ReportSettings._set_plot_settings(curve_items["ideal"], ideal_plots)
|
||||
ReportSettings._set_qt_settings(qt_items, qt_settings)
|
||||
self._set_plot_settings(curve_items["real"], real_plots)
|
||||
for key, item_dict in curve_items["ideal"].items():
|
||||
self._set_plot_settings(item_dict, ideal_plots.child(key))
|
||||
|
||||
@staticmethod
|
||||
def _set_sector_settings(sectors: dict,
|
||||
self._set_qt_settings(qt_items, qt_settings)
|
||||
|
||||
def _set_sector_settings(self,
|
||||
sectors: dict,
|
||||
settings: Parameter) -> None:
|
||||
|
||||
"""Задает параметры секторов в соответствии с настройками"""
|
||||
|
||||
for key, item in sectors.items():
|
||||
sample = settings.child(key)
|
||||
line_color = sample.child("Line color").value()
|
||||
@ -154,10 +186,12 @@ class ReportSettings(QtWidgets.QWidget):
|
||||
reg.lines[1].setPen(pen)
|
||||
reg.setBrush(brush)
|
||||
|
||||
@staticmethod
|
||||
def _set_plot_settings(curves: dict,
|
||||
def _set_plot_settings(self,
|
||||
curves: dict,
|
||||
settings: Parameter) -> None:
|
||||
|
||||
"""Задает параметры кривых в соответствии с настройками"""
|
||||
|
||||
for key, item in curves.items():
|
||||
sample = settings.child(key)
|
||||
line_color = sample.child("Line color").value()
|
||||
@ -172,10 +206,12 @@ class ReportSettings(QtWidgets.QWidget):
|
||||
item.setVisible(visibility)
|
||||
item.setPen(pen)
|
||||
|
||||
@staticmethod
|
||||
def _set_qt_settings(qt_items: dict,
|
||||
def _set_qt_settings(self,
|
||||
qt_items: dict,
|
||||
settings: Parameter) -> None:
|
||||
|
||||
"""Задает параметры Qt элементов в соответствии с настройками"""
|
||||
|
||||
for key, item in qt_items.items():
|
||||
sample = settings.child(key)
|
||||
visibility = sample.child("Visibility").value()
|
||||
@ -1,45 +1,40 @@
|
||||
import locale
|
||||
from typing import Callable, Optional, Any
|
||||
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget, QPushButton, QLineEdit, QHBoxLayout, QVBoxLayout, QLabel,
|
||||
QTableWidget, QTableWidgetItem, QStyledItemDelegate, QRadioButton, QGroupBox, QDoubleSpinBox
|
||||
)
|
||||
from PyQt5.QtWidgets import (QWidget, QPushButton,
|
||||
QLineEdit, QHBoxLayout,
|
||||
QVBoxLayout, QLabel,
|
||||
QTableWidget, QTableWidgetItem,
|
||||
QStyledItemDelegate)
|
||||
from PyQt5.QtGui import QIntValidator, QDoubleValidator
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from utils.json_tools import read_json, write_json
|
||||
from utils import qt_settings as qts
|
||||
|
||||
|
||||
class SettingsWindow(QWidget):
|
||||
class settingsWindow(QWidget):
|
||||
def __init__(self, path: str, name: str, upd_func: Callable[[], None], names: dict):
|
||||
"""
|
||||
Окно настроек для редактирования параметров.
|
||||
|
||||
Загружает и сохраняет параметры из JSON-файла, отображает их в таблице,
|
||||
позволяет редактировать значения и расширять число параметров.
|
||||
|
||||
:param path: Путь к JSON-файлу с настройками.
|
||||
:param path: Путь к файлу настроек (JSON).
|
||||
:param name: Название набора настроек.
|
||||
:param upd_func: Функция-коллбэк для обновления после сохранения.
|
||||
:param associated_names: Словарь отображения ключей настроек в читаемые имена.
|
||||
:param upd_func: Функция обновления (коллбэк).
|
||||
"""
|
||||
def __init__(self, path: str, name: str, upd_func: Callable[[], None], associated_names: dict):
|
||||
super().__init__()
|
||||
self._settings_path = path
|
||||
self._settingsPath = path
|
||||
self._name = name
|
||||
self._data: dict[str, list[Any]] = {}
|
||||
self._upd_func = upd_func
|
||||
self._associated_names = associated_names
|
||||
|
||||
self._num_points: Optional[QLineEdit] = None
|
||||
self._param_table: Optional[QTableWidget] = None
|
||||
self._assosiated_names = names
|
||||
|
||||
self._init_ui()
|
||||
self.load_settings()
|
||||
self._populate_table()
|
||||
self._init_ui()
|
||||
|
||||
def load_settings(self) -> None:
|
||||
"""Загружает настройки из JSON-файла."""
|
||||
data = read_json(self._settings_path)
|
||||
data = read_json(self._settingsPath)
|
||||
if isinstance(data, dict):
|
||||
self._data = data
|
||||
else:
|
||||
@ -47,19 +42,17 @@ class SettingsWindow(QWidget):
|
||||
|
||||
def write_settings(self) -> None:
|
||||
"""Записывает текущие настройки в JSON-файл."""
|
||||
write_json(self._settings_path, self._data)
|
||||
write_json(self._settingsPath, self._data)
|
||||
|
||||
def get_params(self) -> dict:
|
||||
def getParams(self) -> dict:
|
||||
"""Возвращает текущий словарь параметров."""
|
||||
return self._data
|
||||
|
||||
def _init_ui(self) -> None:
|
||||
"""Инициализирует пользовательский интерфейс: кнопки, поля ввода, таблицу."""
|
||||
# Кнопки управления
|
||||
"""Инициализирует UI: кнопки, поля ввода, таблицу."""
|
||||
save_button = QPushButton("Save")
|
||||
restore_button = QPushButton("Restore")
|
||||
|
||||
# Поле для ввода количества точек сварки
|
||||
self._num_points = QLineEdit()
|
||||
self._num_points.setPlaceholderText("Enter the number of welding points")
|
||||
self._num_points.setValidator(QIntValidator())
|
||||
@ -69,16 +62,13 @@ class SettingsWindow(QWidget):
|
||||
control_layout.addWidget(restore_button)
|
||||
control_layout.addWidget(self._num_points)
|
||||
|
||||
# Подключение сигналов к слотам
|
||||
save_button.pressed.connect(self._save)
|
||||
restore_button.pressed.connect(self._restore)
|
||||
self._num_points.editingFinished.connect(self._expand)
|
||||
|
||||
# Таблица для отображения параметров
|
||||
self._param_table = QTableWidget()
|
||||
self._populate_table()
|
||||
|
||||
|
||||
# Основной вертикальный макет
|
||||
layout = QVBoxLayout()
|
||||
header = QLabel(self._name)
|
||||
layout.addWidget(header)
|
||||
@ -88,18 +78,14 @@ class SettingsWindow(QWidget):
|
||||
self.setStyleSheet(qts.dark_style)
|
||||
|
||||
def _populate_table(self) -> None:
|
||||
"""
|
||||
Заполняет таблицу значениями из self._data.
|
||||
|
||||
Если данных нет, таблица очищается. Для каждого параметра устанавливается
|
||||
делегат в зависимости от типа данных (int, float, str).
|
||||
"""
|
||||
"""Заполняет таблицу значениями из self._data."""
|
||||
# Если нет данных для заполнения
|
||||
if not self._data:
|
||||
self._param_table.setRowCount(0)
|
||||
self._param_table.setColumnCount(0)
|
||||
return
|
||||
|
||||
# Определяем количество столбцов на основе длины списка первого ключа
|
||||
# Предполагаем, что у всех ключей одинаковая длина списков параметров.
|
||||
first_key = next(iter(self._data), None)
|
||||
if first_key is None:
|
||||
self._param_table.setRowCount(0)
|
||||
@ -109,31 +95,27 @@ class SettingsWindow(QWidget):
|
||||
column_count = len(self._data[first_key])
|
||||
self._param_table.setRowCount(len(self._data))
|
||||
self._param_table.setColumnCount(column_count)
|
||||
headers = [self._associated_names.get(key, key) for key in self._data.keys()]
|
||||
headers = [self._assosiated_names[key] for key in self._data.keys()]
|
||||
self._param_table.setVerticalHeaderLabels(headers)
|
||||
|
||||
# Создаем делегаты для различных типов данных
|
||||
int_delegate = ValidatorDelegate(data_type='int', parent=self._param_table)
|
||||
float_delegate = ValidatorDelegate(data_type='float', parent=self._param_table)
|
||||
str_delegate = ValidatorDelegate(data_type='str', parent=self._param_table)
|
||||
|
||||
# Заполняем таблицу и устанавливаем делегаты для каждой строки
|
||||
for i, (_, items) in enumerate(self._data.items()):
|
||||
for j, item in enumerate(items):
|
||||
self._param_table.setItem(i, j, QTableWidgetItem(str(item)))
|
||||
# Используем тип последнего элемента строки для выбора делегата
|
||||
if isinstance(items[-1], int):
|
||||
|
||||
if type(item) == int:
|
||||
self._param_table.setItemDelegateForRow(i, int_delegate)
|
||||
elif isinstance(items[-1], float):
|
||||
elif type(item) == float:
|
||||
self._param_table.setItemDelegateForRow(i, float_delegate)
|
||||
else:
|
||||
self._param_table.setItemDelegateForRow(i, str_delegate)
|
||||
|
||||
def _save_data(self) -> None:
|
||||
"""
|
||||
Сохраняет текущие параметры из таблицы в self._data,
|
||||
записывает их в JSON-файл и вызывает функцию обновления.
|
||||
"""
|
||||
|
||||
def _save(self) -> None:
|
||||
"""Сохраняет текущие параметры из таблицы в self._data и вызывает _upd_func()."""
|
||||
new_data = {}
|
||||
col_count = self._param_table.columnCount()
|
||||
for i, key in enumerate(self._data.keys()):
|
||||
@ -143,7 +125,7 @@ class SettingsWindow(QWidget):
|
||||
if cell_item is None:
|
||||
continue
|
||||
param_str = cell_item.text()
|
||||
# Для ключа 'trace_storage_path' оставляем строковое значение
|
||||
# Если ключ не trace_storage_path, конвертируем в float
|
||||
if key != "trace_storage_path":
|
||||
try:
|
||||
param = float(param_str)
|
||||
@ -152,31 +134,20 @@ class SettingsWindow(QWidget):
|
||||
else:
|
||||
param = param_str
|
||||
row_data.append(param)
|
||||
|
||||
new_data[key] = row_data
|
||||
|
||||
self._data = new_data
|
||||
|
||||
def _push_data (self) -> None:
|
||||
self.write_settings()
|
||||
self._upd_func()
|
||||
|
||||
def _save(self) -> None:
|
||||
self._save_data()
|
||||
self._push_data()
|
||||
|
||||
def _restore(self) -> None:
|
||||
"""Перезагружает данные из файла и обновляет таблицу."""
|
||||
self.load_settings()
|
||||
self._populate_table()
|
||||
|
||||
def _expand(self) -> None:
|
||||
"""
|
||||
Расширяет количество столбцов таблицы согласно введённому значению.
|
||||
|
||||
Количество столбцов таблицы
|
||||
устанавливается равным заданному значению, а новые ячейки заполняются последним
|
||||
известным значением для соответствующего параметра.
|
||||
"""
|
||||
"""Расширяет количество столбцов таблицы в зависимости от введённого значения."""
|
||||
if not self._num_points:
|
||||
return
|
||||
|
||||
@ -196,41 +167,34 @@ class SettingsWindow(QWidget):
|
||||
|
||||
self._param_table.setColumnCount(desired_columns)
|
||||
|
||||
# Заполнение новых столбцов последним значением для каждого параметра
|
||||
# Новые столбцы заполняем последним известным параметром для каждого ключа
|
||||
for i, (key, items) in enumerate(self._data.items()):
|
||||
# Если нет данных, пропускаем
|
||||
if not items:
|
||||
continue
|
||||
last_value = str(items[-1])
|
||||
for col in range(prev_columns, desired_columns):
|
||||
self._param_table.setItem(i, col, QTableWidgetItem(last_value))
|
||||
# Добавляем новый элемент также в self._data для консистентности
|
||||
# После этого можно будет сохранить при нажатии Save
|
||||
# Дополним также и в self._data
|
||||
additional_count = desired_columns - prev_columns
|
||||
# Приведение типа: для ключа 'trace_storage_path' оставляем строку, иначе преобразуем к float
|
||||
additional_values = (
|
||||
[float(last_value)] * additional_count
|
||||
if key != "trace_storage_path"
|
||||
else [last_value] * additional_count
|
||||
)
|
||||
self._data[key].extend(additional_values)
|
||||
self._data[key].extend([float(last_value) if key != "trace_storage_path" else last_value] * additional_count)
|
||||
|
||||
|
||||
class SystemSettings(SettingsWindow):
|
||||
"""
|
||||
Настройки системы.
|
||||
|
||||
Окно для редактирования параметров, связанных с работой системы.
|
||||
"""
|
||||
def __init__(self, path: str, name: str, upd_func: Callable[[], None]):
|
||||
associated_names = {
|
||||
class SystemSettings(settingsWindow):
|
||||
def __init__(self, path, name, upd_func):
|
||||
assosiated_names = {
|
||||
"trace_storage_path": "Trace path",
|
||||
"monitor_update_period": "Monitoring period",
|
||||
"a_max_1": "Max lin accel FE, m/s^2",
|
||||
"v_max_1": "Max lin speed FE, m/s",
|
||||
"a_max_2": "Max lin accel ME, m/s^2",
|
||||
"v_max_2": "Max lin speed FE, m/s",
|
||||
"a_max_1": "Max linear acceleration FE, m/s^2",
|
||||
"v_max_1": "Max linear speed FE, m/s",
|
||||
"a_max_2":"Max linear acceleration ME, m/s^2",
|
||||
"v_max_2": "Max linear speed ME, m/s",
|
||||
"mass_1": "Mass FE, kg",
|
||||
"mass_2": "Mass ME, kg",
|
||||
"k_hardness_1": "Hardness coef FE, N/m",
|
||||
"k_hardness_2": "Hardness coef ME, N/m",
|
||||
"k_hardness_1": "Hardness coefficient FE, N/m",
|
||||
"k_hardness_2": "Hardness coefficient ME, N/m",
|
||||
"torque_max_1": "Max torque FE, N*m",
|
||||
"torque_max_2": "Max torque ME, N*m",
|
||||
"transmission_ratio_1": "Transmission ratio FE",
|
||||
@ -240,115 +204,21 @@ class SystemSettings(SettingsWindow):
|
||||
"k_prop": "Proportionality factor",
|
||||
"time_capture": "Calculated points per sec",
|
||||
"UML_time_scaler": "UML_time_scaler",
|
||||
"Range ME, mm": "Range ME, mm"
|
||||
"gun_range": "Range ME-FE, mm"
|
||||
}
|
||||
super().__init__(path, name, upd_func, associated_names)
|
||||
super().__init__(path, name, upd_func, assosiated_names)
|
||||
self._num_points.setVisible(False)
|
||||
|
||||
def _init_ui(self) -> None:
|
||||
super()._init_ui()
|
||||
|
||||
performance_layout = QVBoxLayout()
|
||||
self.btn_trace_movement = QRadioButton()
|
||||
self.btn_trace_movement.setText("Use robot movement time from trace (auto set)")
|
||||
self.btn_trace_movement.setChecked(True)
|
||||
|
||||
self.btn_client_movement = QRadioButton()
|
||||
self.btn_client_movement.setText("Use robot movement time from client (manual set)")
|
||||
|
||||
label_time_before_start = QLabel("Additional time before first Squeeze, ms")
|
||||
self.spin_time_before_start = QDoubleSpinBox()
|
||||
self.spin_time_before_start.setRange(0, 100000.000)
|
||||
self.spin_time_before_start.setValue(0)
|
||||
|
||||
label_time_after_end = QLabel("Additional time after last Relief, ms")
|
||||
self.spin_time_after_end = QDoubleSpinBox()
|
||||
self.spin_time_after_end.setRange(0, 100000.000)
|
||||
self.spin_time_after_end.setValue(0)
|
||||
|
||||
label_client_time = QLabel("Client full time, ms")
|
||||
self.spin_client_time = QDoubleSpinBox()
|
||||
self.spin_client_time.setRange(0, 100000.000)
|
||||
self.spin_client_time.setValue(0)
|
||||
|
||||
widget_time_before_start = QWidget()
|
||||
layout_time_before_start = QHBoxLayout(widget_time_before_start)
|
||||
layout_time_before_start.addWidget(label_time_before_start)
|
||||
layout_time_before_start.addWidget(self.spin_time_before_start)
|
||||
|
||||
widget_time_after_end = QWidget()
|
||||
layout_time_after_end = QHBoxLayout(widget_time_after_end)
|
||||
layout_time_after_end.addWidget(label_time_after_end)
|
||||
layout_time_after_end.addWidget(self.spin_time_after_end)
|
||||
|
||||
widget_client_time = QWidget()
|
||||
layout_client_time = QHBoxLayout(widget_client_time)
|
||||
layout_client_time.addWidget(label_client_time)
|
||||
layout_client_time.addWidget(self.spin_client_time)
|
||||
|
||||
performance_layout.addWidget(self.btn_trace_movement)
|
||||
performance_layout.addWidget(self.btn_client_movement)
|
||||
performance_layout.addWidget(widget_time_before_start)
|
||||
performance_layout.addWidget(widget_time_after_end)
|
||||
performance_layout.addWidget(widget_client_time)
|
||||
|
||||
performance_box = QGroupBox()
|
||||
performance_box.setTitle("Performance settings")
|
||||
performance_box.setLayout(performance_layout)
|
||||
|
||||
self.layout().addWidget(performance_box)
|
||||
|
||||
def _add_performance_mode(self) -> None:
|
||||
if self.btn_trace_movement.isChecked():
|
||||
mode = 'trace'
|
||||
elif self.btn_client_movement.isChecked():
|
||||
mode = 'client'
|
||||
else:
|
||||
mode = None
|
||||
self._data["performance_mode"] = [mode]
|
||||
self._data["time_before_start"] = [self.spin_time_before_start.value()/1000]
|
||||
self._data["time_after_end"] = [self.spin_time_after_end.value()/1000]
|
||||
self._data["client_time"] = [self.spin_client_time.value()/1000]
|
||||
|
||||
def _save_data(self) -> None:
|
||||
super()._save_data()
|
||||
self._add_performance_mode()
|
||||
|
||||
def _load_performance_settings(self) -> None:
|
||||
mode = self._data["performance_mode"][0]
|
||||
if mode == 'trace':
|
||||
self.btn_trace_movement.setChecked(True)
|
||||
elif mode == 'client':
|
||||
self.btn_client_movement.setChecked(True)
|
||||
self.spin_time_before_start.setValue(self._data["time_before_start"][0]*1000)
|
||||
self.spin_time_after_end.setValue(self._data["time_after_end"][0]*1000)
|
||||
self.spin_client_time.setValue(self._data["client_time"][0]*1000)
|
||||
|
||||
def load_settings(self) -> None:
|
||||
super().load_settings()
|
||||
try:
|
||||
self._load_performance_settings()
|
||||
except KeyError:
|
||||
self._add_performance_mode()
|
||||
|
||||
def _expand(self) -> None:
|
||||
# Для системных настроек расширение столбцов не требуется.
|
||||
def _expand(self):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class OperatorSettings(SettingsWindow):
|
||||
"""
|
||||
Настройки оператора.
|
||||
|
||||
Окно для редактирования параметров, связанных с работой оператора.
|
||||
"""
|
||||
def __init__(self, path: str, name: str, upd_func: Callable[[], None]):
|
||||
associated_names = {
|
||||
"distance_h_start_1": "Closing start dist FE, m",
|
||||
class OperatorSettings(settingsWindow):
|
||||
def __init__(self, path, name, upd_func):
|
||||
assosiated_names = {
|
||||
"distance_h_start_1": "Closing start dist FE, m" ,
|
||||
"distance_h_start_2": "Closing start dist ME, m",
|
||||
"distance_s_1": "Rob movement start dist FE, m",
|
||||
"distance_s_2": "Rob movement start dist ME, m",
|
||||
"distance_s_1": "Robot movement start dist FE, m",
|
||||
"distance_s_2": "Robot movement start dist ME, m",
|
||||
"distance_l_1": "Max oncoming dist FE, m",
|
||||
"distance_l_2": "Max oncoming dist ME, m",
|
||||
"distance_h_end1": "Oncoming end dist FE, m",
|
||||
@ -357,6 +227,7 @@ class OperatorSettings(SettingsWindow):
|
||||
"time_command": "Communication time compensator, sec",
|
||||
"time_robot_movement": "Rob movement time, sec",
|
||||
"object_thickness": "Workpiece thickness, m",
|
||||
"object_position": "Workpiece position, m",
|
||||
"force_target": "Target force, N",
|
||||
"force_capture": "Capture force, N",
|
||||
"Tesla closing": "Client closing time, sec",
|
||||
@ -365,46 +236,11 @@ class OperatorSettings(SettingsWindow):
|
||||
"Tesla oncomming_relief": "Client moving to next point time, sec",
|
||||
"Tesla summary time": "Client summary time, sec"
|
||||
}
|
||||
super().__init__(path, name, upd_func, associated_names)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class FilterSettings(SettingsWindow):
|
||||
"""
|
||||
Настройки фильтра.
|
||||
|
||||
Окно для редактирования параметров фильтрации.
|
||||
"""
|
||||
def __init__(self, path: str, name: str, upd_func: Callable[[], None]):
|
||||
associated_names = {
|
||||
"act_pos_decrease": "ME max pos change in Relief, mm",
|
||||
"act_vel_min": "Minimum for ME speed in Closing mm/sec",
|
||||
"act_vel_close": "Maximum for ME speed in Squeeze mm/sec",
|
||||
"act_vel_thresh": "ME zero speed threshold mm/sec",
|
||||
"act_vel_negative": "ME Relief speed mm/sec",
|
||||
"rob_vel_thresh": "Robot zero speed threshold mm/sec",
|
||||
"act_force_close": "Maximum ME force in Closing, N",
|
||||
"act_force_weld": "Minimum ME force in Welding, N",
|
||||
"force_increase": "ME force rising speed in Squeeze",
|
||||
"force_decrease": "ME force falling speed in Relief"
|
||||
}
|
||||
super().__init__(path, name, upd_func, associated_names)
|
||||
self._num_points.setVisible(False)
|
||||
|
||||
def _expand(self):
|
||||
# Для настроек фильтра расширение столбцов не требуется.
|
||||
super().__init__(path, name, upd_func, assosiated_names)
|
||||
pass
|
||||
|
||||
|
||||
class ValidatorDelegate(QStyledItemDelegate):
|
||||
"""
|
||||
Валидация для ввода в ячейках таблицы.
|
||||
|
||||
В зависимости от типа данных ('int', 'float' или 'str') устанавливает соответствующий валидатор.
|
||||
"""
|
||||
def __init__(self, data_type='str', parent=None):
|
||||
super().__init__(parent)
|
||||
self.data_type = data_type
|
||||
@ -413,6 +249,8 @@ class ValidatorDelegate(QStyledItemDelegate):
|
||||
elif self.data_type == 'float':
|
||||
self.validator = QDoubleValidator()
|
||||
self.validator.setNotation(QDoubleValidator.StandardNotation)
|
||||
self.validator.setLocale(QtCore.QLocale("en_US"))
|
||||
# locale
|
||||
else:
|
||||
self.validator = None
|
||||
|
||||
@ -420,13 +258,21 @@ class ValidatorDelegate(QStyledItemDelegate):
|
||||
editor = QLineEdit(parent)
|
||||
if self.validator:
|
||||
editor.setValidator(self.validator)
|
||||
# Добавляем обработчик для замены запятой на точку
|
||||
editor.textChanged.connect(self.replace_comma_with_dot)
|
||||
return editor
|
||||
|
||||
def replace_comma_with_dot(self, text):
|
||||
# Заменяем запятую на точку
|
||||
if ',' in text:
|
||||
text = text.replace(',', '.')
|
||||
# Устанавливаем измененный текст обратно в редактор
|
||||
self.sender().setText(text)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import pyqtgraph as pg
|
||||
# Для демонстрации создаем окно настроек с фиктивной функцией обновления и пустым словарем имен
|
||||
app = pg.mkQApp("Parameter Tree Example")
|
||||
window = SettingsWindow('params/operator_params.json', 'operator', lambda: None, {})
|
||||
window.show()
|
||||
window = settingsWindow('params\operator_params.json', 'operator')
|
||||
app.exec()
|
||||
|
||||
|
||||
|
||||
@ -1,459 +0,0 @@
|
||||
from datetime import datetime as dt
|
||||
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget, QPushButton, QMenuBar, QHBoxLayout, QVBoxLayout, QLabel,
|
||||
QDockWidget, QTabWidget, QProgressBar, QAction, QMainWindow, QGridLayout
|
||||
)
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QSizePolicy as QSP
|
||||
|
||||
from gui.settings_window import SystemSettings, OperatorSettings, FilterSettings
|
||||
from gui.report_gui import ReportSettings
|
||||
from base.base import PlotItems
|
||||
|
||||
|
||||
class StartWidget(QWidget):
|
||||
"""
|
||||
Виджет выбора режима работы приложения.
|
||||
|
||||
Содержит кнопки для перехода в различные режимы:
|
||||
- Сканирование папки в реальном времени
|
||||
- Редактор отчётов
|
||||
- Обработка клиентских трейсов
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._build_main_layout()
|
||||
|
||||
def get_widget(self) -> QWidget:
|
||||
"""
|
||||
Возвращает текущий виджет.
|
||||
"""
|
||||
return self
|
||||
|
||||
def _build_main_layout(self) -> None:
|
||||
"""
|
||||
Строит макет виджета с заголовком и кнопками выбора режима.
|
||||
"""
|
||||
self.resize(800, 800)
|
||||
|
||||
# Создаем кнопки для различных режимов работы
|
||||
self.btn_scanning = QPushButton("Real time folder scanning")
|
||||
self.btn_scanning.setFixedWidth(350)
|
||||
self.btn_report = QPushButton("Raport editor")
|
||||
self.btn_report.setFixedWidth(350)
|
||||
self.btn_client = QPushButton("Client trace processor")
|
||||
self.btn_client.setFixedWidth(350)
|
||||
|
||||
# Размещение кнопок в горизонтальном макете
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(2)
|
||||
button_layout.addWidget(self.btn_scanning)
|
||||
button_layout.addWidget(self.btn_report)
|
||||
button_layout.addWidget(self.btn_client)
|
||||
button_container = QWidget()
|
||||
button_container.setLayout(button_layout)
|
||||
|
||||
# Создаем основной вертикальный макет с заголовком и кнопками
|
||||
main_layout = QVBoxLayout()
|
||||
title_label = QLabel("Select work mode")
|
||||
title_label.setStyleSheet(
|
||||
"""QLabel{
|
||||
color: #ffffff;
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
}"""
|
||||
)
|
||||
main_layout.addWidget(title_label, alignment=Qt.AlignCenter)
|
||||
main_layout.addWidget(button_container)
|
||||
self.setLayout(main_layout)
|
||||
|
||||
|
||||
class CustomStatusBar(QWidget):
|
||||
"""
|
||||
Кастомный виджет для статус-бара, отображающий режим, заметку и прогресс.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._build_status_bar()
|
||||
|
||||
def get_widget(self) -> QWidget:
|
||||
"""
|
||||
Возвращает виджет статус-бара.
|
||||
"""
|
||||
return self
|
||||
|
||||
def set_mode(self, msg: str = '') -> None:
|
||||
"""
|
||||
Устанавливает текст режима в статус-баре.
|
||||
|
||||
:param msg: Сообщение для отображения.
|
||||
"""
|
||||
if not isinstance(msg, str):
|
||||
msg = str(msg)
|
||||
self._mode_label.setText(msg)
|
||||
self._mode_label.adjustSize()
|
||||
|
||||
def set_note(self, msg: str = '') -> None:
|
||||
"""
|
||||
Устанавливает текст заметки в статус-баре.
|
||||
|
||||
:param msg: Сообщение для отображения.
|
||||
"""
|
||||
if not isinstance(msg, str):
|
||||
msg = str(msg)
|
||||
self._note_label.setText(msg)
|
||||
self._note_label.adjustSize()
|
||||
|
||||
def set_progress(self, percent: int = 0) -> None:
|
||||
"""
|
||||
Устанавливает значение прогресс-бара.
|
||||
|
||||
:param percent: Значение прогресса (от 0 до 100).
|
||||
"""
|
||||
if not isinstance(percent, int):
|
||||
percent = 0
|
||||
elif percent < 0:
|
||||
percent = 0
|
||||
elif percent > 100:
|
||||
percent = 100
|
||||
self._progress_bar.setValue(percent)
|
||||
|
||||
def _build_status_bar(self) -> None:
|
||||
"""
|
||||
Создает и настраивает компоненты статус-бара.
|
||||
"""
|
||||
# Создаем горизонтальный макет с отступами и промежутками
|
||||
layout = QHBoxLayout()
|
||||
layout.setContentsMargins(10, 1, 10, 1)
|
||||
layout.setSpacing(15)
|
||||
|
||||
# Инициализируем компоненты статус-бара
|
||||
self._mode_label = QLabel()
|
||||
self._note_label = QLabel()
|
||||
self._progress_bar = QProgressBar()
|
||||
self._progress_bar.setRange(0, 100)
|
||||
self._progress_bar.setValue(0)
|
||||
self._progress_bar.setMinimumWidth(250)
|
||||
self._progress_bar.setMaximumHeight(10)
|
||||
self._progress_bar.setTextVisible(False)
|
||||
|
||||
# Устанавливаем политику размеров для компонентов
|
||||
self._mode_label.setSizePolicy(QSP.Policy.Preferred, QSP.Policy.Preferred)
|
||||
self._note_label.setSizePolicy(QSP.Policy.MinimumExpanding, QSP.Policy.Preferred)
|
||||
self._progress_bar.setSizePolicy(QSP.Policy.Fixed, QSP.Policy.Preferred)
|
||||
|
||||
# Добавляем компоненты в макет
|
||||
layout.addWidget(self._progress_bar)
|
||||
layout.addWidget(self._note_label)
|
||||
layout.addStretch(1)
|
||||
layout.addWidget(self._mode_label)
|
||||
|
||||
self.setLayout(layout)
|
||||
self.setSizePolicy(QSP.Policy.Expanding, QSP.Policy.Preferred)
|
||||
|
||||
|
||||
class CustomTabWidget(QTabWidget):
|
||||
"""
|
||||
Кастомный виджет-вкладка.
|
||||
|
||||
Позволяет создавать вкладки с виджетом графика и закрывать их.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._build_tab_widget()
|
||||
|
||||
def get_widget(self) -> QWidget:
|
||||
"""
|
||||
Возвращает виджет.
|
||||
"""
|
||||
return self
|
||||
|
||||
def _build_tab_widget(self) -> None:
|
||||
"""
|
||||
Настраивает виджет вкладок: включает возможность закрытия вкладок.
|
||||
"""
|
||||
self.setTabsClosable(True)
|
||||
self.tabCloseRequested.connect(self.close_tab)
|
||||
|
||||
def create_tab(self, plot_widget: QWidget) -> None:
|
||||
"""
|
||||
Создает новую вкладку с виджетом графика.
|
||||
|
||||
Извлекает из виджета необходимые данные, сохраняет их в свойствах вкладки.
|
||||
|
||||
:param plot_widget: Виджет, содержащий график.
|
||||
"""
|
||||
# Извлекаем объект PlotItems, сохранённый в свойстве виджета
|
||||
plot_items: PlotItems = plot_widget.property("pyqt_container")
|
||||
new_tab = QWidget()
|
||||
new_tab.setProperty("reg_items", plot_items.regions)
|
||||
new_tab.setProperty("curve_items", plot_items.curves)
|
||||
new_tab.setProperty("qt_items", plot_items.qt_items)
|
||||
grid_layout = QGridLayout()
|
||||
grid_layout.addWidget(plot_widget)
|
||||
new_tab.setLayout(grid_layout)
|
||||
tab_label = "SF_trace_" + dt.now().strftime('%Y_%m_%d-%H_%M_%S')
|
||||
self.addTab(new_tab, tab_label)
|
||||
self.setCurrentWidget(new_tab)
|
||||
|
||||
def close_tab(self, index: int) -> None:
|
||||
"""
|
||||
Закрывает вкладку по заданному индексу.
|
||||
|
||||
:param index: Индекс вкладки для закрытия.
|
||||
"""
|
||||
if self.count() > index and index >= 0:
|
||||
self.removeTab(index)
|
||||
|
||||
|
||||
class CustomMenuBar(QMenuBar):
|
||||
"""
|
||||
Кастомное главное меню с док-виджетами для различных настроек.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
operator_settings: OperatorSettings,
|
||||
system_settings: SystemSettings,
|
||||
report_settings: ReportSettings,
|
||||
filter_settings: FilterSettings) -> None:
|
||||
super().__init__()
|
||||
self._operator_settings = operator_settings
|
||||
self._system_settings = system_settings
|
||||
self._report_settings = report_settings
|
||||
self._filter_settings = filter_settings
|
||||
self._build_dock_widgets()
|
||||
self._build_menu()
|
||||
|
||||
def get_widget(self) -> QWidget:
|
||||
"""
|
||||
Возвращает виджет меню.
|
||||
"""
|
||||
return self
|
||||
|
||||
def setup(self, parent: QMainWindow) -> None:
|
||||
"""
|
||||
Добавляет док-виджеты и устанавливает главное меню для главного окна.
|
||||
|
||||
:param parent: Главное окно приложения.
|
||||
"""
|
||||
parent.addDockWidget(Qt.RightDockWidgetArea, self._operator_dock)
|
||||
parent.addDockWidget(Qt.RightDockWidgetArea, self._system_dock)
|
||||
parent.addDockWidget(Qt.RightDockWidgetArea, self._report_dock)
|
||||
parent.addDockWidget(Qt.RightDockWidgetArea, self._filter_dock)
|
||||
parent.setMenuBar(self)
|
||||
|
||||
def _build_dock_widgets(self) -> None:
|
||||
"""
|
||||
Инициализирует док-виджеты для настроек и скрывает их по умолчанию.
|
||||
"""
|
||||
# Док-виджет для настроек оператора
|
||||
self._operator_dock = QDockWidget("Operator Settings", self)
|
||||
self._operator_dock.setWidget(self._operator_settings)
|
||||
self._operator_dock.setObjectName("OperatorSettings")
|
||||
self._operator_dock.hide()
|
||||
|
||||
# Док-виджет для системных настроек
|
||||
self._system_dock = QDockWidget("System Settings", self)
|
||||
self._system_dock.setWidget(self._system_settings)
|
||||
self._system_dock.setObjectName("SystemSettings")
|
||||
self._system_dock.hide()
|
||||
|
||||
# Док-виджет для настроек представления отчёта
|
||||
self._report_dock = QDockWidget("View settings", self)
|
||||
self._report_dock.setWidget(self._report_settings)
|
||||
self._report_dock.setObjectName("ReportSettings")
|
||||
self._report_dock.hide()
|
||||
|
||||
# Док-виджет для фильтр-настроек
|
||||
self._filter_dock = QDockWidget("Filter settings", self)
|
||||
self._filter_dock.setWidget(self._filter_settings)
|
||||
self._filter_dock.setObjectName("FilterSetting")
|
||||
self._filter_dock.hide()
|
||||
|
||||
# Устанавливаем функциональные возможности для каждого док-виджета
|
||||
self._set_dock_features(self._operator_dock)
|
||||
self._set_dock_features(self._system_dock)
|
||||
self._set_dock_features(self._report_dock)
|
||||
self._set_dock_features(self._filter_dock)
|
||||
|
||||
def _build_menu(self) -> None:
|
||||
"""
|
||||
Инициализирует элементы главного меню.
|
||||
"""
|
||||
# Создаем меню для выбора режима и настроек
|
||||
modes_menu = self.addMenu("Mode")
|
||||
settings_menu = self.addMenu("Settings")
|
||||
|
||||
# Действия для переключения режимов
|
||||
self.action_scanning = QAction("Real time folder scanning", self)
|
||||
self.action_report = QAction("Raport editor", self)
|
||||
self.action_client = QAction("Client trace processor", self)
|
||||
|
||||
# Действия для настроек
|
||||
system_settings_action = QAction("System settings", self)
|
||||
system_settings_action.setIcon(QIcon('resources/system_ico.png'))
|
||||
system_settings_action.triggered.connect(lambda: self._toggle_visibility(self._system_dock))
|
||||
|
||||
operator_settings_action = QAction("Operator settings", self)
|
||||
operator_settings_action.setIcon(QIcon('resources/operator_ico.png'))
|
||||
operator_settings_action.triggered.connect(lambda: self._toggle_visibility(self._operator_dock))
|
||||
|
||||
self.action_view_settings = QAction("View settings", self)
|
||||
self.action_view_settings.setIcon(QIcon('resources/view_ico.png'))
|
||||
self.action_view_settings.triggered.connect(lambda: self._toggle_visibility(self._report_dock))
|
||||
|
||||
self.action_filter_settings = QAction("Filter settings", self)
|
||||
self.action_filter_settings.triggered.connect(lambda: self._toggle_visibility(self._filter_dock))
|
||||
|
||||
# Добавляем действия в меню "Mode"
|
||||
modes_menu.addAction(self.action_scanning)
|
||||
modes_menu.addAction(self.action_report)
|
||||
modes_menu.addAction(self.action_client)
|
||||
|
||||
# Добавляем действия в меню "Settings"
|
||||
settings_menu.addAction(system_settings_action)
|
||||
settings_menu.addAction(operator_settings_action)
|
||||
settings_menu.addAction(self.action_view_settings)
|
||||
settings_menu.addAction(self.action_filter_settings)
|
||||
|
||||
def _toggle_visibility(self, dock: QDockWidget = None) -> None:
|
||||
"""
|
||||
Переключает видимость указанного док-виджета.
|
||||
|
||||
:param dock: Док-виджет для переключения.
|
||||
"""
|
||||
if dock:
|
||||
is_visible = dock.isVisible()
|
||||
dock.setVisible(not is_visible)
|
||||
|
||||
@staticmethod
|
||||
def _set_dock_features(dock: QDockWidget = None) -> None:
|
||||
"""
|
||||
Устанавливает возможность перемещения, закрытия и открепления для док-виджета.
|
||||
|
||||
:param dock: Док-виджет для настройки.
|
||||
"""
|
||||
if dock:
|
||||
features = QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetClosable | QDockWidget.DockWidgetFloatable
|
||||
dock.setFeatures(features)
|
||||
|
||||
|
||||
class RaportWidget(QWidget):
|
||||
"""
|
||||
Виджет для режима редактирования отчёта.
|
||||
|
||||
Содержит вкладки и кнопки для сохранения состояния и открытия файла.
|
||||
"""
|
||||
|
||||
def __init__(self, tab_widget: CustomTabWidget):
|
||||
super().__init__()
|
||||
self._tab_widget = tab_widget
|
||||
self._build_widget()
|
||||
|
||||
def get_widget(self) -> QWidget:
|
||||
"""
|
||||
Возвращает виджет.
|
||||
"""
|
||||
return self
|
||||
|
||||
def _build_widget(self) -> None:
|
||||
"""
|
||||
Строит макет виджета с вкладками и кнопками управления.
|
||||
"""
|
||||
main_layout = QVBoxLayout()
|
||||
self.btn_save_state = QPushButton("Save state")
|
||||
self.btn_save_state.setFixedWidth(185)
|
||||
self.btn_open_file = QPushButton("Open file")
|
||||
self.btn_open_file.setFixedWidth(185)
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(2)
|
||||
button_layout.addWidget(self.btn_save_state)
|
||||
button_layout.addWidget(self.btn_open_file)
|
||||
button_container = QWidget()
|
||||
button_container.setLayout(button_layout)
|
||||
|
||||
main_layout.addWidget(self._tab_widget)
|
||||
main_layout.addWidget(button_container)
|
||||
self.setLayout(main_layout)
|
||||
|
||||
|
||||
class SeekingWidget(QWidget):
|
||||
"""
|
||||
Виджет для режима поиска (сканирования папок).
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, tab_widget: CustomTabWidget):
|
||||
super().__init__()
|
||||
self._tab_widget = tab_widget
|
||||
self._build_widget()
|
||||
|
||||
def get_widget(self) -> QWidget:
|
||||
"""
|
||||
Возвращает виджет.
|
||||
"""
|
||||
return self
|
||||
|
||||
def _build_widget(self) -> None:
|
||||
"""
|
||||
Создает макет виджета с вкладками.
|
||||
"""
|
||||
main_layout = QVBoxLayout()
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(2)
|
||||
button_container = QWidget()
|
||||
button_container.setLayout(button_layout)
|
||||
|
||||
main_layout.addWidget(self._tab_widget)
|
||||
main_layout.addWidget(button_container)
|
||||
self.setLayout(main_layout)
|
||||
|
||||
|
||||
class ClientAnalyzerWidget(QWidget):
|
||||
"""
|
||||
Виджет для клиентского анализа трассировки.
|
||||
|
||||
Содержит вкладки и кнопки для сохранения состояния, открытия папки и расчёта TCW.
|
||||
"""
|
||||
|
||||
def __init__(self, tab_widget: CustomTabWidget):
|
||||
super().__init__()
|
||||
self._tab_widget = tab_widget
|
||||
self._build_widget()
|
||||
|
||||
def get_widget(self) -> QWidget:
|
||||
"""
|
||||
Возвращает виджет.
|
||||
"""
|
||||
return self
|
||||
|
||||
def _build_widget(self) -> None:
|
||||
"""
|
||||
Строит макет виджета с вкладками и управляющими кнопками.
|
||||
"""
|
||||
main_layout = QVBoxLayout()
|
||||
self.btn_save_state = QPushButton("Save state")
|
||||
self.btn_save_state.setFixedWidth(185)
|
||||
self.btn_open_folder = QPushButton("Open folder")
|
||||
self.btn_open_folder.setFixedWidth(185)
|
||||
self.btn_calc_tcw = QPushButton("Calculate TCW")
|
||||
self.btn_calc_tcw.setFixedWidth(185)
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(2)
|
||||
button_layout.addWidget(self.btn_save_state)
|
||||
button_layout.addWidget(self.btn_open_folder)
|
||||
button_layout.addWidget(self.btn_calc_tcw)
|
||||
button_container = QWidget()
|
||||
button_container.setLayout(button_layout)
|
||||
|
||||
main_layout.addWidget(self._tab_widget)
|
||||
main_layout.addWidget(button_container)
|
||||
self.setLayout(main_layout)
|
||||
36
src/main.py
36
src/main.py
@ -1,44 +1,36 @@
|
||||
import sys
|
||||
|
||||
import pyqtgraph as pg
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from gui.main_gui import MainWindow
|
||||
from controller.file_manager import DirectoryMonitor, FileManager
|
||||
from gui.mainGui import MainWindow
|
||||
from controller.monitor import DirectoryMonitor
|
||||
from controller.mediator import Mediator
|
||||
from controller.converter import DataConverter
|
||||
from gui.plotter import PlotWidget
|
||||
from controller.controller import Controller
|
||||
from controller.passport_former import PassportFormer
|
||||
from performance.roboter import TraceProcessor
|
||||
from controller.passportFormer import PassportFormer
|
||||
|
||||
|
||||
def main():
|
||||
pg.setConfigOptions(useOpenGL=False, antialias=False)
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
monitor = DirectoryMonitor()
|
||||
file_manager = FileManager(monitor=monitor)
|
||||
monitor._file_manager = file_manager
|
||||
data_converter = DataConverter()
|
||||
controller = Controller(file_manager=file_manager)
|
||||
plot_widget_builder = PlotWidget(controller=controller)
|
||||
plot_widget_builder = PlotWidget()
|
||||
controller = Controller()
|
||||
passport_former = PassportFormer()
|
||||
window = MainWindow()
|
||||
trace_processor = TraceProcessor()
|
||||
mediator = Mediator(data_converter, passport_former, plot_widget_builder, controller, file_manager, trace_processor)
|
||||
window = MainWindow(controller)
|
||||
mediator = Mediator(monitor, data_converter, passport_former, plot_widget_builder, controller)
|
||||
window.show()
|
||||
|
||||
window.signal_mode.connect(controller.set_working_mode)
|
||||
window.signal_settings.connect(controller.update_settings)
|
||||
window.signal_replot_all.connect(controller.update_plots)
|
||||
window.signal_open_file.connect(controller.open_file)
|
||||
window.signal_open_dir.connect(controller.open_dir)
|
||||
window.signal_save_file.connect(controller.save_file)
|
||||
window.signal_TCW_for_client.connect(controller.build_TCW_for_client)
|
||||
|
||||
controller.signal_widgets.connect(window.show_plot_tabs)
|
||||
controller.signal_progress_bar.connect(window.status_widget.set_progress)
|
||||
controller.signal_status_text.connect(window.status_widget.set_note)
|
||||
controller.signal_statusBar.connect(window.update_progressBar)
|
||||
controller.signal_statusText.connect(window.update_stateLabel)
|
||||
controller.signal_settings.connect(mediator.update_settings)
|
||||
controller.signal_open_file.connect(monitor.custom_csv_extract_only)
|
||||
controller.signal_raport_mode.connect(monitor.start_raport)
|
||||
controller.signal_seeking_mode.connect(monitor.start_seeking)
|
||||
controller.signal_update_plots.connect(monitor.update_plots)
|
||||
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
BIN
src/performance/__pycache__/roboter.cpython-310.pyc
Normal file
BIN
src/performance/__pycache__/roboter.cpython-310.pyc
Normal file
Binary file not shown.
@ -1,118 +1,24 @@
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from typing import Optional
|
||||
import os
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from roboter import Performance
|
||||
|
||||
#TODO: Реализовать поиск времени простоя
|
||||
class DowntimeAnalyzer:
|
||||
def __init__(self) -> None:
|
||||
...
|
||||
class PerformanceProcessor:
|
||||
|
||||
def generate_downtime_report(self) -> list:
|
||||
...
|
||||
|
||||
def _separate_conditions(self, TWC_raw:pd.DataFrame, robot_raw:pd.DataFrame) -> Tuple[dict, dict]:
|
||||
robot_splitter = RobotConditionSplitter()
|
||||
TWC_splitter = TWC_ConditionSplitter()
|
||||
#comm_splitter = CommConditionSplitter()
|
||||
rob_conditions = robot_splitter.split(robot_raw)
|
||||
TWC_conditions = TWC_splitter.split(TWC_raw)
|
||||
#comm_df = comm_splitter.split(rob_conditions["communication"], TWC_conditions["communication"])
|
||||
return (rob_conditions, TWC_conditions)
|
||||
def calc_performance(self, path:str, TWC_raw:pd.DataFrame):
|
||||
factory = Performance()
|
||||
comm_df, rob_df, TWC_df = factory.job(path, TWC_raw)
|
||||
|
||||
|
||||
class ConditionSplitter:
|
||||
def __init__(self):
|
||||
self._signals = []
|
||||
|
||||
def _find_indexes(self,
|
||||
signal: str,
|
||||
dataframe: pd.DataFrame) -> list[list[float], list[float]]:
|
||||
stage_diff = np.diff(dataframe[signal])
|
||||
start_idx = np.where(stage_diff == 1)
|
||||
finish_idx = np.where(stage_diff == -1)
|
||||
return start_idx[0], finish_idx[0]
|
||||
|
||||
def _find_events(self,
|
||||
dataframe: pd.DataFrame,
|
||||
signals: list[str]) -> Optional[dict[dict[pd.Series]]]:
|
||||
intervals = {}
|
||||
end_time = 0
|
||||
for signal in signals:
|
||||
start_idx, finish_idx = np.array(self._find_indexes(signal, dataframe))
|
||||
start_series = dataframe.loc[start_idx, "time"].reset_index(drop=True)
|
||||
end_series = dataframe.loc[finish_idx, "time"].reset_index(drop=True)
|
||||
end_series.fillna(end_time)
|
||||
intervals[signal] = {"rise": start_series,
|
||||
"fall": end_series}
|
||||
return intervals
|
||||
|
||||
def _form_intervals(self,
|
||||
start: pd.Series,
|
||||
end: pd.Series) -> dict:
|
||||
if len(start) != len(end):
|
||||
for i in range(1, len(end)):
|
||||
if end[i-1] > start[i]:
|
||||
start = start.drop(i).reset_index(drop=True)
|
||||
intervals = {'start':start.tolist, 'end':end.tolist}
|
||||
return intervals
|
||||
|
||||
|
||||
class RobotConditionSplitter(ConditionSplitter):
|
||||
def __init__(self):
|
||||
self._signals = [
|
||||
"$OUT3012",
|
||||
"$IN3003",
|
||||
"$OUT3003",
|
||||
"$OUT3244"
|
||||
]
|
||||
|
||||
def split(self, dataframe: pd.DataFrame) -> dict:
|
||||
events = self._find_events(dataframe, self._signals)
|
||||
communication_sig = {'sent':events["$OUT3244"]["fall"], 'received':events[""][""]}
|
||||
point_interval = self._form_intervals(start=events["$OUT3012"]["rise"], end=events["$OUT3012"]["fall"])
|
||||
movement_interval = self._form_intervals(start=events["$OUT3244"]["rise"], end=events["$OUT3244"]["fall"])
|
||||
conditions = {"communication":communication_sig, "waiting": point_interval, "moving": movement_interval}
|
||||
return conditions
|
||||
|
||||
|
||||
class TWC_ConditionSplitter(ConditionSplitter):
|
||||
def __init__(self):
|
||||
self._signals = [
|
||||
"Closing",
|
||||
"Squeeze",
|
||||
"Welding",
|
||||
"Relief",
|
||||
"Oncoming"
|
||||
]
|
||||
|
||||
def split(self, dataframe: pd.DataFrame) -> dict:
|
||||
events = self._find_events(dataframe, self._signals)
|
||||
communication_sig = {'sent':events[""][""], 'received':events[""][""]} #ситара что-то делает -конец сигнала
|
||||
closing_interval = self._form_intervals(start=events["Closing"]["rise"], end=events["Closing"]["fall"])
|
||||
squeeze_interval = self._form_intervals(start=events["Squeeze"]["rise"], end=events["Squeeze"]["fall"])
|
||||
relief_interval = self._form_intervals(start=events["Relief"]["rise"], end=events["Relief"]["fall"])
|
||||
oncoming_interval = self._form_intervals(start=events["Oncoming"]["rise"], end=events["Oncoming"]["fall"])
|
||||
conditions = {
|
||||
"communication":communication_sig,
|
||||
'closing':closing_interval,
|
||||
'squeeze':squeeze_interval,
|
||||
'relief':relief_interval,
|
||||
'oncoming':oncoming_interval
|
||||
}
|
||||
return conditions
|
||||
|
||||
|
||||
class CommConditionSplitter(ConditionSplitter):
|
||||
"""
|
||||
Определяет промежуток, в который происходит взаимодействие между нодами.
|
||||
"""
|
||||
|
||||
def split(self, node1: dict, node2: dict) -> pd.DataFrame:
|
||||
n1_to_n2 = self._form_intervals(start=node1['sent'], end=node2['received'])
|
||||
n2_to_n1 = self._form_intervals(start=node2['sent'], end=node1['received'])
|
||||
return pd.concat([pd.DataFrame(n1_to_n2), pd.DataFrame(n2_to_n1)])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@ -1,627 +1,203 @@
|
||||
from __future__ import annotations
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
import os
|
||||
from typing import Tuple, Union, Optional, List
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from loguru import logger
|
||||
|
||||
from base.base import (
|
||||
BaseKukaDataParser, BaseKukaTextParser,
|
||||
BaseTraceStageDetector, BaseTextStageDetector,
|
||||
BaseRawTraceProcessor, KukaDataHead,
|
||||
KukaTXT, Settings
|
||||
)
|
||||
|
||||
|
||||
class KukaDataParser(BaseKukaDataParser):
|
||||
"""
|
||||
Класс для парсинга данных Кука.
|
||||
|
||||
Читает заголовочный файл (.dat) и соответствующий файл данных (.r64),
|
||||
объединяет их в pandas.DataFrame.
|
||||
"""
|
||||
class BasePerformanceFactory(ABC):
|
||||
|
||||
def parse(self, head_filepath: str) -> pd.DataFrame:
|
||||
"""
|
||||
Основной метод парсинга. Читает заголовочный файл и файл данных.
|
||||
@abstractmethod
|
||||
def factory_method(self):
|
||||
...
|
||||
|
||||
:param head_filepath: Путь к заголовочному файлу.
|
||||
:return: DataFrame с объединёнными данными.
|
||||
"""
|
||||
header = self._parse_header_file(head_filepath)
|
||||
body_filepath = os.path.join(os.path.dirname(head_filepath), header.filename)
|
||||
dataframe = self._parse_body_file(body_filepath, header)
|
||||
def job(self):
|
||||
...
|
||||
|
||||
def _get_file_data(self, path) -> pd.DataFrame:
|
||||
head, file = self._dat_parser(path)
|
||||
self.dat_name = file[:-4]
|
||||
path_r64 = os.path.dirname(path) + '\\' + file
|
||||
time_axis, dataframe = self._r64_parser(path_r64, head)
|
||||
dataframe = pd.concat([dataframe, time_axis], axis=1)
|
||||
return dataframe
|
||||
|
||||
def _parse_header_file(self, filepath: str) -> KukaDataHead:
|
||||
"""
|
||||
Парсит заголовочный файл (.dat) и извлекает информацию о каналах.
|
||||
|
||||
:param filepath: Путь к заголовочному файлу.
|
||||
:return: Объект KukaDataHead с информацией о данных.
|
||||
"""
|
||||
with open(filepath, 'r', encoding='cp1252') as file:
|
||||
data_head = KukaDataHead(0, "", {})
|
||||
is_inside_channel = False
|
||||
self._current_channel = None # Текущее название канала
|
||||
def _dat_parser(self, path: str) -> list[dict, str]:
|
||||
with open(path, 'r') as file:
|
||||
head = {'channels': 0}
|
||||
inside_channel = False
|
||||
channels = 0
|
||||
for line in file:
|
||||
line = line.strip()
|
||||
if line in ('#BEGINCHANNELHEADER', "#BEGINGLOBALHEADER"):
|
||||
is_inside_channel = True
|
||||
elif line in ('#ENDCHANNELHEADER', "#ENDGLOBALHEADER"):
|
||||
is_inside_channel = False
|
||||
else:
|
||||
if is_inside_channel:
|
||||
self._parse_header_line(line, data_head)
|
||||
return data_head
|
||||
if line == '#BEGINCHANNELHEADER' or line == "#BEGINGLOBALHEADER":
|
||||
inside_channel = True
|
||||
|
||||
def _parse_header_line(self, line: str, data_head: KukaDataHead) -> None:
|
||||
"""
|
||||
Обрабатывает отдельную строку заголовочного файла и обновляет объект data_head.
|
||||
|
||||
:param line: Строка из файла.
|
||||
:param data_head: Объект KukaDataHead для обновления.
|
||||
"""
|
||||
tag, value = line.split(',')
|
||||
match tag:
|
||||
elif line == '#ENDCHANNELHEADER' or line == "#ENDGLOBALHEADER":
|
||||
inside_channel = False
|
||||
pass
|
||||
# Формирование словаря
|
||||
elif inside_channel:
|
||||
_, data = line.split(',')
|
||||
match _:
|
||||
case '102':
|
||||
data_head.rob_ID = value
|
||||
head['rob_id'] = data
|
||||
case '200':
|
||||
self._current_channel = str(value)
|
||||
data_head.channels[self._current_channel] = {}
|
||||
ch_name = data
|
||||
if ch_name != 'Zeit': channels +=1
|
||||
head[ch_name] = {}
|
||||
case '202':
|
||||
data_head.channels[self._current_channel]['unit'] = str(value)
|
||||
head[ch_name]['unit'] = data
|
||||
case '211':
|
||||
data_head.filename = str(value)
|
||||
file = data
|
||||
case '220':
|
||||
data_head.channels[self._current_channel]['len'] = int(value)
|
||||
head[ch_name]['len'] = int(data)
|
||||
case '221':
|
||||
data_head.channels[self._current_channel]['num'] = int(value)
|
||||
head[ch_name]['num'] = int(data)
|
||||
case '241':
|
||||
# Переименовано в "multiplier" для повышения читаемости
|
||||
data_head.channels[self._current_channel]['multiplier'] = float(value)
|
||||
head[ch_name]['multiplyer'] = float(data)
|
||||
head['channels'] = int(channels)
|
||||
return head, file
|
||||
|
||||
def _parse_body_file(self, filepath: str, data_head: KukaDataHead) -> pd.DataFrame:
|
||||
"""
|
||||
Парсит файл данных (.r64) и объединяет его с временной осью.
|
||||
|
||||
:param filepath: Путь к файлу данных.
|
||||
:param data_head: Заголовочная информация.
|
||||
:return: DataFrame с данными.
|
||||
"""
|
||||
time_axis = self._build_time_axis(data_head)
|
||||
multipliers, channel_names = self._extract_channels_data(data_head)
|
||||
raw_data = self._read_r64_file(filepath)
|
||||
|
||||
if raw_data.size % len(channel_names) != 0:
|
||||
raise ValueError(f"Количество записей в {filepath} не кратно количеству найденных каналов ({len(channel_names)})")
|
||||
try:
|
||||
data_reshaped = raw_data.reshape(-1, len(channel_names))
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Ошибка при изменении формы raw_data: {e}")
|
||||
|
||||
# Применяем множители к данным каналов
|
||||
df_data = pd.DataFrame(data_reshaped * multipliers, columns=channel_names)
|
||||
dataframe = pd.concat([time_axis, df_data], axis=1)
|
||||
return dataframe
|
||||
|
||||
@staticmethod
|
||||
def _extract_channels_data(data_head: KukaDataHead) -> Tuple[np.ndarray, List[str]]:
|
||||
"""
|
||||
Извлекает данные по каналам, сортируя их по номеру.
|
||||
|
||||
:param data_head: Заголовочная информация.
|
||||
:return: Кортеж из массива множителей и списка имён каналов.
|
||||
"""
|
||||
sorted_channels = sorted(
|
||||
(item for item in data_head.channels.items() if item[0] != "Zeit"),
|
||||
key=lambda item: item[1]['num']
|
||||
)
|
||||
multipliers = np.array([info['multiplier'] for _, info in sorted_channels])
|
||||
channel_names = [key for key, _ in sorted_channels]
|
||||
return multipliers, channel_names
|
||||
|
||||
@staticmethod
|
||||
def _build_time_axis(data_head: KukaDataHead) -> pd.Series:
|
||||
"""
|
||||
Строит временную ось на основе информации из канала 'Zeit'.
|
||||
|
||||
:param data_head: Заголовочная информация.
|
||||
:return: Серия pandas с временными метками.
|
||||
"""
|
||||
num_timestamps = data_head.channels['Zeit']['len'] - 1
|
||||
time_step = data_head.channels['Zeit']['multiplier']
|
||||
time_axis = pd.Series(np.arange(0, num_timestamps * time_step, time_step))
|
||||
def _r64_parser(self, path: str, head: dict) -> Optional[list[pd.Series, pd.DataFrame]]:
|
||||
ch = head['channels']
|
||||
keys = list(head.keys())[-ch:]
|
||||
len_timestamps = head['Zeit']['len']
|
||||
t_step = head['Zeit']['multiplyer']
|
||||
time_axis = pd.Series(np.arange(0, len_timestamps*t_step, t_step))
|
||||
time_axis.name = 'time'
|
||||
return time_axis
|
||||
|
||||
@staticmethod
|
||||
def _read_r64_file(filepath: str) -> np.ndarray:
|
||||
"""
|
||||
Считывает бинарный файл (.r64) и возвращает массив чисел.
|
||||
|
||||
:param filepath: Путь к файлу.
|
||||
:return: Массив numpy с типом float.
|
||||
"""
|
||||
with open(filepath, 'rb') as file:
|
||||
dataframe = pd.DataFrame({})
|
||||
with open(path, 'rb') as file:
|
||||
data = file.read()
|
||||
numbers = np.frombuffer(data, dtype='<d')
|
||||
return numbers
|
||||
floats = np.frombuffer(data, dtype='<d') # Little-endian double
|
||||
for key in keys:
|
||||
step = head[key]['num']-1
|
||||
result = pd.Series(np.array(floats[step::ch])* head[key]['multiplyer'])
|
||||
result.name = key
|
||||
dataframe = pd.concat([dataframe, result], axis=1)
|
||||
return time_axis, dataframe
|
||||
|
||||
|
||||
class KukaTextParser(BaseKukaTextParser):
|
||||
"""
|
||||
Класс для парсинга текстовых данных Кука.
|
||||
|
||||
Извлекает сообщения из файла.
|
||||
"""
|
||||
class BaseProduct(ABC):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._in_message = False
|
||||
self._data_packs: List[KukaTXT] = []
|
||||
self._signals = []
|
||||
|
||||
def parse(self, filepath: str) -> List[KukaTXT]:
|
||||
"""
|
||||
Парсит текстовый файл и возвращает список объектов KukaTXT.
|
||||
def _find_indexes(self,
|
||||
signal: str,
|
||||
dataframe: pd.DataFrame) -> list[list[float], list[float]]:
|
||||
stage_diff = np.diff(dataframe[signal])
|
||||
start_idx = np.where(stage_diff == 1)
|
||||
finish_idx = np.where(stage_diff == -1)
|
||||
return start_idx[0], finish_idx[0]
|
||||
|
||||
:param filepath: Путь к текстовому файлу.
|
||||
:return: Список объектов KukaTXT.
|
||||
"""
|
||||
self._data_packs = []
|
||||
with open(filepath, 'r') as file:
|
||||
current_pack = KukaTXT()
|
||||
for line in file:
|
||||
line = line.strip()
|
||||
current_pack = self._process_message_flow(line, current_pack)
|
||||
return self._data_packs
|
||||
def _find_events(self,
|
||||
dataframe: pd.DataFrame,
|
||||
signals: list[str]) -> Optional[dict[dict[pd.Series]]]:
|
||||
intervals = {}
|
||||
end_time = 0
|
||||
for signal in signals:
|
||||
start_idx, finish_idx = np.array(self._find_indexes(signal, dataframe))
|
||||
start_series = dataframe.loc[start_idx, "time"].reset_index(drop=True)
|
||||
end_series = dataframe.loc[finish_idx, "time"].reset_index(drop=True)
|
||||
end_series.fillna(end_time)
|
||||
intervals[signal] = {"rise": start_series,
|
||||
"fall": end_series}
|
||||
return intervals
|
||||
|
||||
def _process_message_flow(self, line: str, current_pack: KukaTXT) -> KukaTXT:
|
||||
"""
|
||||
Обрабатывает поток сообщений, определяя начало и конец блока.
|
||||
def _form_intervals(self,
|
||||
start: pd.Series,
|
||||
end: pd.Series) -> dict:
|
||||
if len(start) != len(end):
|
||||
for i in range(1, len(end)):
|
||||
if end[i-1] > start[i]:
|
||||
start = start.drop(i).reset_index(drop=True)
|
||||
intervals = {'start':start.tolist, 'end':end.tolist}
|
||||
return intervals
|
||||
|
||||
:param line: Строка из файла.
|
||||
:param current_pack: Текущий объект KukaTXT.
|
||||
:return: Обновлённый объект KukaTXT.
|
||||
"""
|
||||
if line == "#BEGINMOTIONINFO":
|
||||
self._in_message = True
|
||||
current_pack = KukaTXT()
|
||||
elif line == "#ENDMOTIONINFO":
|
||||
self._in_message = False
|
||||
self._data_packs.append(current_pack)
|
||||
current_pack = KukaTXT()
|
||||
elif self._in_message:
|
||||
current_pack = self._process_line(line, current_pack)
|
||||
return current_pack
|
||||
|
||||
def _process_line(self, line: str, current_pack: KukaTXT) -> KukaTXT:
|
||||
"""
|
||||
Обрабатывает отдельную строку внутри блока сообщения.
|
||||
|
||||
:param line: Строка с данными.
|
||||
:param current_pack: Текущий объект KukaTXT.
|
||||
:return: Обновлённый объект KukaTXT.
|
||||
"""
|
||||
tag, value = line.split(":")
|
||||
match tag:
|
||||
case "TIME":
|
||||
current_pack.time = float(value)
|
||||
case "ENDTIME":
|
||||
current_pack.endtime = float(value)
|
||||
case "MODULE":
|
||||
pass
|
||||
case "FUNCTION/PROCEDURE":
|
||||
current_pack.func = value
|
||||
case "TYPE":
|
||||
current_pack.type_ = value
|
||||
case "SIGNAL":
|
||||
current_pack.signal = value
|
||||
case "LINE":
|
||||
pass
|
||||
case "POINT NAME":
|
||||
pass
|
||||
case "POINT COORDINATES":
|
||||
pass
|
||||
case "BLENDING":
|
||||
pass
|
||||
case "BLENDING PARAMETER":
|
||||
pass
|
||||
case "VELOCITIES":
|
||||
pass
|
||||
case "ACCELERATIONS":
|
||||
pass
|
||||
case "BASE":
|
||||
pass
|
||||
case "TOOL":
|
||||
pass
|
||||
case "IPO MODE":
|
||||
pass
|
||||
case "MOTION MODE":
|
||||
pass
|
||||
case "LOAD":
|
||||
pass
|
||||
case "LOAD A3":
|
||||
pass
|
||||
return current_pack
|
||||
@abstractmethod
|
||||
def operation(self):
|
||||
...
|
||||
|
||||
|
||||
class TraceStageDetector(BaseTraceStageDetector):
|
||||
"""
|
||||
Класс для детекции этапов (стадий) на основе данных трассировки.
|
||||
class Performance(BasePerformanceFactory):
|
||||
|
||||
Определяет переходы между различными стадиями процесса по пороговым значениям.
|
||||
"""
|
||||
def __init__(self, parent: TraceProcessor = None):
|
||||
super().__init__(parent)
|
||||
def robot_method(self) -> BaseProduct:
|
||||
return RobotData()
|
||||
|
||||
@staticmethod
|
||||
def is_closing(robot_velocity: float, actual_velocity: float, actual_force: float, thresholds: dict) -> bool:
|
||||
"""
|
||||
Определяет, находится ли робот в стадии закрытия.
|
||||
def TWC_method(self) -> BaseProduct:
|
||||
return TWC_Data()
|
||||
|
||||
:param robot_velocity: Скорость робота.
|
||||
:param actual_velocity: Фактическая скорость.
|
||||
:param actual_force: Фактическое значение силы.
|
||||
:param thresholds: Словарь пороговых значений.
|
||||
:return: True, если условие стадии закрытия выполнено.
|
||||
"""
|
||||
return actual_velocity > thresholds['act_vel_min'] and abs(robot_velocity) < thresholds['rob_vel_thresh'] and actual_force < thresholds['act_force_close']
|
||||
def comm_method(self) -> BaseProduct:
|
||||
return CommData()
|
||||
|
||||
@staticmethod
|
||||
def is_squeeze(robot_velocity: float, actual_velocity: float, actual_force: float, force_rate: float, thresholds: dict) -> bool:
|
||||
"""
|
||||
Определяет, находится ли робот в стадии сжатия.
|
||||
|
||||
:param robot_velocity: Скорость робота.
|
||||
:param actual_velocity: Фактическая скорость.
|
||||
:param actual_force: Фактическое значение силы.
|
||||
:param force_rate: Темп изменения силы.
|
||||
:param thresholds: Словарь пороговых значений.
|
||||
:return: True, если условие стадии сжатия выполнено.
|
||||
"""
|
||||
return abs(robot_velocity) < thresholds['rob_vel_thresh'] and actual_velocity < thresholds['act_vel_close'] and force_rate > thresholds['force_increase']
|
||||
|
||||
@staticmethod
|
||||
def is_welding(robot_velocity: float, actual_velocity: float, actual_force: float, thresholds: dict) -> bool:
|
||||
"""
|
||||
Определяет, находится ли робот в стадии сварки.
|
||||
|
||||
:param robot_velocity: Скорость робота.
|
||||
:param actual_velocity: Фактическая скорость.
|
||||
:param actual_force: Фактическое значение силы.
|
||||
:param thresholds: Словарь пороговых значений.
|
||||
:return: True, если условие стадии сварки выполнено.
|
||||
"""
|
||||
return abs(robot_velocity) < thresholds['rob_vel_thresh'] and abs(actual_velocity) < thresholds['act_vel_thresh'] and actual_force > thresholds['act_force_weld']
|
||||
|
||||
@staticmethod
|
||||
def is_relief(actual_velocity: float, actual_position_diff: float, force_rate: float, thresholds: dict) -> bool:
|
||||
"""
|
||||
Определяет, находится ли робот в стадии снятия усилия (relief).
|
||||
|
||||
:param actual_velocity: Фактическая скорость.
|
||||
:param actual_position_diff: Разница в позициях.
|
||||
:param force_rate: Темп изменения силы.
|
||||
:param thresholds: Словарь пороговых значений.
|
||||
:return: True, если условие стадии снятия усилия выполнено.
|
||||
"""
|
||||
return force_rate < -thresholds['force_decrease'] and abs(actual_position_diff) < thresholds['act_pos_decrease'] and actual_velocity < -thresholds['act_vel_negative']
|
||||
|
||||
def detect_stages(self, df: pd.DataFrame) -> List[Tuple[str, float, float]]:
|
||||
"""
|
||||
Детектирует стадии процесса по данным трассировки.
|
||||
|
||||
:param df: DataFrame с данными трассировки.
|
||||
:return: Список кортежей (название стадии, время начала, время окончания).
|
||||
"""
|
||||
timestamps = df['time'].to_list()
|
||||
n = len(df)
|
||||
# Извлекаем пороговые значения из настроек
|
||||
thresholds = {key: item[0] for key, item in self._parent._settings.filter.items()}
|
||||
|
||||
# Вычисляем производную силы и разницу позиций
|
||||
actual_force = df["DriveMotorTorq_Act7"].values
|
||||
force_diff = np.diff(actual_force, prepend=actual_force[0])
|
||||
actual_position = df["DriveMotorPos_Act7"].values
|
||||
position_diff = np.diff(actual_position, prepend=actual_position[0])
|
||||
|
||||
stages = []
|
||||
current_state = "Oncomming"
|
||||
state_start = timestamps[0]
|
||||
|
||||
# Проходим по всем записям DataFrame
|
||||
for i in range(n):
|
||||
robot_velocity = df.loc[i, "CartVel_Act"]
|
||||
actual_velocity = df.loc[i, "DriveMotorVel_Act7"]
|
||||
force_value = df.loc[i, "DriveMotorTorq_Act7"]
|
||||
current_force_rate = force_diff[i]
|
||||
current_position_diff = position_diff[i]
|
||||
|
||||
if current_state == "Oncomming":
|
||||
if self.is_closing(robot_velocity, actual_velocity, force_value, thresholds):
|
||||
state_end = timestamps[i]
|
||||
stages.append(("Oncomming", state_start, state_end))
|
||||
current_state = "Closing"
|
||||
state_start = timestamps[i]
|
||||
|
||||
elif current_state == "Closing":
|
||||
if self.is_squeeze(robot_velocity, actual_velocity, force_value, current_force_rate, thresholds):
|
||||
state_end = timestamps[i]
|
||||
stages.append(("Closing", state_start, state_end))
|
||||
current_state = "Squeeze"
|
||||
state_start = timestamps[i]
|
||||
|
||||
elif current_state == "Squeeze":
|
||||
if self.is_welding(robot_velocity, actual_velocity, force_value, thresholds):
|
||||
state_end = timestamps[i]
|
||||
stages.append(("Squeeze", state_start, state_end))
|
||||
current_state = "Welding"
|
||||
state_start = timestamps[i]
|
||||
|
||||
elif current_state == "Welding":
|
||||
if self.is_relief(actual_velocity, current_position_diff, current_force_rate, thresholds):
|
||||
state_end = timestamps[i]
|
||||
stages.append(("Welding", state_start, state_end))
|
||||
current_state = "Relief"
|
||||
state_start = timestamps[i]
|
||||
|
||||
elif current_state == "Relief":
|
||||
# Если признаки снятия усилия не соблюдаются, завершаем цикл и переходим в Oncomming
|
||||
if not self.is_relief(actual_velocity, current_position_diff, current_force_rate, thresholds):
|
||||
state_end = timestamps[i]
|
||||
stages.append(("Relief", state_start, state_end))
|
||||
current_state = "Oncomming"
|
||||
state_start = timestamps[i]
|
||||
|
||||
# Фиксируем последний сегмент
|
||||
stages.append((current_state, state_start, timestamps[-1]))
|
||||
return stages
|
||||
def job(self, path:str, TWC_raw:pd.DataFrame) -> list[pd.DataFrame, list[pd.DataFrame], list[pd.DataFrame]]:
|
||||
robot = self.robot_method()
|
||||
TWC = self.TWC_method()
|
||||
comm=self.comm_method()
|
||||
dataframe = self._get_file_data(path)
|
||||
rob_comm, rob_df = robot.operation(dataframe)
|
||||
TWC_comm, TWC_df = TWC.operation(TWC_raw)
|
||||
comm_df = comm.operation(rob_comm, TWC_comm)
|
||||
return comm_df, rob_df, TWC_df
|
||||
|
||||
|
||||
class TextStageDetector(BaseTextStageDetector):
|
||||
"""
|
||||
Класс для детекции сварочных стадий на основе текстовых данных.
|
||||
"""
|
||||
def __init__(self, parent: TraceProcessor = None):
|
||||
super().__init__(parent)
|
||||
|
||||
@staticmethod
|
||||
def detect_welding(data: List[KukaTXT]) -> List[dict]:
|
||||
"""
|
||||
Детектирует этап сварки по текстовым сообщениям.
|
||||
|
||||
:param data: Список объектов KukaTXT.
|
||||
:return: Список словарей с информацией о сварке (стадия, время начала и окончания).
|
||||
"""
|
||||
stages = []
|
||||
for i in range(len(data) - 1): # Предотвращаем выход за пределы списка
|
||||
if data[i].func == " SPOT" and data[i].signal == " END":
|
||||
stages.append({
|
||||
"stage": "welding",
|
||||
"start_time": data[i].time,
|
||||
"end_time": data[i + 1].time
|
||||
})
|
||||
return stages
|
||||
|
||||
|
||||
class TraceProcessor(BaseRawTraceProcessor):
|
||||
"""
|
||||
Основной класс для обработки трассировок.
|
||||
|
||||
Объединяет данные, детектирует стадии и инициирует рендеринг через медиатор.
|
||||
"""
|
||||
class RobotData(BaseProduct):
|
||||
def __init__(self):
|
||||
self._settings = Settings()
|
||||
data_parser = KukaDataParser()
|
||||
text_parser = KukaTextParser()
|
||||
trace_detector = TraceStageDetector(self)
|
||||
text_detector = TextStageDetector(self)
|
||||
self._signals = [
|
||||
"$OUT3012",
|
||||
"$IN3003",
|
||||
"$OUT3003",
|
||||
"$OUT3244"
|
||||
]
|
||||
|
||||
super().__init__(data_parser, text_parser, trace_detector, text_detector)
|
||||
def operation(self, dataframe: pd.DataFrame) -> list[dict, pd.DataFrame]:
|
||||
events = self._find_events(dataframe, self._signals)
|
||||
communication_sig = {'sent':events["$OUT3244"]["fall"], 'received':events[""][""]}
|
||||
point_interval = self._form_intervals(start=events["$OUT3012"]["rise"], end=events["$OUT3012"]["fall"])
|
||||
movement_interval = self._form_intervals(start=events["$OUT3244"]["rise"], end=events["$OUT3244"]["fall"])
|
||||
return communication_sig, pd.DataFrame({'in_point':point_interval,
|
||||
'in_move':movement_interval})
|
||||
|
||||
def prerender(self, data: List[str]) -> None:
|
||||
|
||||
class TWC_Data(BaseProduct):
|
||||
def __init__(self):
|
||||
self._signals = [
|
||||
"Closing",
|
||||
"Squeeze",
|
||||
"Welding",
|
||||
"Relief",
|
||||
"Oncoming"
|
||||
]
|
||||
|
||||
def operation(self, dataframe: pd.DataFrame) -> list[dict, pd.DataFrame]:
|
||||
events = self._find_events(dataframe, self._signals)
|
||||
communication_sig = {'sent':events[""][""], 'received':events[""][""]} #ситара что-то делает -конец сигнала
|
||||
closing_interval = self._form_intervals(start=events["Closing"]["rise"], end=events["Closing"]["fall"])
|
||||
squeeze_interval = self._form_intervals(start=events["Squeeze"]["rise"], end=events["Squeeze"]["fall"])
|
||||
relief_interval = self._form_intervals(start=events["Relief"]["rise"], end=events["Relief"]["fall"])
|
||||
oncoming_interval = self._form_intervals(start=events["Oncoming"]["rise"], end=events["Oncoming"]["fall"])
|
||||
return communication_sig, pd.DataFrame({'in_closing':closing_interval,
|
||||
'in_squeeze':squeeze_interval,
|
||||
'in_relief':relief_interval,
|
||||
'in_oncoming':oncoming_interval})
|
||||
|
||||
class CommData(BaseProduct):
|
||||
"""
|
||||
Препроцессинг данных для отрисовки трейсов и обнаруженных событий.
|
||||
|
||||
:param data: Список путей к файлам данных.
|
||||
Определяет промежуток, в который происходит взаимодействие между нодами.
|
||||
Подразумевается следующая структура: node: tuple(list, list)
|
||||
node[0] - время отправки пакетов. node[1] - время приема пакетов.
|
||||
"""
|
||||
rendered_data = self._render_data(data)
|
||||
self._mediator.prerender_TCW(self, rendered_data)
|
||||
|
||||
def final_render(self) -> None:
|
||||
"""
|
||||
Финальный рендеринг данных для отрисовки
|
||||
трейсов клиента и расчета трейсов ТСК.
|
||||
"""
|
||||
events = self._detect_stages(self._trace_df, self._text_data)
|
||||
renamed_df = self._rename_df_columns(self._trace_df)
|
||||
part_position, max_open = self._detect_coords(renamed_df, events)
|
||||
self._mediator.render_TCW(self, [renamed_df, events, [part_position, max_open]])
|
||||
|
||||
def _detect_coords(self, dataframe: pd.DataFrame, events: dict) -> Tuple[List[float], List[float]]:
|
||||
"""
|
||||
Детектирует координаты на основе событий и данных.
|
||||
|
||||
:param dataframe: DataFrame с данными.
|
||||
:param events: Словарь с событиями.
|
||||
:return: Кортеж из списков: координаты детали и максимальное открытие электрода.
|
||||
"""
|
||||
weld_timings = events["Welding"]
|
||||
oncommings = events["Oncomming"]
|
||||
open_positions = []
|
||||
part_positions = []
|
||||
for i in range(len(weld_timings[0])):
|
||||
weld_start = weld_timings[0][i]
|
||||
weld_end = weld_timings[1][i]
|
||||
onc_start = oncommings[0][i]
|
||||
onc_end = oncommings[1][i]
|
||||
pos_part = dataframe[(dataframe["time"] > weld_start) & (dataframe["time"] < weld_end)]["Electrode Position, mm ME"].mean()
|
||||
part_positions.append(float(pos_part) / 1000)
|
||||
pos_open = dataframe[(dataframe["time"] > onc_start) & (dataframe["time"] < onc_end)]["Electrode Position, mm ME"].abs().max()
|
||||
open_positions.append(float(pos_open) / 1000)
|
||||
return (part_positions, open_positions)
|
||||
|
||||
def _render_data(self, data: List[str]) -> List:
|
||||
"""
|
||||
Обрабатывает входные файлы данных и возвращает предварительно обработанные данные.
|
||||
|
||||
:param data: Список путей к файлам (первый для трассировки, второй для текстовых данных).
|
||||
:return: Список, содержащий DataFrame и события.
|
||||
"""
|
||||
if data and len(data) == 2:
|
||||
trace_filepath = data[0]
|
||||
text_filepath = data[1]
|
||||
elif data:
|
||||
trace_filepath = data[0]
|
||||
text_filepath = None
|
||||
else:
|
||||
trace_filepath = None
|
||||
text_filepath = None
|
||||
|
||||
self._trace_df = self._unpack_trace(trace_filepath)
|
||||
self._text_data = self._unpack_text(text_filepath)
|
||||
events = self._detect_stages(self._trace_df, self._text_data)
|
||||
renamed_df = self._rename_df_columns(self._trace_df)
|
||||
return [renamed_df, events]
|
||||
|
||||
def update_settings(self, settings: Settings) -> None:
|
||||
"""
|
||||
Обновляет настройки обработки.
|
||||
|
||||
:param settings: Объект настроек.
|
||||
"""
|
||||
self._settings = settings
|
||||
|
||||
def _unpack_trace(self, trace_filepath: str = None) -> Optional[pd.DataFrame]:
|
||||
"""
|
||||
Распаковывает трассировочные данные из файла.
|
||||
|
||||
:param trace_filepath: Путь к файлу трассировки.
|
||||
:return: DataFrame с данными или None.
|
||||
"""
|
||||
if trace_filepath:
|
||||
return self._dataparser.parse(trace_filepath)
|
||||
return None
|
||||
|
||||
def _unpack_text(self, text_filepath: str = None) -> Optional[List[KukaTXT]]:
|
||||
"""
|
||||
Распаковывает текстовые данные из файла.
|
||||
|
||||
:param text_filepath: Путь к текстовому файлу.
|
||||
:return: Список объектов KukaTXT или None.
|
||||
"""
|
||||
if text_filepath:
|
||||
return self._textparser.parse(text_filepath)
|
||||
return None
|
||||
|
||||
def _detect_stages(self, trace_df: pd.DataFrame, text_data: List) -> Optional[dict]:
|
||||
"""
|
||||
Детектирует события на основе трассировочных и текстовых данных.
|
||||
|
||||
:param trace_df: DataFrame с трассировочными данными.
|
||||
:param text_data: Список текстовых данных.
|
||||
:return: Словарь с событиями или None.
|
||||
"""
|
||||
if trace_df is not None and text_data is not None:
|
||||
trace_stages = self._data_detector.detect_stages(trace_df)
|
||||
events = self._form_events(trace_stages)
|
||||
return events
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _form_events(trace_stages: list) -> dict:
|
||||
"""
|
||||
Формирует словарь событий на основе списка этапов трассировки.
|
||||
|
||||
:param trace_stages: Список этапов (кортежи: название, время начала, время окончания).
|
||||
:return: Словарь с событиями.
|
||||
"""
|
||||
events = {
|
||||
"Closing": [[], []],
|
||||
"Squeeze": [[], []],
|
||||
"Welding": [[], []],
|
||||
"Relief": [[], []],
|
||||
"Oncomming": [[], []]
|
||||
}
|
||||
for stage in trace_stages:
|
||||
name, start_time, end_time = stage
|
||||
events[name][0].append(start_time)
|
||||
events[name][1].append(end_time)
|
||||
return events
|
||||
|
||||
@staticmethod
|
||||
def _rename_df_columns(dataframe: pd.DataFrame) -> Optional[pd.DataFrame]:
|
||||
"""
|
||||
Переименовывает столбцы DataFrame на основе корректной карты соответствия.
|
||||
|
||||
:param dataframe: Исходный DataFrame.
|
||||
:return: DataFrame с переименованными столбцами или None в случае ошибки.
|
||||
"""
|
||||
correct_mapping = {
|
||||
"time": ["Time", "Timestamp"],
|
||||
"Tool Coordinate, mm X": ["X_Act"],
|
||||
"Tool Coordinate, mm Y": ["Y_Act"],
|
||||
"Tool Coordinate, mm Z": ["Z_Act"],
|
||||
"Electrode Force, N ME": ["DriveMotorTorq_Act7"],
|
||||
"Electrode Position, mm ME": ["AxisPos_Act7"],
|
||||
"Electrode Speed, mm ME": ["AxisVel_Act7"],
|
||||
"Rotor Position, deg FE": ["DriveMotorPos_Act7"],
|
||||
"Rotor Speed, deg/s ME": ["DriveMotorVel_Act7"],
|
||||
"Rotor Current, A ME": ["DriveMotorCurr_Act7"],
|
||||
"Cartesian Tool Speed, mm/s ME": ["CartVel_Act"],
|
||||
}
|
||||
try:
|
||||
df_copy = dataframe.copy(deep=True)
|
||||
working_mapping = {key: [item.lower() for item in items] for key, items in correct_mapping.items()}
|
||||
|
||||
new_columns = {}
|
||||
for col in df_copy.columns:
|
||||
col_lower = col.lower()
|
||||
for key, values in working_mapping.items():
|
||||
if col_lower in values:
|
||||
new_columns[col] = key
|
||||
break
|
||||
else:
|
||||
new_columns[col] = col
|
||||
df_copy.rename(columns=new_columns, inplace=True)
|
||||
df_copy = df_copy.loc[:, ~df_copy.columns.duplicated()]
|
||||
return df_copy
|
||||
except AttributeError as e:
|
||||
logger.error(f"_rename_df_columns - AttributeError: Проверьте, что переданный объект является DataFrame. {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"_rename_df_columns - Непредвиденная ошибка: {e}")
|
||||
return None
|
||||
def operation(self, node1: dict, node2: dict) -> pd.DataFrame:
|
||||
n1_to_n2 = self._form_intervals(start=node1['sent'], end=node2['received'])
|
||||
n2_to_n1 = self._form_intervals(start=node2['sent'], end=node1['received'])
|
||||
return pd.concat([pd.DataFrame(n1_to_n2), pd.DataFrame(n2_to_n1)])
|
||||
|
||||
|
||||
|
||||
|
||||
"""
|
||||
Примеры комментариев для этапов процесса:
|
||||
|
||||
Перемещение:
|
||||
# FUNCTION/PROCEDURE: SW_RSP030TL01_SN - какое-то перемещение
|
||||
#
|
||||
# ИЛИ
|
||||
#
|
||||
# FUNCTION/PROCEDURE: SGL_MoveToPos - перемещение между точками
|
||||
# SIGNAL: BLENDING
|
||||
|
||||
Смыкание и набор усилия:
|
||||
# FUNCTION/PROCEDURE: SGM_MOVE_TO_FORCE - перемещение электрода с движением робота
|
||||
# ...
|
||||
# FUNCTION/PROCEDURE: SGM_MOVE_TO_FORCE - перемещение 0.5 мм роботом с движением электрода
|
||||
# ...
|
||||
# FUNCTION/PROCEDURE: SGM_MOVE_TO_FORCE - остановка в позиции (может быть набор усилия?)
|
||||
|
||||
Сварка:
|
||||
# FUNCTION/PROCEDURE: SPOT - Начало сварочного процесса
|
||||
# SIGNAL: START
|
||||
#
|
||||
# FUNCTION/PROCEDURE: SPOT - Конец сварочного процесса
|
||||
# SIGNAL: END
|
||||
|
||||
Снятие усилия и разъезд:
|
||||
# FUNCTION/PROCEDURE: SGL_MoveToPos - Выход из контакта с точкой движением робота и электрода
|
||||
# SIGNAL: START
|
||||
"""
|
||||
|
||||
@ -1,45 +1,22 @@
|
||||
# TODO: Тесты должны лежать в директории tests.
|
||||
|
||||
# TODO: Правило хорошего тона при оформлении импортов: сначала встроенные библиотеки,
|
||||
# через одну пустую строку - дополнительно установленные, и еще через одну пустую строку - собственные импорты.
|
||||
from src.OptAlgorithm.OptAlgorithm import OptAlgorithm
|
||||
from src.utils import read_json
|
||||
|
||||
from matplotlib import pyplot as plt, use
|
||||
|
||||
from numpy import cos, sin, sqrt, cbrt, arcsin, linspace, array
|
||||
# TODO: Отступ перед блоком if __name__ == "__main__" - 2 пустых строки.
|
||||
|
||||
if __name__ == "__main__":
|
||||
# TODO: Что означают переменные ниже? Имена переменных должны быть осмысленными, чтобы их смысл был понятен не
|
||||
# не только автору кода.
|
||||
|
||||
tq = 1
|
||||
ts = linspace(0, tq, 200000)
|
||||
|
||||
operator_params = read_json("../params/operator_params.json")
|
||||
system_params = read_json("../params/system_params.json")
|
||||
|
||||
# TODO: 2 блока кода ниже (с циклами) нарушают принцип DRY (Don't Repeat Yourself):
|
||||
# повторяющийся блок кода следует вынести в отдельную функцию.
|
||||
# Например, в такую:
|
||||
# def foo(params_dict: dict) -> dict:
|
||||
# result_dict = {}
|
||||
# i = 1
|
||||
# for key, value in params_dict.items():
|
||||
# if isinstance(value, list):
|
||||
# result_dict[key] = value[i] if len(value) > i else value[0]
|
||||
# else:
|
||||
# result_dict[key] = value
|
||||
# return result_dict
|
||||
# И потом вызвать ее как-то так:
|
||||
# non_array_operator_params, non_array_system_params = foo(operator_params), foo(system_params)
|
||||
operator_params = read_json("params/operator_params.json")
|
||||
system_params = read_json("params/system_params.json")
|
||||
|
||||
non_array_operator_params = {}
|
||||
i = 1
|
||||
for key, value in operator_params.items():
|
||||
# TODO: Не совсем понятен смысл такой проверки. Если хотим проверить принадлежность value к list,
|
||||
# то лучше использовать для этого isinstance.
|
||||
if hasattr(value, "__len__"):
|
||||
# TODO: Можно заменить однострочным выражением: result_dict[key] = value[i] if len(value) > i else value[0]
|
||||
if len(value) > i:
|
||||
non_array_operator_params[key] = value[i]
|
||||
else:
|
||||
|
||||
BIN
src/uml/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
src/uml/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/uml/__pycache__/creator.cpython-310.pyc
Normal file
BIN
src/uml/__pycache__/creator.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/uml/__pycache__/request_generator.cpython-310.pyc
Normal file
BIN
src/uml/__pycache__/request_generator.cpython-310.pyc
Normal file
Binary file not shown.
@ -1,10 +1,7 @@
|
||||
from uml.request_generator import Request
|
||||
from src.uml.request_generator import Request
|
||||
|
||||
|
||||
class UMLCreator:
|
||||
# TODO: в методах, обремененных некой логикой, постоянно встречается проверка режима if not self.theor_mode.
|
||||
# Есть предложение реализовать паттерн СТРАТЕГИЯ, который в зависимости от режима делал бы что-то
|
||||
# разными способами. Это бы помогло избавиться от спагетти и обособить логику работы в зависимости от ситуации.
|
||||
def __init__(self, request_generator: Request, path_to_save: str):
|
||||
self._request_generator = request_generator
|
||||
self.path_to_save = path_to_save
|
||||
@ -14,7 +11,7 @@ class UMLCreator:
|
||||
real_data = []
|
||||
ideal_data = []
|
||||
if not self.theor_mode:
|
||||
# TODO: Писать так условия - моветон. Как вариант, можно применить конструкцию match - case.
|
||||
|
||||
for key, items in self.timings_dict.items():
|
||||
if key == 'closure': ideal_time = self._ideal_time[0]
|
||||
elif key == 'compression': ideal_time = self._ideal_time[1]
|
||||
@ -26,15 +23,6 @@ class UMLCreator:
|
||||
real_data.append([item[1]*self.scaler, '{-}'])
|
||||
|
||||
if key == 'compression':
|
||||
# TODO: Дублирование проверки значения ключа. Можно объединить проверки вверху и тут
|
||||
# в одном match - case выражении:
|
||||
# match key:
|
||||
# case "compression":
|
||||
# ...
|
||||
# case "opening":
|
||||
# ...
|
||||
# case _:
|
||||
# ...
|
||||
ideal_data.append([(item[1]-ideal_time)*self.scaler, str(key) + '#yellow'])
|
||||
ideal_data.append([(item[1]-0.0001)*self.scaler, '{-}'])
|
||||
else:
|
||||
@ -42,16 +30,10 @@ class UMLCreator:
|
||||
ideal_data.append([(item[0]+ideal_time)*self.scaler, '{-}'])
|
||||
if key == 'opening':
|
||||
ideal_data.append([item[1]*self.scaler+0.0001, 'coming #yellow'])
|
||||
# TODO: elif key == 'opening': ideal_time = self._ideal_time[2]. Почему здесь при условии
|
||||
# значения ключа opening уже self._ideal_time[3]?
|
||||
ideal_data.append([(item[1]+self._ideal_time[3])*self.scaler, '{-}'])
|
||||
|
||||
else:
|
||||
# TODO: real_data уже определена как [] в самом начале. Если не попадаем в верхнее условие,
|
||||
# то она таковой и останется. Дублирование.
|
||||
real_data = []
|
||||
# TODO: Похоже, что все данные в этом списке - константы. Следовательно, можно проинициализировать этот
|
||||
# набор данных однократно при инициализации класса, а не делать это время всякий раз при вызове метода.
|
||||
ideal_data = [
|
||||
[0.0, 'closure #yellow'],
|
||||
[self._ideal_time[0] * self.scaler, '{-}'],
|
||||
@ -63,12 +45,7 @@ class UMLCreator:
|
||||
[(sum(self._ideal_time[:3]) + self.WeldTime) * self.scaler, 'coming #yellow'],
|
||||
[(sum(self._ideal_time[:4]) + self.WeldTime) * self.scaler, '{-}'],
|
||||
]
|
||||
# TODO: Очень неудачный способ хранения данных разного типа: сложен в восприятии,
|
||||
# легко ошибиться или запутаться.
|
||||
# Используй NamedTuple или dataclass: будут определены поля, их типы.
|
||||
# Взаимодействие также будет гораздо проще (по имени поля, а не индексу).
|
||||
# TODO: Похоже, что все данные в этом списке - константы. Следовательно, можно проинициализировать этот набор
|
||||
# данных однократно при инициализации класса, а не делать это время всякий раз при вызове метода.
|
||||
|
||||
client_data = [
|
||||
[0.0 * self.scaler, 'closure'],
|
||||
[0.165 * self.scaler, '{-}'],
|
||||
@ -80,7 +57,7 @@ class UMLCreator:
|
||||
[0.300 * self.scaler, '{-}'],
|
||||
]
|
||||
|
||||
# TODO: Зачем метод возвращает self.bool_dict, если никаких манипуляций с ним не проводит?
|
||||
|
||||
return real_data, client_data, ideal_data, self.bool_dict
|
||||
|
||||
def _generate_svg(self, real_data, client_data, ideal_data, bool_data) -> None:
|
||||
@ -93,8 +70,6 @@ class UMLCreator:
|
||||
|
||||
for i, [signal, changes] in enumerate(bool_data.items()):
|
||||
name = 'bool_' + str(i)
|
||||
# TODO: 3 строки ниже замени на list comprehension
|
||||
# times = [[str(float(f[0])*scaler), f[1]] for f in changes]
|
||||
times = []
|
||||
for f in changes:
|
||||
times.append([str(float(f[0])*self.scaler), f[1]])
|
||||
@ -123,9 +98,7 @@ class UMLCreator:
|
||||
timings_dict: dict,
|
||||
mode: bool,
|
||||
name:str):
|
||||
# TODO: Аргумент mode вводит в заблуждение: ожидаешь увидеть какой-то режим, на деле же это флаг
|
||||
# теоретического режима. Поправь название аргумента.
|
||||
# TODO: Инициализацию атрибутов класса в конструкторе надо делать!
|
||||
|
||||
self._ideal_time = ideal_time
|
||||
self.bool_dict = bool_dict
|
||||
self.float_dict = float_dict
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
from os.path import abspath
|
||||
|
||||
from plantuml import PlantUML
|
||||
from os.path import abspath
|
||||
|
||||
|
||||
class Request:
|
||||
# TODO: Имя класса не соответствует тому, что он делает.
|
||||
def __init__(self, server_url: str):
|
||||
self._server_url = server_url
|
||||
|
||||
@ -15,15 +13,12 @@ class Request:
|
||||
self.clear()
|
||||
|
||||
def _startUML(self):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self.reqArr.append('@startuml')
|
||||
|
||||
def _endUML(self):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self.reqArr.append('@enduml')
|
||||
|
||||
def clear(self):
|
||||
# TODO: Атрибуты класса инициализируются в конструкторе класса.
|
||||
self.timestamps = {}
|
||||
self.reqArr = []
|
||||
self.variables = []
|
||||
@ -32,36 +27,28 @@ class Request:
|
||||
self._startUML()
|
||||
|
||||
def addAnalog(self, name, string):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self.variables.append(f'analog "{string}" as {name}')
|
||||
|
||||
def addBinary(self, name, string, style = ''):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
if style: name = name + '<<' + style + '>>'
|
||||
self.variables.append(f'binary "{string}" as {name}')
|
||||
|
||||
def addClock(self, name, string):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self.variables.append(f'clock "{string}" as {name}')
|
||||
|
||||
def addConcise(self, name, string):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self.variables.append(f'concise "{string}" as {name}')
|
||||
|
||||
def addRobust(self, name, string):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self.variables.append(f'robust "{string}" as {name}')
|
||||
|
||||
def appendStr(self, string = ''):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self.variables.append(string)
|
||||
|
||||
def addLineStyle(self, name, color = 'green', thicknes = 1):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self.lineStyles[name] = [f'LineColor {color}', f'LineThickness {thicknes}']
|
||||
|
||||
def generateSVG(self, name):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self._compileUML(name)
|
||||
filename = abspath(f'{name}.txt')
|
||||
self.server.processes_file(filename, outfile=f'{name}.svg')
|
||||
@ -69,7 +56,6 @@ class Request:
|
||||
#return result
|
||||
|
||||
def setTimestamps(self, name, input):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
for time, state in input:
|
||||
try:
|
||||
self.timestamps[f'@{time}'].append(f'{name} is {state}')
|
||||
@ -77,13 +63,11 @@ class Request:
|
||||
self.timestamps[f'@{time}'] = [f'{name} is {state}']
|
||||
|
||||
def _addTimestamp(self, timecode, vars):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self.reqArr.append(timecode)
|
||||
for var in vars:
|
||||
self.reqArr.append(var)
|
||||
|
||||
def _addHeader(self):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self.reqArr.append('<style>')
|
||||
if self.lineStyles:
|
||||
self.reqArr.append('timingDiagram {')
|
||||
@ -96,13 +80,9 @@ class Request:
|
||||
self.reqArr.append('</style>')
|
||||
|
||||
def _addVariables(self):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
for var in self.variables: self.reqArr.append(str(var))
|
||||
|
||||
def _compileUML(self, name):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
# TODO: Метод и строку собирает, и в файл пишет. Разделить логику формирования строки и записи в файл
|
||||
# по разным методам.
|
||||
self._addHeader()
|
||||
self._addVariables()
|
||||
|
||||
@ -111,8 +91,6 @@ class Request:
|
||||
self._addTimestamp(key, item)
|
||||
|
||||
self._endUML()
|
||||
# TODO: Зачем делать из self.stringUML атрибут класса?
|
||||
# Используется только тут - достаточно локальной переменной.
|
||||
self.stringUML = [line + '\n' for line in self.reqArr]
|
||||
with open(f'{name}.txt', 'w', encoding='utf-8') as file:
|
||||
file.writelines(self.stringUML)
|
||||
|
||||
BIN
src/utils/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
src/utils/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/utils/__pycache__/base.cpython-310.pyc
Normal file
BIN
src/utils/__pycache__/base.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/utils/__pycache__/base_widgets.cpython-310.pyc
Normal file
BIN
src/utils/__pycache__/base_widgets.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/utils/__pycache__/diagram_parser.cpython-310.pyc
Normal file
BIN
src/utils/__pycache__/diagram_parser.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/utils/__pycache__/json_tools.cpython-310.pyc
Normal file
BIN
src/utils/__pycache__/json_tools.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/utils/__pycache__/qt_settings.cpython-310.pyc
Normal file
BIN
src/utils/__pycache__/qt_settings.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/utils/base/__pycache__/base.cpython-310.pyc
Normal file
BIN
src/utils/base/__pycache__/base.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/utils/base/__pycache__/base_widgets.cpython-310.pyc
Normal file
BIN
src/utils/base/__pycache__/base_widgets.cpython-310.pyc
Normal file
Binary file not shown.
613
src/utils/base/base.py
Normal file
613
src/utils/base/base.py
Normal file
@ -0,0 +1,613 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Optional, Union, Any
|
||||
from cachetools import LRUCache
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
|
||||
import pandas as pd
|
||||
from PyQt5.QtCore import QObject, QTimer
|
||||
from PyQt5.QtWidgets import QWidget, QTabWidget, QMainWindow, QVBoxLayout
|
||||
from OptAlgorithm import OptAlgorithm
|
||||
from utils.qt_settings import dark_style
|
||||
|
||||
|
||||
|
||||
|
||||
class BaseMediator:
|
||||
def __init__(self,
|
||||
monitor: BaseDirectoryMonitor,
|
||||
converter: BaseDataConverter,
|
||||
passportFormer: BasePointPassportFormer,
|
||||
plot: BasePlotWidget,
|
||||
controller: BaseController):
|
||||
self._monitor = monitor
|
||||
self._monitor.mediator = self
|
||||
self._converter = converter
|
||||
self._converter.mediator = self
|
||||
self._passportFormer = passportFormer
|
||||
self._passportFormer.mediator = self
|
||||
self._plot = plot
|
||||
self._plot.mediator = self
|
||||
self._controller = controller
|
||||
self._controller.mediator = self
|
||||
|
||||
def notify(self,
|
||||
source: Union[BaseDirectoryMonitor, BaseDataConverter, BasePointPassportFormer, BasePlotWidget],
|
||||
data: Union[list[str], list[pd.DataFrame], list[list], list[QWidget]]):
|
||||
...
|
||||
def update_settings (self, data: list[dict]):
|
||||
...
|
||||
|
||||
def update_status(self, msg: Union[str, float]) -> None:
|
||||
...
|
||||
|
||||
class BaseDirectoryMonitor:
|
||||
|
||||
update_timer = QTimer()
|
||||
|
||||
def __init__(self,
|
||||
mediator: Optional[BaseMediator] = None):
|
||||
super().__init__()
|
||||
self._directory_path = None
|
||||
self._update_time = None
|
||||
self.isActive = False
|
||||
self._files = []
|
||||
self._mediator = mediator
|
||||
|
||||
|
||||
@property
|
||||
def directory_path(self) -> str:
|
||||
return self._directory_path
|
||||
|
||||
@property
|
||||
def update_time(self) -> int:
|
||||
return self._update_time
|
||||
|
||||
@property
|
||||
def files(self) -> list[str]:
|
||||
return self._files
|
||||
|
||||
@property
|
||||
def mediator(self) -> BaseMediator:
|
||||
return self._mediator
|
||||
|
||||
@mediator.setter
|
||||
def mediator(self, mediator: BaseMediator) -> None:
|
||||
self._mediator = mediator
|
||||
|
||||
def _init_state(self):
|
||||
files = os.listdir(self._directory_path)
|
||||
self._files = files
|
||||
|
||||
def start(self):
|
||||
self.isActive = True
|
||||
self.update_timer.start(int(self._update_time))
|
||||
|
||||
def stop(self):
|
||||
self.isActive = False
|
||||
self.update_timer.stop()
|
||||
|
||||
def update_settings(self, data: list[dict]) -> None:
|
||||
...
|
||||
|
||||
def update_plots(self) -> None:
|
||||
...
|
||||
|
||||
def force_all_dir(self):
|
||||
...
|
||||
|
||||
class BaseDataConverter:
|
||||
def __init__(self, mediator: Optional[BaseMediator] = None):
|
||||
self._mediator = mediator
|
||||
|
||||
@property
|
||||
def mediator(self) -> BaseMediator:
|
||||
return self._mediator
|
||||
|
||||
@mediator.setter
|
||||
def mediator(self, mediator: BaseMediator) -> None:
|
||||
self._mediator = mediator
|
||||
|
||||
def convert_data(self, files: list[str]) -> None:
|
||||
...
|
||||
|
||||
|
||||
class BasePlotWidget:
|
||||
def __init__(self,
|
||||
mediator: Optional[BaseMediator] = None):
|
||||
super().__init__()
|
||||
self._mediator = mediator
|
||||
|
||||
self._stage_colors = {
|
||||
"Closing": [220, 20, 60, 100], # Crimson
|
||||
"Squeeze": [30, 144, 255, 100], # Dodger Blue
|
||||
"Welding": [128, 128, 128, 100], # Gray
|
||||
"Relief": [34, 139, 34, 100], # Forest Green
|
||||
"Oncomming": [255, 165, 0, 100] # Orange
|
||||
}
|
||||
self._plt_channels = {
|
||||
"Electrode Force, N & Welding Current, kA": {
|
||||
"Settings": {
|
||||
"zoom": False,
|
||||
"stages": True,
|
||||
"performance": True,
|
||||
"ideals": True,
|
||||
"mirror ME": False,
|
||||
"workpiece": False,
|
||||
"force compensation FE": False,
|
||||
"force accuracy":True
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Electrode Force, N ME",
|
||||
"pen": 'r',
|
||||
},
|
||||
{
|
||||
"name": "Electrode Force, N FE",
|
||||
"pen": 'w',
|
||||
},
|
||||
{
|
||||
"name": "Welding Current ME",
|
||||
"pen": "y",
|
||||
}
|
||||
],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Force",
|
||||
"pen": {'color': 'g', 'width':3},
|
||||
}
|
||||
]
|
||||
},
|
||||
"Electrode Position, mm": {
|
||||
"Settings": {
|
||||
"zoom": False,
|
||||
"stages": True,
|
||||
"performance": False,
|
||||
"ideals": True,
|
||||
"mirror ME": True,
|
||||
"workpiece": True,
|
||||
"force compensation FE": True,
|
||||
"force accuracy":False
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Rotor Position, mm ME",
|
||||
"pen": {'color': 'r', 'width':2},
|
||||
},
|
||||
{
|
||||
"name": "Rotor Position, mm FE",
|
||||
"pen": {'color': 'w', 'width':2},
|
||||
}
|
||||
],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Position ME",
|
||||
"pen": {'color': 'g', 'width':4},
|
||||
},
|
||||
{
|
||||
"name": "Position FE",
|
||||
"pen": {'color': 'b', 'width':4},
|
||||
}
|
||||
]
|
||||
},
|
||||
"Electrode Speed, mm/s": {
|
||||
"Settings": {
|
||||
"zoom": False,
|
||||
"stages": True,
|
||||
"performance": False,
|
||||
"ideals": True,
|
||||
"mirror ME": False,
|
||||
"workpiece": False,
|
||||
"force compensation FE": False,
|
||||
"force accuracy":False
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Rotor Speed, mm/s ME",
|
||||
"pen": 'r',
|
||||
"zoom": False
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed, mm/s FE",
|
||||
"pen": 'w',
|
||||
"zoom": False
|
||||
}
|
||||
],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Rotor Speed ME",
|
||||
"pen": {'color': 'g', 'width':3},
|
||||
"zoom": False
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed FE",
|
||||
"pen": {'color': 'b', 'width':3},
|
||||
"zoom": False
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
def set_style(self, object: Union[QTabWidget, QWidget]) -> None:
|
||||
object.setStyleSheet(
|
||||
"""QLabel {
|
||||
color: #ffffff;
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
}""")
|
||||
|
||||
def _downsample_data(self, x, y, max_points=5000):
|
||||
"""
|
||||
Понижает разрешение данных до заданного количества точек для улучшения производительности навигатора.
|
||||
"""
|
||||
if len(x) > max_points:
|
||||
factor = len(x) // max_points
|
||||
x_downsampled = x[::factor]
|
||||
y_downsampled = y[::factor]
|
||||
return x_downsampled, y_downsampled
|
||||
return x, y
|
||||
|
||||
def _create_navigator(self,
|
||||
time_region:tuple[float, float],
|
||||
main_plot: pg.PlotItem) -> list[pg.PlotWidget, pg.LinearRegionItem]:
|
||||
"""
|
||||
Создаёт график-навигатор, отображающий все данные в уменьшенном масштабе.
|
||||
"""
|
||||
navigator = pg.PlotItem(title="Navigator")
|
||||
navigator.setFixedHeight(100)
|
||||
# Получение кривых из main_plot
|
||||
for curve in main_plot.listDataItems():
|
||||
# Извлекаем данные из кривой
|
||||
x, y = curve.getData()
|
||||
curve_name = curve.opts.get("name", None)
|
||||
signal_pen = curve.opts.get("pen", None)
|
||||
x_downsampled, y_downsampled = self._downsample_data(x, y, max_points=1000)
|
||||
navigator.plot(x_downsampled, y_downsampled, pen=signal_pen, name=curve_name)
|
||||
|
||||
ROI_region = pg.LinearRegionItem(values=time_region, movable=True, brush=pg.mkBrush(0, 0, 255, 100), pen=pg.mkPen(width=4))
|
||||
ROI_region.setBounds([0, x[-1]])
|
||||
navigator.addItem(ROI_region)
|
||||
navigator.getViewBox().setLimits(xMin=0, xMax=x[-1])
|
||||
|
||||
# Связываем изменение региона навигатора с обновлением области просмотра основного графика
|
||||
ROI_region.sigRegionChanged.connect(lambda: self._sync_main_plot_with_navigator(main_plot, ROI_region))
|
||||
|
||||
return navigator, ROI_region
|
||||
|
||||
def _sync_main_plot_with_navigator(self,
|
||||
main_plot: pg.PlotItem,
|
||||
region: pg.LinearRegionItem):
|
||||
"""
|
||||
Синхронизирует область просмотра основного графика с регионом навигатора.
|
||||
"""
|
||||
x_min, x_max = region.getRegion()
|
||||
if main_plot:
|
||||
main_plot.blockSignals(True)
|
||||
main_plot.setXRange(x_min, x_max, padding=0)
|
||||
main_plot.blockSignals(False)
|
||||
|
||||
def _mirror_shift_data(self,
|
||||
valid_str: str,
|
||||
signals: list[dict],
|
||||
dataframe: pd.DataFrame,
|
||||
shift: float) -> pd.DataFrame:
|
||||
keys = dataframe.keys()
|
||||
for signal in signals:
|
||||
if valid_str in signal["name"] and signal["name"] in keys:
|
||||
dataframe[signal["name"]] = dataframe[signal["name"]].apply(lambda x: shift-x)
|
||||
return dataframe
|
||||
|
||||
def _mirror_shift_ideal(self,
|
||||
name_ME: str, name_FE: str,
|
||||
signals: list[dict],
|
||||
dataframe: pd.DataFrame,
|
||||
shift: float,
|
||||
P1: float, P2: float) -> pd.DataFrame:
|
||||
keys = dataframe.keys()
|
||||
for signal in signals:
|
||||
if name_ME in signal["name"] and signal["name"] in keys:
|
||||
dataframe[signal["name"]] = dataframe[signal["name"]].apply(lambda x: -x+P1)
|
||||
|
||||
if name_FE in signal["name"] and signal["name"] in keys:
|
||||
dataframe[signal["name"]] = dataframe[signal["name"]].apply(lambda x: x+P2)
|
||||
|
||||
return dataframe
|
||||
|
||||
def _shift_data(self,
|
||||
valid_str: str,
|
||||
signals: list[dict],
|
||||
dataframe: pd.DataFrame,
|
||||
shift: float) -> pd.DataFrame:
|
||||
keys = dataframe.keys()
|
||||
for signal in signals:
|
||||
if valid_str in signal["name"] and signal["name"] in keys:
|
||||
dataframe[signal["name"]] = dataframe[signal["name"]].apply(lambda x: x + shift)
|
||||
return dataframe
|
||||
|
||||
def _sync_navigator_with_main(self, main_plot: pg.PlotItem, region:pg.LinearRegionItem):
|
||||
"""
|
||||
Синхронизирует регион навигатора с областью просмотра основного графика.
|
||||
"""
|
||||
if region:
|
||||
x_min, x_max = main_plot
|
||||
region.blockSignals(True) # Предотвращаем рекурсию
|
||||
region.setRegion([x_min, x_max])
|
||||
region.blockSignals(False)
|
||||
|
||||
@property
|
||||
def mediator(self) -> BaseMediator:
|
||||
return self._mediator
|
||||
|
||||
@mediator.setter
|
||||
def mediator(self, mediator: BaseMediator) -> None:
|
||||
self._mediator = mediator
|
||||
|
||||
@property
|
||||
def opt(self) -> BaseIdealDataBuilder:
|
||||
return self._opt
|
||||
|
||||
@opt.setter
|
||||
def opt(self, opt: BaseIdealDataBuilder):
|
||||
self._opt = opt
|
||||
|
||||
def build(self, data: list[pd.DataFrame]) -> list[QWidget]:
|
||||
...
|
||||
|
||||
|
||||
class BaseController(QObject):
|
||||
|
||||
def send_widgets(self, widgets: list[QWidget]) -> None:
|
||||
...
|
||||
|
||||
def update_settings(self, settings: list[dict]) -> None:
|
||||
...
|
||||
|
||||
def raport_mode (self) -> None:
|
||||
...
|
||||
|
||||
def seeking_mode(self) -> None:
|
||||
...
|
||||
|
||||
def open_file(self, filepath: str) -> None:
|
||||
...
|
||||
|
||||
def update_plots(self) -> None:
|
||||
...
|
||||
|
||||
def update_status(self, msg: Union[str, float]) -> None:
|
||||
...
|
||||
|
||||
class BaseIdealDataBuilder(OptAlgorithm):
|
||||
|
||||
def __init__(self, params: list[dict]):
|
||||
operator_params, system_params = params
|
||||
self.mul = system_params['time_capture']
|
||||
self.welding_time = operator_params['time_wielding']
|
||||
|
||||
|
||||
|
||||
super().__init__(operator_params, system_params)
|
||||
|
||||
|
||||
def _get_data(self, end_timestamp:float, func:function) -> pd.DataFrame:
|
||||
data = []
|
||||
for i in range (0, int(end_timestamp*self.mul)+1):
|
||||
time = i/self.mul
|
||||
X1, X2, V1, V2, F = func(time)
|
||||
data.append({"time":time, "Position FE":X1*1000,"Position ME":X2*1000, "Rotor Speed FE":V1*1000, "Rotor Speed ME":V2*1000, "Force":F})
|
||||
X1, X2, V1, V2, F = func(end_timestamp)
|
||||
data.append({"time":end_timestamp, "Position FE":X1*1000,"Position ME":X2*1000, "Rotor Speed FE":V1*1000, "Rotor Speed ME":V2*1000, "Force":F})
|
||||
return pd.DataFrame(data)
|
||||
|
||||
def get_closingDF(self) -> pd.DataFrame:
|
||||
...
|
||||
|
||||
def get_compressionDF(self) -> pd.DataFrame:
|
||||
...
|
||||
|
||||
def get_openingDF(self) -> pd.DataFrame:
|
||||
...
|
||||
|
||||
def get_tmovementDF(self) -> pd.DataFrame:
|
||||
...
|
||||
|
||||
def get_weldingDF(self) -> pd.DataFrame:
|
||||
...
|
||||
|
||||
def get_oncomingDF(self) -> pd.DataFrame:
|
||||
...
|
||||
|
||||
def get_ideal_timings(self) -> list[float, float, float, float]:
|
||||
...
|
||||
|
||||
def get_cycle_time(self) -> float:
|
||||
result = sum(self.get_ideal_timings())
|
||||
return result
|
||||
|
||||
class BaseMainWindow(QMainWindow):
|
||||
def __init__(self,
|
||||
controller: Optional[BaseController] = None):
|
||||
super().__init__()
|
||||
self.resize(200,200)
|
||||
self._controller = controller
|
||||
# Создаем центральный виджет и устанавливаем его
|
||||
self._central_widget = QWidget()
|
||||
self.setCentralWidget(self._central_widget)
|
||||
|
||||
# Устанавливаем основной вертикальный макет для центрального виджета
|
||||
self._central_layout = QVBoxLayout()
|
||||
self._central_widget.setLayout(self._central_layout)
|
||||
self.set_style(self)
|
||||
...
|
||||
|
||||
@property
|
||||
def controller(self) -> BaseController:
|
||||
return self._controller
|
||||
|
||||
@controller.setter
|
||||
def controller(self, controller: BaseController) -> None:
|
||||
self._controller = controller
|
||||
|
||||
def set_style(self, object: Union[QTabWidget, QWidget, QMainWindow]) -> None:
|
||||
object.setStyleSheet(dark_style)
|
||||
|
||||
class BasePointPassportFormer:
|
||||
|
||||
def __init__(self,
|
||||
mediator: Optional[BaseMediator] = None):
|
||||
self._mediator = mediator
|
||||
self._clear_stage = "Welding"
|
||||
self._stages = [
|
||||
"Closing",
|
||||
"Squeeze",
|
||||
"Welding",
|
||||
"Relief",
|
||||
"Oncomming"
|
||||
]
|
||||
self._tesla_stages = [
|
||||
"Tesla squeeze",
|
||||
"Tesla closing",
|
||||
"Tesla welding",
|
||||
"Tesla oncomming_relief"
|
||||
]
|
||||
self._params = []
|
||||
self._ideal_data_cashe = LRUCache(maxsize=1000)
|
||||
self._OptAlgorithm_operator_params = [
|
||||
"dist_open_start_1",
|
||||
"dist_open_start_2",
|
||||
"dist_open_after_1",
|
||||
"dist_open_after_2",
|
||||
"dist_open_end_1",
|
||||
"dist_open_end_2",
|
||||
"dist_close_end_1",
|
||||
"dist_close_end_2",
|
||||
"time_command",
|
||||
"time_robot_movement",
|
||||
"object_thickness",
|
||||
"object_position",
|
||||
"distance_h_end1",
|
||||
"distance_h_end2",
|
||||
"distance_l_1",
|
||||
"distance_l_2",
|
||||
"force_target",
|
||||
"force_capture",
|
||||
"time_wielding"]
|
||||
self._OptAlgorithm_system_params = [
|
||||
"a_max_1",
|
||||
"v_max_1",
|
||||
"a_max_2",
|
||||
"v_max_2",
|
||||
"mass_1",
|
||||
"mass_2",
|
||||
"k_hardness_1",
|
||||
"k_hardness_2",
|
||||
"torque_max_1",
|
||||
"torque_max_2",
|
||||
"transmission_ratio_1",
|
||||
"transmission_ratio_2",
|
||||
"position_start_1",
|
||||
"position_start_2",
|
||||
"k_prop",
|
||||
"time_capture"]
|
||||
|
||||
def _find_indexes(self,
|
||||
signal: str,
|
||||
dataframe: pd.DataFrame) -> tuple[np.ndarray, np.ndarray]:
|
||||
stage_diff = np.diff(dataframe[signal])
|
||||
start_idx = np.where(stage_diff == 1)
|
||||
finish_idx = np.where(stage_diff == -1)
|
||||
return start_idx[0], finish_idx[0]
|
||||
|
||||
def _find_events(self,
|
||||
signal: str,
|
||||
times:pd.Series,
|
||||
dataframe: pd.DataFrame) -> tuple[list[float], list[float]]:
|
||||
start_idx, finish_idx = self._find_indexes(signal, dataframe)
|
||||
|
||||
if len(start_idx) > 0 and len(finish_idx) > 0 and start_idx[0] > finish_idx[0]:
|
||||
start_idx = np.insert(start_idx, 0, 0)
|
||||
start_list = times.iloc[start_idx].tolist() if len(start_idx) > 0 else []
|
||||
end_list = times.iloc[finish_idx].tolist() if len(finish_idx) > 0 else []
|
||||
if len(start_list) - len(end_list) == 1:
|
||||
end_list.append(float(times.iloc[-1]))
|
||||
|
||||
return start_list, end_list
|
||||
|
||||
def _filter_events(self,
|
||||
times: pd.Series,
|
||||
dataframe: pd.DataFrame) -> tuple[dict[str, list[list[float]]], int]:
|
||||
events = {}
|
||||
point_quantity = 0
|
||||
if self._clear_stage in self._stages:
|
||||
start_list, end_list = self._find_events(self._clear_stage, times, dataframe)
|
||||
point_quantity = len(start_list)
|
||||
if point_quantity == 0:
|
||||
#TODO: добавить обработку исключения
|
||||
return []
|
||||
|
||||
for stage in self._stages:
|
||||
start_list, end_list = self._find_events(stage, times, dataframe)
|
||||
temp = min([len(start_list), len(end_list)])
|
||||
if temp < point_quantity:
|
||||
print ("cant find enough", stage)
|
||||
start_list += [0]*(point_quantity - temp)
|
||||
end_list += [1]*(point_quantity - temp)
|
||||
|
||||
events[stage] = [start_list, end_list]
|
||||
return events, point_quantity
|
||||
|
||||
def _build_ideal_data(self,
|
||||
idealDataBuilder: Optional[BaseIdealDataBuilder] = None,
|
||||
params: list[dict] = None) -> dict:
|
||||
self.opt = idealDataBuilder(params)
|
||||
stage_ideals = {
|
||||
"Closing": self._opt.get_closingDF(),
|
||||
"Squeeze": self._opt.get_compressionDF(),
|
||||
"Welding": self._opt.get_weldingDF(),
|
||||
"Relief": self._opt.get_openingDF(),
|
||||
"Oncomming": self._opt.get_oncomingDF(),
|
||||
"Ideal cycle": self._opt.get_cycle_time(),
|
||||
"Ideal timings": self._opt.get_ideal_timings()
|
||||
}
|
||||
return stage_ideals
|
||||
|
||||
def form_passports(self) -> list[list[pd.DataFrame, dict, list]]:
|
||||
...
|
||||
|
||||
def update_settings(self, params: list) -> None:
|
||||
...
|
||||
|
||||
def _generate_cache_key(self,
|
||||
params_list: list[dict[str, Any]]) -> tuple:
|
||||
"""
|
||||
Преобразует params_list в хешируемый ключ для кэша.
|
||||
"""
|
||||
operator_settings, system_settings = params_list
|
||||
|
||||
# Преобразуем словари в отсортированные кортежи пар ключ-значение
|
||||
operator_tuple = frozenset((key, value)
|
||||
for key, value in operator_settings.items()
|
||||
if str(key) in self._OptAlgorithm_operator_params)
|
||||
|
||||
system_tuple = frozenset((key, value)
|
||||
for key, value in system_settings.items()
|
||||
if str(key) in self._OptAlgorithm_system_params)
|
||||
return (operator_tuple, system_tuple)
|
||||
|
||||
@property
|
||||
def opt(self) -> BaseIdealDataBuilder:
|
||||
return self._opt
|
||||
|
||||
@opt.setter
|
||||
def opt(self, opt: BaseIdealDataBuilder):
|
||||
self._opt = opt
|
||||
|
||||
@property
|
||||
def mediator(self) -> BaseMediator:
|
||||
return self._mediator
|
||||
|
||||
@mediator.setter
|
||||
def mediator(self, mediator: BaseMediator) -> None:
|
||||
self._mediator = mediator
|
||||
@ -9,34 +9,27 @@ class DiagramParser:
|
||||
system_config["Welding_signal"],
|
||||
system_config["Release_signal"],
|
||||
system_config["Oncomming_signal"]]
|
||||
# TODO: Переменные в Python именуются в lowercase либо snake_case.
|
||||
|
||||
self.boolDict = {}
|
||||
self.floatDict = {}
|
||||
self.timingsDict = {}
|
||||
self.theor_mode = False
|
||||
|
||||
def setData(self, path):
|
||||
# TODO: Методы в Python именуются в lowercase либо snake_case.
|
||||
# TODO: Данный метод устанавливает какой-то режим, читает CSV, наполняет словари некими данными.
|
||||
# Декомпозировать на разные методы, каждый из которых будет решать какую-то свою задачу.
|
||||
if not path:
|
||||
self.theor_mode = True
|
||||
else:
|
||||
self.data = pd.read_csv(path)
|
||||
|
||||
# TODO: 2 цикла ниже: дублирование кода. Устранить.
|
||||
for signalName in self.data.columns:
|
||||
# TODO: if isinstance...
|
||||
if type (self.data[signalName].iloc[0]) == np.bool:
|
||||
self.boolDict[signalName] = self._getBoolChanges(signalName)
|
||||
|
||||
for signalName in self.data.columns:
|
||||
# TODO: if isinstance...
|
||||
if type (self.data[signalName].iloc[0]) == np.float64:
|
||||
self.floatDict[signalName] = self._getFloatChanges(signalName)
|
||||
|
||||
for key, items in self.boolDict.items():
|
||||
# TODO: Писать так условия - моветон. Как вариант, можно применить конструкцию match - case.
|
||||
if key == self.signals[0]: name = "closure"
|
||||
elif key == self.signals[1]: name = "compression"
|
||||
elif key == self.signals[2]: name = "welding"
|
||||
@ -53,34 +46,22 @@ class DiagramParser:
|
||||
self.timingsDict[name].append([items[i][0], items[i][0]+0.01])
|
||||
|
||||
def getBoolDict (self) -> dict:
|
||||
# TODO: сделать через декоратор @property:
|
||||
# @property
|
||||
# def bool_dict(self) -> dict:
|
||||
# return self._bool_dict
|
||||
# TODO: Методы в Python именуются в lowercase либо snake_case.
|
||||
return self.boolDict
|
||||
|
||||
def getFloatDict (self) -> dict:
|
||||
# TODO: сделать через декоратор @property:
|
||||
# TODO: Методы в Python именуются в lowercase либо snake_case.
|
||||
return self.floatDict
|
||||
|
||||
def getRealTimings(self) -> dict:
|
||||
# TODO: сделать через декоратор @property:
|
||||
# TODO: Методы в Python именуются в lowercase либо snake_case.
|
||||
return self.timingsDict
|
||||
|
||||
def getMode(self) -> bool:
|
||||
# TODO: сделать через декоратор @property:
|
||||
# TODO: Методы в Python именуются в lowercase либо snake_case.
|
||||
return self.theor_mode
|
||||
|
||||
def _getBoolChanges(self, signalName) -> list:
|
||||
# TODO: Методы в Python именуются в lowercase либо snake_case.
|
||||
timeCode = self.data['time']
|
||||
signal_values = self.data[signalName]
|
||||
changes = []
|
||||
# TODO: if len(signal_values)...
|
||||
|
||||
if len(signal_values) > 0:
|
||||
changes.append([float(timeCode.iloc[0]), 'high' if bool(signal_values.iloc[0]) else 'low'])
|
||||
|
||||
@ -92,7 +73,6 @@ class DiagramParser:
|
||||
|
||||
|
||||
def _getFloatChanges(self, signalName) -> list:
|
||||
# TODO: Методы в Python именуются в lowercase либо snake_case.
|
||||
timeCode = self.data['time']
|
||||
signal_values = self.data[signalName]
|
||||
changes = []
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
# TODO: Было бы логично разместить данный модуль в каталоге gui.
|
||||
|
||||
dark_style = """
|
||||
/*
|
||||
------------------------------------------------------
|
||||
@ -285,10 +283,6 @@ QTableView {
|
||||
border: 1px solid #424242;
|
||||
selection-background-color: #FFCC00; /* Жёлтый при выделении */
|
||||
selection-color: #121212; /* Тёмный цвет текста при выделении */
|
||||
font-size: 14px;
|
||||
}
|
||||
QTableView QLineEdit {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Горизонтальный заголовок в таблицах */
|
||||
@ -455,16 +449,6 @@ QLabel {
|
||||
}"""
|
||||
|
||||
colors = [
|
||||
'#FF6F61', # яркий коралловый
|
||||
'#6B5B95', # приглушенный фиолетовый
|
||||
'#88B04B', # яркий зеленый
|
||||
'#F7CAC9', # светлый розовый
|
||||
'#92A8D1', # светло-голубой
|
||||
'#955251', # теплый терракотовый
|
||||
'#B565A7', # лавандовый
|
||||
'#009B77', # глубокий бирюзовый
|
||||
'#DD4124', # ярко-красный
|
||||
'#45B8AC', # мягкий мятный
|
||||
'#FF6F61', # яркий коралловый
|
||||
'#6B5B95', # приглушенный фиолетовый
|
||||
'#88B04B', # яркий зеленый
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user