Fractal Softworks Forum

Please login or register.

Login with username, password and session length
Advanced search  


Starsector 0.95.1a is out! (12/10/21); Blog post: Hyperspace Topography (10/12/22)

Show Posts

This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.

Messages - intrinsic_parity

Pages: [1] 2 3 ... 196
I agree with the Dev that the previous state of the game (where you would just bring 15 paragons and get 50% more DP than the enemy to trivialize the fight) was boring and needed to be fixed.

I do feel like a better direction would be to try and change the campaign layer of the game so that bringing tons of large ships is very difficult to achieve or undesirable (via logistics or access to ships, and the new skills and bonus XP already do this to some extent) rather than making those extra ships have no deployment benefit if you bring them along.

I feel like between skill thresholds, bonus XP, and logistics/ship access, it should be possible to make the 'bring 15 paragons to get +50% DP' strategy unviable, while still using the old system of initial DP ratio being decided by the ships in the fleet. Maybe considering both officers and ships is best though.

I think equal deployment points for both sides is a bad idea. It's way too much of an advantage for the smaller fleet, which is frequently the players because of the new skill system.

Suggestions / Re: AI Frigates happily blocking friendly fire
« on: November 26, 2022, 05:48:32 PM »
I would guess it happens more in real combat than in a 1v1. When 'battle lines' form, there's not really any way for frigates to get into range in the middle of the battle line without getting in front of bigger ships with longer range, so unless they end up on the fringes/flanks of the fight, they are mostly just getting in the way.

I know there's one way to exactly calculate the EV, which is to treat the armor as a function of all of the shots up until this point. The problem is that you end up with a nested loop for every shot up until that point which is completely intractable computationally.

edit: I will also say, that I think it does satisfy the markov property, so I think you could categorize it as some sort of 'discrete time continuous state markov process'. In that case you should only have to consider the current state/shot. That's probably the right direction to search if you really want to figure it out, but I have a feeling the non-linearity will make it a bit to niche/specific for you to find to many results.

It's good that we started doing latex. What you wrote is super crisp to read and makes it quite clear what exactly is going on.

Regardless of any pooling or distribution, the error should still be due to using hit strength based on expected value of armor, instead of expected value of hit strength, no? Ie. E((h)/(h+armor)) vs h/(h+E(armor)). All the other operations are linear - or almost always linear at least. The upside is this should be small like you found out yourself in the monte carlo you did earlier. So this is looking very good. Unless I'm missing another source of error?
I think the max() operations are non-linear too.

Also, I realized I forgot summations in the hull equation. And while I was at it, I moved some stuff out of the ArmorDamage equation so that it is not a function of the armor cell, and all that i,j stuff is handled in the armor and hull equations.

There will always be a tradeoff between readability and performance I suppose. I guess for now we can leave things in the most readable state until we get to a point where performance improvements might be worthwhile.

Also, CapnHector. The weights for pooling should be 1 or 1/2 depending on inner and outer cells. The initial values of the armor cells are 1/15th of the total armor, so they start on the 1/15th scale. The adjusted shot damage gets 1/15 and 1/30 multipliers when it is distributed amongst the cells so that the damage is on the same 1/15th scale as the cell armor values.

Here are the equations explicitly written out:

Edit: looking at this closely, I'm realizing I could implement it with a single 'indexes' array instead of two separate 'inner' and 'outer' index arrays. That would theoretically save some time because I avoid some element wise additions and also only allocate one array, however, that array would be numerical instead of boolean, and I would be doing the pooling operations (multiplications and summations) over the entire array element-wise instead of the current implementation where I use logical indexing to only do the math on the non-zero multipliers. I'm sort of curious which would be faster.

