Skip to content

Commit cbf9c57

Browse files
authored
Merge pull request #2747 from AnsbigetHildElarik/thermodynamics-pyfa
Thermodynamics Calculations
2 parents cc76e48 + 5a49d28 commit cbf9c57

3 files changed

Lines changed: 297 additions & 0 deletions

File tree

gui/builtinViewColumns/heat.py

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
# =============================================================================
2+
# 2026 Ansbiget Hild Elarik
3+
#
4+
# This file is part of pyfa.
5+
#
6+
# pyfa is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# pyfa is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
18+
# =============================================================================
19+
20+
# noinspection PyPackageRequirements
21+
import wx
22+
import math
23+
24+
from gui.bitmap_loader import BitmapLoader
25+
from eos.const import FittingModuleState
26+
from eos.saveddata.fit import Fit
27+
from eos.saveddata.module import Module
28+
from gui.viewColumn import ViewColumn
29+
from service.fit import Fit
30+
31+
import gui.mainFrame
32+
33+
class Thermodynamics():
34+
def __init__(self, fit):
35+
self.fit = fit
36+
self.hgm = fit.ship.getModifiedItemAttr("heatGenerationMultiplier")
37+
self.harm = self.calcHeatAbsorbtionRateModifier()
38+
self.slotfactor = self.calcSlotFactor()
39+
self.simTime = 120
40+
41+
def getSlotPos(self, mod): # get rack position of mod, 0-7
42+
rack = []
43+
for m in self.fit.modules:
44+
if m.slot == mod.slot:
45+
rack.insert(0, m)
46+
47+
for i, m in enumerate(rack):
48+
if m == mod:
49+
return i
50+
51+
def calcHeatAbsorbtionRateModifier(self):
52+
harm = [0,0,0,0] # 0 is a dummy slot, align with mod.slot constants, 1=low, 2=med, 3=hi, 4=rig, ...
53+
54+
for mod in self.fit.modules:
55+
if(mod.state == FittingModuleState.OVERHEATED):
56+
harm[mod.slot] += mod.getModifiedItemAttr("heatAbsorbtionRateModifier")
57+
58+
return harm
59+
60+
"""
61+
HANGAR.ShipInfoThermodynamics.prototype.getHARM = function() {
62+
var harm = [0,0,0];
63+
var rack = ["hs", "ms", "ls"];
64+
65+
for(var i = 0; i < rack.length; i++) {
66+
67+
for(var j = 1; j <= 8; j++) {
68+
// if slot and slot is overheated
69+
if(this.shipinfo.ship.slots[rack[i]+j] && this.fitwindow.slots[rack[i]+j].find(" .sloticon").hasClass("heat") ) {
70+
harm[i] += this.shipinfo.ship.slots[rack[i]+j].heatAbsorbtionRateModifier;
71+
}
72+
}
73+
}
74+
75+
return harm;
76+
};
77+
"""
78+
79+
def calcSlotFactor(self):
80+
slots = self.fit.ship.getModifiedItemAttr("hiSlots") + self.fit.ship.getModifiedItemAttr("medSlots") + self.fit.ship.getModifiedItemAttr("lowSlots")
81+
empty = self.fit.getSlotsFree(3) + self.fit.getSlotsFree(2) + self.fit.getSlotsFree(1) # FittingSlot.HIGH doesn"t work here?
82+
rigslots = self.fit.getNumSlots(4)
83+
84+
return (slots - empty) / (slots + rigslots)
85+
86+
"""
87+
HANGAR.ShipInfoThermodynamics.prototype.getSlotFactor = function() {
88+
var slots = 0;
89+
var emptyslots = 0;
90+
for(var i = 1; i <= 8; i++) {
91+
92+
var hs = this.fitwindow.slots["hs"+i];
93+
var ms = this.fitwindow.slots["ms"+i];
94+
var ls = this.fitwindow.slots["ls"+i];
95+
96+
if(hs.hasClass("highslot") ) {
97+
slots++;
98+
if(!hs.hasClass("occupied") || hs.hasClass("offline") ) {
99+
emptyslots++;
100+
}
101+
}
102+
if(ms.hasClass("midslot") ) {
103+
slots++;
104+
if(!ms.hasClass("occupied") || ms.hasClass("offline") ) {
105+
emptyslots++;
106+
}
107+
}
108+
if(ls.hasClass("lowslot") ) {
109+
slots++;
110+
if(!ls.hasClass("occupied") || ls.hasClass("offline") ) {
111+
emptyslots++;
112+
}
113+
}
114+
}
115+
116+
return (slots-emptyslots)/(slots + this.shipinfo.ship.data.rigSlots);
117+
};
118+
"""
119+
120+
def calcDamageProbability(self, mod, t): # get chance the module is damaged when overheated at time t
121+
keys = ["", "heatAttenuationLow", "heatAttenuationMed", "heatAttenuationHi"]
122+
att = self.fit.ship.getModifiedItemAttr(keys[mod.slot], 0.25)
123+
rackheat = 1 - pow(math.e, (-t * self.hgm * self.harm[mod.slot]))
124+
slotpos = self.getSlotPos(mod)
125+
126+
probs = []
127+
for m in self.fit.modules:
128+
if (m == mod): continue
129+
if m.slot == mod.slot:
130+
if m.state == FittingModuleState.OVERHEATED:
131+
i = self.getSlotPos(m)
132+
pos = abs(i - slotpos) # get rack distance to other overheated module
133+
probs.append(pow(att, pos) * self.slotfactor * rackheat)
134+
135+
p = 1
136+
for i in range(0, len(probs)):
137+
p *= (1 - probs[i])
138+
139+
selfprob = self.slotfactor * rackheat
140+
res = selfprob if p == 1 else 1 - p * (1 - selfprob)
141+
142+
return res
143+
144+
"""
145+
HANGAR.ShipInfoThermodynamics.prototype.getDamageProb = function(slot, t) {
146+
var rack = slot[0] == "h" ? "hs" : slot[0] == "m" ? "ms" : "ls";
147+
var harmNdx = rack === "hs" ? 0 : rack === "ms" ? 1 : 2;
148+
var att = rack == "hs" ? this.shipinfo.ship.data.heatAttenuationHi :
149+
rack == "ms" ? this.shipinfo.ship.data.heatAttenuationMed :
150+
this.shipinfo.ship.data.heatAttenuationLow ?
151+
this.shipinfo.ship.data.heatAttenuationLow : 0.25;
152+
153+
var slotpos = parseInt( slot.substr(2) );
154+
var rackheat = 1 + -Math.pow(Math.E, (-t * this.hgm * this.harm[harmNdx]));
155+
156+
var prob = [];
157+
for(var i = 1; i <= 8; i++) {
158+
if(rack+i == slot) continue;
159+
if(this.shipinfo.ship.slots[ rack+i ] && this.shipinfo.ship.slots[ rack+i ].state === "overload"){
160+
var pos = Math.abs(i - slotpos);
161+
prob.push( Math.pow(att, pos)*this.slotfactor*rackheat );
162+
}
163+
}
164+
165+
var p = 1;
166+
for(var i = 0; i < prob.length; i++) {
167+
p *= (1-prob[i]);
168+
}
169+
170+
var selfprob = this.slotfactor * rackheat;
171+
if(p === 1) {
172+
return selfprob;
173+
} else {
174+
return 1 - p*(1-selfprob);
175+
}
176+
};
177+
"""
178+
179+
def calcBurnCycles(self, mod): # estimates the number of cycles a module will OH before it burns out
180+
speed = mod.getModifiedItemAttr("speed")
181+
duration = mod.getModifiedItemAttr("duration")
182+
inc = speed / 1000 if speed else duration / 1000
183+
t = inc
184+
185+
fp = [] # failure probabilities
186+
p = lastp = 0
187+
while(t < self.simTime):
188+
p = self.calcDamageProbability(mod, t)
189+
fp.append(p)
190+
191+
if f"{p:.2f}" == f"{lastp:.2f}":
192+
break
193+
194+
t += inc
195+
lastp = p
196+
197+
E = 0 # expected wait to failure
198+
n = math.ceil(mod.getModifiedItemAttr("hp") / mod.getModifiedItemAttr("heatDamage")) # fault tolerance
199+
a = [1]
200+
201+
for i in range(n):
202+
a.append(0)
203+
204+
for t, fp_t in enumerate(fp):
205+
E += (t + 1) * fp_t * a[n - 1]
206+
207+
for k in range(n - 1, 0, -1):
208+
a[k] = (1 - fp_t) * a[k] + fp_t * a[k - 1]
209+
210+
a[0] = (1 - fp_t) * a[0]
211+
212+
for k in range(n):
213+
E += (t + 1 + (n - k) * (1 / fp[t])) * a[k]
214+
215+
return math.floor(E)
216+
217+
"""
218+
HANGAR.ShipInfoThermodynamics.prototype.calcBurnCycles = function(slot) {
219+
var fp = [];
220+
var p = 0, lastp = 0;
221+
var mod = this.shipinfo.ship.slots[slot];
222+
var inc = mod.speed ? mod.speed/1000 : mod.duration/1000;
223+
var t = inc;
224+
225+
while(t < this.simTime) {
226+
p = this.getDamageProb(slot, t);
227+
fp.push(p);
228+
if(p.toFixed(2) === lastp.toFixed(2)) break;
229+
t += inc;
230+
lastp = p;
231+
}
232+
233+
//http://jsfiddle.net/kkspy/86/
234+
var E = 0;
235+
var n = Math.ceil(mod.hp / mod.heatDamage);
236+
var a = [1];
237+
for(var i = 1; i < n;i++) { a.push(0); }
238+
239+
for(var t = 0; t < fp.length; t++) {
240+
E += (t+1)*fp[t]*a[n-1];
241+
for(var k = n-1; k > 0; k--) {
242+
a[k] = (1-fp[t])*a[k] + fp[t]*a[k-1];
243+
}
244+
a[0] = (1-fp[t])*a[0];
245+
}
246+
247+
t--;
248+
for(var k = 0; k < n; k++) {
249+
E += ( t+1 + (n-k)*(1/fp[t]))*a[k];
250+
}
251+
252+
return Math.floor(E);
253+
};
254+
"""
255+
256+
class Heat(ViewColumn):
257+
name = "Heat"
258+
259+
def __init__(self, fittingView, params):
260+
ViewColumn.__init__(self, fittingView)
261+
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
262+
self.resizable = False
263+
self.size = 54
264+
self.maxsize = self.size * 2
265+
self.imageId = fittingView.imageList.GetImageIndex("state_overheated_small", "gui")
266+
self.bitmap = BitmapLoader.getBitmap("state_overheated_small", "gui")
267+
self.mask = wx.LIST_MASK_IMAGE
268+
269+
def getText(self, mod):
270+
if not isinstance(mod, Module) or mod.state != FittingModuleState.OVERHEATED:
271+
return ""
272+
273+
thermo = Thermodynamics(Fit.getInstance().getFit(self.mainFrame.getActiveFit()))
274+
burnCycles = thermo.calcBurnCycles(mod)
275+
duration = mod.getModifiedItemAttr("duration") / 1000
276+
speed = mod.getModifiedItemAttr("speed") / 1000
277+
cycleTime = duration or speed
278+
279+
t = burnCycles * cycleTime
280+
s = t % 60
281+
m = (t / 60) % 60
282+
h = (t / 3600) % 24
283+
out = [f"{int(m):02d}", f"{int(s):02d}"]
284+
285+
if int(h) > 0: # hours is rarely relevant, only show if it is
286+
out.insert(0, f"{int(h):02d}")
287+
288+
return ":".join(out) # display as 00:00:00 to vertically align across slot cols consistently
289+
290+
def getToolTip(self, mod):
291+
if isinstance(mod, Module) and mod.state == FittingModuleState.OVERHEATED:
292+
return "Estimated time til burnout" # TODO localize
293+
294+
295+
Heat.register()

gui/builtinViews/fittingView.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ class FittingView(d.Display):
148148
"Miscellanea",
149149
"Price",
150150
"Ammo",
151+
"Heat",
151152
]
152153

153154
def __init__(self, parent):

gui/viewColumn.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ def delayedText(self, display, colItem):
8383
graphColor,
8484
graphLightness,
8585
graphLineStyle,
86+
heat,
8687
maxRange,
8788
misc,
8889
price,

0 commit comments

Comments
 (0)