ShotgunNinja / Kerbalism

Hundreds of Kerbals were killed in the making of this mod.
The Unlicense
43 stars 19 forks source link

Proposal for thermal control + various changes #105

Open gotmachine opened 7 years ago

gotmachine commented 7 years ago

The PR contains a proposal for the thermal control system and various tweaks. Meant as a reference for future implementation

CHANGES :

Issue #81 : Being able to use different modifiers for input rate and degeneration rate in a rule

Added an optional "modifier_degen" field to rule, that allow to specify a separate set of modifiers for the degeneration rate. I tried to check "modifiers_degen" in all places where rules modifiers existence was checked, but this may need to be reviewed.

Issue #114 : Temperature inconsistency between loaded / unloaded vessels

Sim.Temperature() now doesn't use the skinTemp of the vessel root part.

Issue #83 : Thermal control

The proposed system has the following features :

Other modifiers changes / addition

Planner changes

ShotgunNinja commented 7 years ago

First of all, thanks a lot for your continuous contributions to this project. I see that you are growing familiar with the codebase, and in general I like what you are doing here. I just added a couple comments in the changes about minor performance issues.

Now, you are probably aware I'm about to refactor/rewrite a lot of stuff. One of the potential things is the modifier, that I want to improve.

I already got a 'generic expression parser' done. Its very fast, and the only limitation is that it doesn't support operator precedence.

So I was thinking to introduce the concept of a 'modifier-expression'.

That way you can write modifier-expressions like:

modifier = max(env.temperature - 295, 5) - 5

to get temperature above survival range. Note how the actual settings of survival range just become a part of the modifier.

Or you could write a modifier-expression like:

modifier = Atmosphere.rate > 0

that will evaluate to 1.0 when something is pressurizing the habitat, and 0.0 otherwise.

Or this one:

modifier = Atmosphere.level > 0.9

that is 1.0 if pressure is ~90 kPA, and 0.0 otherwise.

This new 'modifier-expressions' concept is also going to be used in two other (awesome) new things that are coming: a new telemetry system where arbitrary modifier-expression readings are provided by parts/rules, and a new alert system where arbitrary modifier-expression conditions are checked by parts/rules.

The new telemetry system in particular is going to be relevant here. You have already understood the fundamental impossibility of providing the current planner analysis panels in a generic rule-driven framework. Therefore the current planner panels are a sort of 'hack', good enough but show its limitations as soon as the user start specifying rules very different than the ones I developed against. I hope the new telemetry system may completely replace the planner panels too.

This is all very early-design / experiments. I'm really interested in what you think about these proposed improvements. Best!

gotmachine commented 7 years ago

Thanks for the reviewing of my code. I'm a hobbyist programmer and while I usually succeed in making things work, I'm really struggling to find the most efficient/fast/clean way of doing it, so your remarks are super useful.

All this is meant to be an implementation proposal, not a real PR. It would require a testing / fixing / optimization pass as well as a rebasing on your dev branch.

The modifier change seems a good idea, indeed having to maintain duplicates of functional code for flight and editor (planner) situations is not ideal. My tough on this when I discovered how all this work is that ideally, the "Vessel" reference in the "vessel_info" class should be allowed to be null. Then all the functions used to calculate the "vessel_info" properties should be able to perform with the actual vessel being optional.

This is what I did for the Habitat static function that calculate atmospheric flux and use the root part skinTemp when loaded to account for reentry heat. This allow me to use the same function in planner and vessel_info: double Habitat.atmo_flux(CelestialBody mainBody, double altitude, double surface, double env_temperature, Vessel v = null) That allow me to use it like that in vessel_info : Habitat.atmo_flux(v.mainBody, v.altitude, surface, temperature, v) And like that in the planner : Habitat.atmo_flux(env.body, env.altitude, surface, env.temperature) Then in the function, to illustrate : some_value = (v == null) ? fallback_value_for_planner : value_when_loaded;