Updated code to eliminate some temporary variables. Now I have a big gross mess of a line of code though lol. Oh well, I'll consider trying to improve the approach to getting the correct armor cell indexes later if necessary. For now, this works.
def armorUpdate(dmg, location, armor_grid, minimum_armor):
    # dmg is a vector with elements:
    # [shield_damage, armor_damage, hull_damage, shield_hit_strength, armor_hit_strength, hull_hit_strength]
    # location is a tuple of the coordinates in the armor grid where the shot lands
    # armor_grid is an array with each element corresponding to an armor cell
    # minimum armor is the smallest armor value the pooled armor is allowed to take. Typically .05*max_armor

    inner_cells, outer_cells = getArmorIndexes(location, armor_grid)

    damage_armor = (1 / 15 * inner_cells + 1 / 30 * outer_cells) * \
        np.maximum(dmg[4] / (dmg[4] + np.maximum(np.sum(armor_grid[inner_cells]) + 1 / 2 * np.sum(armor_grid[outer_cells]), minimum_armor)), .15) * dmg[1]

    damage_hull = np.sum(np.maximum(damage_armor - armor_grid, 0) * dmg[2] / dmg[1])

    return damage_armor, damage_hull

E(f(x))=sum(f(X)p(X)) for a discrete random variable undoubtedly. The main thing is what is f(x). In terms of the total damage dealt to the armor matrix around x by the shot hitting x it should be exactly correct to say that for that damage it must be d*h/(h+sum of armor around cell x)). You get to the equations I was doing by trying to figure the value of the damage distribution cell by cell around x.
f(x) is just the calculations to determine armor damage given the shot location x. Exactly what you would do if there was no probabilistic aspect. If you compute the deterministic damage done for every possible shot location, multiply the damage by the probability of each shot location, and sum, you should get the expected value.

Conversely, to go back from the latex matrix thing to this, you must pool d*D_ij*h/(h+armor value of cell)). Now if it should happen that the pooling process is just that you reverse the D_ij distribution and sum the armor value then the two formulations are actually exactly equivalent.
I think this is where you're going wrong. A single armor cell value should not appear in the denominator of that fraction by itself, it has to be the pooled value. There's no way for sum(d*h/(h+armor value of cell))) to be equivalent to d*h/(h+sum(armor value of cell))) because the expression is non-linear.

If you wanted to think about it in terms of that D matrix, you're gonna have a different armor damage reductions for each probability pk because the pooled armor value is different for each possible shot location. So I don't think you can decompose it that way (separate the probabilities from the armor calculations like that) and still be correct. To put it another way, each shot location will  have an associated p*(adjusted damage) and the damage to a specific cell would be a weighted sum of those p*dmg terms over all the relevant shot locations. The best you could do to separate the probabilities would be to take the dot product of the probability vector with the adjusted damage, but that's already what I'm doing more or less. I just calculate all the adjusted damages first and then take the dot product at the end (I actually do a loop but looping a sum of products is equivalent to a dot product).

I'd still feel safer if you give it a whirl with a normal distribution, but having gone through this mental calculation I'm actually going to switch to supporting your version of the code. Just integrate it as the armor damage calculation into the Pythonized version of the R code I posted earlier, and if it produces the same results we're done here.
I would expect the results to not exactly match what you did because I don't think the two approaches are equivalent. Should definitely test and verify as much as possible though, and I'm happy to use all the probability distribution code that exists. I tried to make it modular so it could slot into other stuff easily.

Yeah, I knew there were a bunch of unnecessary temporary variables (mentioned it in an edit lol). I guess I could probably use comments to give the same level of readability, but it just feels nice.

In terms of pre-initializing the arrays outside of the functions, they would have to get passed down a significant function call stack because initializing them in the armor damage function doesn't do anything, and that get's called expected armor function too, so possibly 3-4 functions. Also, kinda difficult to do without having the rest of the higher level code yet. Another option is to try and generate a list of indices instead of a full boolean array for the inner/outer cell arrays which at least would be a much smaller temporary variable. I might try that later.

Also, all those weird idiosyncrasies are why I like to just use Julia these days lmao.

The distribution shouldn't matter that much because expected value doesn't care! E[f(x)] = sum(f(X)*p(X)) for X in x. f(X) doesn't care what the probability distribution is, it's all handled by p(X). All I've done is implement that E[f(x)] expression. The p(X) stuff can all be done independently. If you wanted to calculate variance, you would have a lot more trouble though.

Liral, did you see the python code I posted a little earlier? Pretty sure that is correct unless you can find some errors. It is based on code that matched other results previously.

CapnHector, it looks like your armor calculation for an armor cell only use the value of that same armor cell. That sort of defeats the purpose of the armor gird. The whole idea is that adjacent cells are contributing armor? I don't really see the point in using something that is 'weird and possibly wrong in some way' when there is code that (I think) is correct?

You definitely can't not do armor pooling. If you aren't doing it, something is wrong. The pooled armor goes through the non-linear armor damage multiplier, so you have to do it, I'm pretty sure.

I think your Xij expression is wrong. Or more precisely, not equivalent to what happens in the game.

Not gonna lie, your notation is very terse and I haven't felt like figuring it out in detail until now lamo.

Looking at it more closely, I find it really odd that D matrix, including probabilities, is going into the armor damage reduction. I don't think those probabilities should be appearing in the h/(h + a) expression. Probability should not be passed through the non-linear function. They just multiply the results.

Also, I don't really understand why h is being multiplied by the D array either? only armor should be multiplied by it.

The correct armor damage multiplier considering pooling should be something like hit_strength/(hit_strength + sum(wij*aij)) where wij are the associated weights for inner and outer cells.

Overall, I think the main difference in our approaches, is that you are trying to loop through the armor cells and calculate each one individually, and then handle the different possibilities of shot locations inside those expressions. What I am doing is looping through the shot locations with outer loop and then accounting for all the cells affected by that shot location in an inner loop (which is actually vectorized). My approach is very convenient because the armor pooling calculations and damage distribution are a function of the shot location, so it's very easy to code.

Yeah, the location_probabilities can come from whatever distribution you want. As long as you have a list of all the possible shot locations and the associated probabilities, everything else is pretty straight forward.

I will say that this way of calculating EV is not perfectly correct for a sequence of shots. I've verified that it's perfectly accurate for one shot. At least, I verified that the expected armor damage of a single shot is accurate to the average damage of one shot over 10k monte carlo trials. There was on the order of 1e-12 error, so basically to within numerical error. I haven't bothered to cook up a test case where hull damage comes into play in one single shot.

But for a sequence of shots, there is decent amount of error due to the fact that you are essentially using the EV of the armor grid from the previous calculation/shot to compute the Ev of the next armor values, which is not strictly correct. I actually went on about this a while back when we were initially discussing distributions. There is a somewhat significant amount of error introduced due to this. In a quick test case with 10000 hull and 1000 armor vs 400 energy damage, the error between the expected hull values and the average hull from 10k monte carlo runs maxed out at a few hundred hull points. So on the order of 5% error. The number of shots to kill though was fairly close and stable.

Edit: forgot to mention this is all for a pretty wide uniform distribution of shots because I am lazy and just wanted to test things. Not sure how different distributions will change things. A very quick test found that the magnitude of the error seemed to decrease approximately proportionally with the width of the uniform distribution, so as the shots get tighter, there is less error.

Some fun plots to highlight this:

You can see in the first figure that the dark black line (the EV) is somewhat off from the average of the red dashed lines (the MC trials). In the second figure, I plotted the error between the EV and the average of the MC trials.

I already verified that my MATLAB code matched the TTKs from vanshilars excel speed sheet way back when we started all of this (in the other thread when we discussing movement based inaccuracy and distributions). I also verified that the EVs from my MATLAB code were reasonably close to the averaged damage values from large numbers of deterministic trials. All I did today was port the code into python and verify that it produces the same results as the MATLAB code.

I think, you're making this way harder than it has to be. I really don't understand what's wrong with looping over shot locations. It works fine.

I ported my code into python, and verified it produces the same results as my MATLAB code. It probably could be optimized but it's reasonably fast. A quick test found that calculating the expected damage of one shot took ~.00064 seconds (averaged over 10000 trials) on my laptop (2.3 GHz i9).

import numpy as np

def getArmorIndexes(location, armor_grid):

    inner_cells = np.full(armor_grid.shape, False)
    outer_cells = np.full(armor_grid.shape, False)

    if any(np.less_equal(location, 1)) | location[0] > armor_grid.shape[0] - 3 | location[1] > armor_grid.shape[1] - 3:
        raise Exception("hit location must not be on the edge of the grid")

    inner_cells[location[0] - 1: location[0] + 2, location[1] - 1: location[1] + 2] = True

    outer_cells[location[0] - 2, location[1] - 1: location[1] + 2] = True
    outer_cells[location[0] + 2, location[1] - 1: location[1] + 2] = True
    outer_cells[location[0] - 1: location[0] + 2, location[1] - 2] = True
    outer_cells[location[0] - 1: location[0] + 2, location[1] + 2] = True
    return inner_cells, outer_cells