Unless I misunderstand what you are proposing, it seems to me that this is what you are trying to do : an unified vessel variables cache that would be indifferent to the game context (editor/flight loaded/flight unloaded). If this happen and together with your (awesome) "modifier-expression" idea, this could indeed allow a config-driven planner (and in-flight monitor) UI system. Another thing that I was thinking is that processes and rules should be merged in a single system. Maybe this is too much but here is some prospective work for an unified system, using habitat thermal control as an example :

Rules
{
  // a VesselValue is like a resource, excepted it isn't stored in a part but by vessel
  // when docking, the resulting vessel get the sum of the two vessels amount & capacity
  // the capacity modifier allow to determine factor to split the stored amount between the two vessels after undocking 
  VesselValue
  {
    name = neg_kjoule             // this can be used like a resource in modifier-expression (amount/capacity/level...)
    title = kJ                    // name of the value to display in the GUI    
    capacity = neg_kjoule_amount  // modifier used to calculate max amount of VesselValue, if unspecified capacity = amount at all times
  }

  Modifier
  {
    name = neg_kjoule_amount
    expression = env.volume * 100000.0 * 25.0 // min temperature is ideal temperature - 25 K
  }

  // heat losses from environment
  ValueInput
  {
    name = heat losses          // reference/id to use in the GUI
    value = neg_kjoule          // value to add
    rate = 1.0
    interval = 1.0
    modifier = watt_flux_neg
  }

  // heat gains from environment
  ValueOutput
  {
    name = heat gains
    value = neg_kjoule    
    rate = 1.0
    interval = 1.0
    modifier = watt_flux_pos
  }

  // calculate net thermal flux (kW) for the habitat (real formula would more complex)
  Modifier
  {
    name = net_flux
    expression = ((5,67E-08 * env.surface * env.temperature * env.temperature * env.temperature * env.temperature) - (5,67E-08 * env.surface * 0.5 * env.temperature * env.temperature * env.temperature * env.temperature ) + (env.crew_count * 30.0)) * 0.001
    // would be great to be able to do pow(env.temperature,4.0)
  }

  Modifier
  {
    name = watt_flux_pos
    expression = (net_flux > 0.0) * net_flux
  }

  Modifier
  {
    name = watt_flux_neg
    expression = (net_flux < 0.0) * net_flux * -1.0 // is abs(net_flux) possible ?
  }

  // used to determine the amount of ECLSS heater available, for use in the ProcessControler module
  // no capacity specified : capacity is ignored (made equal to the amount at all times)
  VesselValue
  {
    name = _heater               // this can be used like a resource in modifier-expression (amount/capacity/level...)
    title = ECLSS heater
  }

  // the heater should not remove more heat than the stored heat !
  Modifier
  {
    name = heater_output
    expression = min(neg_kjoule.amount, _heater.amount)
  }

  // ECLSS heater input
  ResourceInput
  {
    name = heater
    resource = ElectricCharge
    rate = 1.0
    modifier = heater_output
  }

  // ECLSS heater output
  ValueInput
  {
    name = heater
    value = neg_kjoule    
    rate = 1.0
    interval = 1.0
    modifier = heater_output
  }

  // a KerbalValue is like a resource, excepted it isn't stored in a part but by each Kerbal on the vessel
  KerbalValue
  {
    name = kerbal_neg_temp             // this can be used like a resource in modifier-expression (amount/capacity/level...)
    capacity = kerbal_neg_temp_amount  // modifier used to calculate the capacity
    ksc_reset = true                   // is the KerbalValue amount set to 0.0 when the kerbal get back to KSC
    vessel_reset = true                // is the KerbalValue amount set to 0.0 when the vessel the kerbal is in is modified
  }

  Modifier
  {
    name = kerbal_neg_temp_amount
    expression = 60 * 60 * 24   // 24 hours to death at min temperature
  }

  ValueInput
  {
    value = kerbal_neg_temp          
    rate = 1.0
    interval = 1.0
    modifier = hab_neg_temp_input
  }

  Modifier
  {
    name = hab_neg_temp_input
    expression = pow(neg_kjoule.level,5.0) 
  }

  ValueOuput
  {
    value = kerbal_neg_temp    
    rate = 1.0
    interval = 1.0
    modifier = hab_pos_temp_output
  }

  Modifier
  {
    name = hab_neg_temp_output
    expression = neg_kjoule.level < 0.01 
  }

  Event
  {
    text = $ON_VESSEL$KERBAL froze to death   // the text to show on screen
    timewarp_break = true                     // does the event stop timewarp
    unfocused_show = true                     // does the text show up if this isn't the current vessel
    modifier = neg_temp_death                 // modifier-expression that trigger the event when turning from false to true
    consequence = death                       // death/breakdown/none
  }

  Modifier
  {
    name = pos_temp_death
    expression = kerbal_pos_temp.amount == kerbal_pos_temp.capacity
  }

  Modifier
  {
    name = watt_flux_pos
    expression = info.tempertature * info.
  }

  PlannerPanel
  {
    title = Thermal control                 // name of the panel
    section = 3                             // where to put it in the planner window
  }

  PlannerLine
  {
    panel = Thermal control                 // the line should be in this panel
    title = Environment flux (kW)           // the line left text
    value = net_flux                        // the line right text, modifier or use "" to evaluate as a string
  }

  PlannerLine
  {
    panel = Thermal control      // the line should be in this panel
    title = death by freezing    // the line left text
    value = freezing_time        // the line right text, modifier or use "" to evaluate as a string
    value_format = duration      // returned value is Lib.HumanReadableDuration(value)
    color = #ff0000              // color of the value
    visible = freezing_isvisible // modifier-expression that determine visibility
  }

  PlannerLine
  {
    panel = Thermal control        // the line should be in this panel
    title = temperature is stable  // the line left text
    color = #00ff00                // color of the value
    visible = tempstable_isvisible // modifier-expression that determine visibility
  }

  Modifier
  {
    name = freezing_time
    expression = (neg_kjoule.capacity / neg_kjoule.rate) + (60 * 60 * 24)
  }

  Modifier
  {
    name = freezing_isvisible
    expression = neg_kjoule.rate > 0.0
  }

  Modifier
  {
    name = tempstable_isvisible
    expression = (neg_kjoule.rate == 0.0) && (pos_kjoule.rate == 0.0)
  }

Another GUI example for stress :

Rule
{
  ...

  Event
  {
    text = The crew is feeling gravity again // the text to show on screen
    timewarp_break = false                   // does the event stop timewarp
    unfocused_show = false                   // does the text show up if this isn't the current vessel
    modifier = gravity_message               // modifier-expression that trigger the alert when turning from false to true
    consequence = none                       // death/breakdown/none
  }

  Modifier
  {
    name = gravity_message
    expression = env.firm_ground == 1.0
  }

  PlannerLine
  {
    title = breakdown                       // the line left text
    value = 1.0 / (degen modifier formula)  // the line right text, modifier-expression or use "" to evaluate as a string
    value_format = duration                 // returned value is Lib.HumanReadableDuration(value)
    visible = info.comfort < 0.2            // modifier-expression that determine visibility
  }
  PlannerLine
  {
    title = comfort              // the line left text
    value = "poor"               // the line right text, modifier-expression or use "" to evaluate as a string
    color = #ff0000              // color of the value
    visible = info.comfort < 0.2 // modifier-expression that determine visibility
  }
  PlannerLine
  {
    title = comfort
    value = "modest"
    color = #ff8300
    visible = (info.comfort >= 0.2) && (info.comfort < 0.4)
  }
  ...
  PlannerTooltip
  {
    line_title = comfort      // show tooltip on Rule_UI_line whose title = comfort
    title = firm ground       // the line left text
    value = info.firm_ground  // the line right text, modifier-expression or use "" to evaluate as a string
    value_format = boolean    // returned value is Lib.HumanReadableBoolean(bool value) {return value ? "true" : "false";}
    color = #00ff00           // color of the value
  }
  ...
}
ShotgunNinja commented 7 years ago

@gotmachine That's very good ideas and I feel the need to speed-update you on what's going on, so we can design this together. I'll create a new issue with the proposed changes to the mod architecture and we'll discuss there.