def armorUpdate(dmg, location, armor_grid, minimum_armor):
    # dmg is a vector with elements:
    # [shield_damage, armor_damage, hull_damage, shield_hit_strength, armor_hit_strength, hull_hit_strength]
    # location is a tuple of the coordinates in the armor grid where the shot lands
    # armor_grid is an array with each element corresponding to an armor cell
    # minimum armor is the smallest armor value the pooled armor is allowed to take. Typically .05*max_armor

    inner_cells, outer_cells = getArmorIndexes(location, armor_grid)

    armor_pooled = np.maximum(np.sum(armor_grid[inner_cells]) + 1 / 2 * np.sum(armor_grid[outer_cells]), minimum_armor)

    armor_damage_reduction = np.maximum(dmg[4] / (dmg[4] + armor_pooled), .15)

    damage_armor = (1 / 15 * inner_cells + 1 / 30 * outer_cells) * armor_damage_reduction * dmg[1]

    damage_hull = np.maximum(damage_armor - armor_grid, 0) * dmg[2] / dmg[1]

    damage_hull_total = np.sum(damage_hull)

    return damage_armor, damage_hull_total

def expectedArmorDamage(locations, location_probabilities, dmg, armor_grid, minimum_armor):

    damage_armor_expected = np.zeros(armor_grid.shape)
    damage_hull_expected = 0

    for i, location in enumerate(locations):
        damage_armor, damage_hull = armorUpdate(dmg, location, armor_grid, minimum_armor)
        damage_armor_expected += damage_armor * location_probabilities[i]
        damage_hull_expected += damage_hull * location_probabilities[i]

    return damage_armor_expected, damage_hull_expected

Lmao, as soon as I looked at my code, I noticed optimizations I could do. For instance, I pass in the entire armor_grid array to the getArmorIndexes function when I only need to pass in the size of the array. There's also quite a few unnecessary intermediate variables, but I kinda like the improved readability.

Also, just to summarize what the code does. The first function (getArmorIndexes) takes shot location coordinates and the armor grid (should just be the size of the armor grid) and returns boolean arrays of the same size as the armor grid with all the inner cells or outer cells set to 'true' and all others set to false. That's super convenient for doing calculations.

The second function (armorUpdate) takes the initial armor grid, the damage of the shot, the location the shot lands, and the minimum armor value, and returns the damage dealt to each armor cells as well as the total hull damage dealt.

The last function expectedArmorDamage takes all the arguments of armorUpdate, except the shot location is replaced by the set of possible shot locations and the associate probabilities. It returns the expected damage to each armor cell and the expected hull damage.

You have a set of possible events (locations the shot can land) along with the damage that occurs (over multiple cells) if the shot lands there (a function of the shot location). The expected value of a function of a discrete random variable is just  E[f(x)] = sum(f(x) * p(x)) over all values of x. So in this case x is the shot location, and if you consider the armor cell the shot landed on as the random variable, then it is a discrete random variable. Then f(x) is just the normal damage calculations. The hard part is figuring out the probability distribution of that random variable p(x) based on the underlying distribution of possible shot angles, but I think we already did that.

Then on a high level the code should be:

loop over possible shot locations

calculate damage to the entire armor array and hull given that the shot landed in that particular location (damage|location) and store the values

end loop

expected_damage = sum(damage|location*p(location)) (again both hull and armor cell values)

I guess you could do the probability multiplication inside the loop and sum as you go to get the total expected damage, and then subtract the total expected damage off of the armor values and hull value at the end of the loop. It's very important to not subtract the armor damage from the armor values mid loop though, because you need to use the pre-shot armor values for the entire loop.

Edit, maybe the confusion is that inside that f(x) function, there is another loop through armor cells? That is the loop that should not contain the (d/(d+a)) expression. The outer loop (over shot locations, which is also a loop over armor cells) obviously has to contain all the damage calculations. Maybe you were trying to do it all in a single loop? I'm not sure if that is possible.

Pages: [1] 2 3 ... 196