Fractal Softworks Forum

Please login or register.

Login with username, password and session length
Pages: 1 ... 20 21 [22] 23 24 ... 32

Author Topic: Optimizing the Conquest: a Mathematical Model of Space Combat  (Read 25270 times)

Liral

  • Admiral
  • *****
  • Posts: 718
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #315 on: December 22, 2022, 11:09:05 AM »

I have read your advice, thought, poked, and rearranged the code until it produces almost the same output as yours does.  I still get some negative probabilities, but they are usually extremely small.  If this approach doesn't work, then let's go function by function.

Code
Code
"""
Calculate the optimum angle to place the enemy ship relative to ours.


Assume
- Angle exactly to the right is 0 degrees
- Possible angles are integers from -179 to 180
- Our ship is:
  - heading towards +90 degrees
  - pointlike.
- The enemy ship is:
  - at a constant range
  - a straight line of hittable armor cells
  - oriented tangentially to a circle defined by our ship's position
    and range.
- The ships are so far apart that the arc across that circle, from one
  point on the enemy ship to the other, approximates the secant between
  those points.


Weapon Arcs
A weapon cannot exceed its maximum turn angle and has constant spread;
therefore, the maximum mean points of the probability distribution of a
weapon are

{ maximum turn angle - spread / 2, minimum turn angle + spread/2 }

A weapon tracking a target tries to align the mean of the probability
distribution with the target. Therefore we can find the signed angle
of the target from the mean of the distribution by computing the
following in this order:

1. The mean point, that is, max mean, when target is above max mean
2. target, when target is between max mean
3. min mean, min mean, when target is below min mean.
4. Minimum and maximum means for the weapon and find the median of
its hit distribution ("transform hit coord" for coordinates of mean
of modified dist in terms of the original distribution)


Arcs vs Widths
Referring to the width of the enemy ship in pixels but to the placement
and tracking ability of turrets as angles is convenient. Therefore, we
must convert between the two. Note that we assume we can use the arc
to describe the enemy ship.


Methods
"""
import math
from statistics import NormalDist


def probability_hit_before_bound(
        x: float,
        standard_deviation: float,
        uniform_distribution_radius: float) -> float:
    """
    Return the probability that the hit coordinate is less than x.
   
    This probability equals the integral, from -inf to x, of the hit
    probability distribution, which is
   
    - normal if the parameter of the uniform distribution is 0
    - uniform if the standard deviation of the normal distribution is 0
    - trivial if both are 0, the hit distribution is trivial: the CDF
      equals a step function going from 0 to 1 where x = 0.
    - a normal distribution convolved with a uniform one if both exceed 0
   
    x - real number
    standard_deviation - standard deviation of normal distribution
    uniform_distribution_radius - radius of uniform distribution
    """
    def g(x: float, normal_distribution: object) -> float:
        """
        Return the cumulative distribution function of the convolved
        normal distribution.
       
        x - real number
        normal_distribution - a Python standard library NormalDist
        """
        return x * normal_distribution.cdf(x) + normal_distribution.pdf(x)

    def f(x: float,
          normal_distribution: float,
          uniform_distribution_parameter: float) -> float:
        a = x / normal_distribution.stdev
        b = uniform_distribution_parameter / normal_distribution.stdev
        return (normal_distribution.stdev
                / (2 * uniform_distribution_parameter)
                * (g(a + b, normal_distribution)
                   - g(a - b, normal_distribution)))
               
    if standard_deviation > 0:
        normal_distribution = NormalDist(0, standard_deviation)
        return (f(x, normal_distribution, uniform_distribution_radius)
                if uniform_distribution_radius > 0
                else normal_distribution.cdf(x))
    if b > 0: return (f(x, NormalDist(0, standard_deviation),
                        uniform_distribution_radius) if a > 0
                      else max(0, min(1, b / 2 + x)))
    return 0 if x < 0 else 1


def arc_measure(arc_length: float, radius: float) -> float:
    """
    Return the degree angle of an arc of some radius.
   
    arc -
    radius -
    """
    return arc_length / radius / 180 * math.pi


def arc_length(degrees: float, radius: float) -> float:
    """
    Return the arc of a degree angle of some radius.
   
    degrees -
    radius -
    """
    return degrees * math.pi / 180 * radius


def transformed_hit_coord(
        angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    """
    angle -
    minimum_mean -
    maximum_mean -
    """
    return max(minimum_mean, min(maximum_mean, angle))


def transformed_angle(
        angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    """
    Return the angle between the mean and target.
   
    We refer to this as "transforming" since we are moving from
    expressing the angle wrt. ship to wrt. weapon.
   
    angle -
    minimum_mean -
    maximum_mean -
    """
    return angle - transformed_hit_coord(angle, minimum_mean, maximum_mean)


def total_hit_probability(
        weapon_facing: float,
        spread_distance: float,
        error_distance: float,
        target_facing: float,
        target_radius: float,
        distance: float) -> float:
    """
    Return the total probability of hitting the target per second.
   
    This probability equals the integral, from ship lower angle to
    ship greater angle, of the probability distribution. By the
    fundamental theorem of calculus, this integral equals
   
    CDF(upper edge) - CDF(lower edge).
   
    weapon_facing -
    spread_distance -
    error_distance -
    target_facing - target ship orientation
    target_radius -
    distance -
    """
    #angle of the ship's upper bound, in coordinates of the
    #distribution mean; note that this is weapon specific
   


def weapon_adjustment_distance(
        weapon_facing: float,
        minimum_mean: float,
        maximum_mean: float,
        distance: float) -> float:
    """
   
    weapon_facing -
    minimum_mean -
    maximum_mean -
    distance -
    """
    #segment from the old angle plus the new angle plus the weapon's
    #angle.
    angle_difference = transformed_angle(weapon_facing, minimum_mean,
                                         maximum_mean)
    #location of the weapon's mean as it tries to track the target
    #from transformed_angle
    return angle_difference / 180 * math.pi * distance


def hit_distribution(
        bounds: tuple,
        standard_deviation: float,
        spread: float) -> tuple:
    """
    Hit distribution function.
   
    bounds -
    standard_deviation -
    spread -
    """
    if standard_deviation == 0:
        if spread == 0:
            #all shots hit 1 cell, even if the ship has evenly many
            #cells, to prevent such ships from appearing tougher
            return 0, + tuple(1 if bounds[i-1] < 0 <= bound else 0
                              for i, bound in enumerate(bounds[1:]))
        double_spread = 2 * spread
        return (min(1, max(0, (bounds[0] + spread)) / double_spread),
                + tuple(min(1, max(0, bounds[j] + spread) / double_spread)
                        - min(1, max(0, bounds[j-1] + spread) / double_spread)
                        for i, bound in enumerate(bounds[1:]))
                + (1 - min(1, max(0, bounds[-1] + spread) / double_spread)),)
    elif spread == 0:
        cdf = NormalDist(0, standard_deviation).cdf
        return (cdf(bounds[0]),
                + tuple(cdf(bound) - cdf(bounds[i-1]) for i, bound in
                        enumerate(bounds[1:]))
                + (1 - cdf(bounds[-1])),)
    numbers = standard_deviation, spread
    return (probability_hit_within_bound(bounds[0], *numbers),
            + tuple(probability_hit_within_bound(bound, *numbers)
                    - probability_hit_within_bound(bounds[i-1], *numbers)
                    for i, bound in enumerate(bounds[1:]))
            + (1 - probability_hit_within_bound(bounds[-1], *numbers)),)
   

def hit_distribution_at_optimum_angle(
        spread_distance: float,
        weapon_facing: float,
        minimum_mean: float,
        maximum_mean: float,
        upper_bounds: tuple) -> tuple:
    """
   
    spread_distance -
    weapon_facing -
    minimum_mean -
    maximum_mean -
    upper_bounds -
    """
    adjustment_distance = weapon_adjustment_distance(weapon_facing,
                                                     minimum_mean,
                                                     maximum_mean,
                                                     distance)
    adjusted_upper_bounds = tuple(upper_bound + adjustment_distance for
                                  upper_bound in upper_bounds)
    return hit_distribution(adjusted_upper_bounds, error, spread_distance)


def main():
    #Test against a Dominator. We wish to integrate this code
    #with the other modules eventually, so we will use the full ship
    #definition, even though only the width is needed.
    distance = 1000
    cell_count = 12
    target_radius = 220#(14000, 500, 10000, 1500, 220, 12, 440, 1.0, 200)
    target_arc_measure = arc_measure(target_radius, distance)
   
    #two weapons slightly angled from each other, arcs overlapping
    #in the middle, and rear-mounted point defense weapons to test
    #wraparound, extreme angles
    weapons = (
        {"damage" : 100, "facing" : -10, "arc" : 20, "spread" : 5},
        {"damage" : 100, "facing" : 10, "arc" : 20, "spread" : 5},
        {"damage" : 30, "facing" : -160, "arc" : 20, "spread" : 0},
        {"damage" : 30, "facing" : 180, "arc" : 20, "spread" : 0},
        {"damage" : 30, "facing" : 160, "arc" : 20, "spread" : 0},
        {"damage" : 120, "facing" : 90, "arc" : 0, "spread" : 5}
    )
    for weapon in weapons:
        #we have defined spread in degrees, so we must convert it to
        #pixels to be consistent
        half_difference = (weapon["arc"] - weapon["spread"]) / 2
        minimum_mean = weapon["facing"] - half_difference
        maximum_mean = weapon["facing"] + half_difference
        weapon["minimum mean"] = min(minimum_mean, maximum_mean)
        weapon["maximum mean"] = max(minimum_mean, maximum_mean)
        weapon["spread distance"] = arc_length(weapon["spread"], distance)
        print("spread angle", weapon["spread"])
        print("spread distance", round(weapon["spread distance"]))
        print("minimum mean", weapon["minimum mean"])
        print("maximum mean", weapon["maximum mean"])
        print()
    #same standard deviation as in other modules
    error_standard_deviation = 0.05
    error_distance = error_standard_deviation * distance
   
    #Map signed to unsigned angles
   
    #Define a vector from -360 to 360, encompassing all possible
    #firing angles because a weapon can face from -180 to 180
    #degrees off the ship's facing and can track at most 360
    #degrees total.
    damage_per_second_totals_expected = []
    for target_facing in range(-359,361):
        damage_per_second_total_expected = 0
        for weapon in weapons:
            weapon_facing = transformed_angle(target_facing,
                                              weapon["minimum mean"],
                                              weapon["maximum mean"])
            weapon_arc = arc_length(weapon_facing, distance)
            lower_bound = arc_length(weapon_arc - target_radius, distance)
            upper_bound = arc_length(weapon_arc + target_radius, distance)
            probability = (
                probability_hit_before_bound(upper_bound, error_distance,
                                             weapon["spread distance"])
                - probability_hit_before_bound(lower_bound, error_distance,
                                               weapon["spread distance"]))
            damage_per_second_total_expected += weapon["damage"] * probability
        damage_per_second_totals_expected.append(
            damage_per_second_total_expected)
   
    print("damage per second totals expected")
    for damage_per_second in damage_per_second_totals_expected:
        print(damage_per_second)
    print()

    #-180 corresponds to +180
    #-181 corresponds to +179 etc, so 360 + index for indices 1:180
    for i in range(180):
        damage_per_second_totals_expected[i+360] += (
            damage_per_second_totals_expected[i])
   
    #and +360 corresponds to 0, +359 to -1 etc.
    #so index - 360 for indices 540:720
    for i in range(540, 720):
        damage_per_second_totals_expected[i-360] += (
            damage_per_second_totals_expected[i])
   
    #finally, to get angles -179 to 180, select indices 181 to 540
    #of the new vector.
    damage_per_second_totals_expected = (
        damage_per_second_totals_expected[181:540])
   
    #note that vector indices no longer correspond to angles, rather
    #vector index 1 corresponds to -179. to get a correct plot add
    xaxis = range(-179,180)
   
    #the optimum angle is the midmost maximum
    #optimumangle = xaxis[which(round(dps_at_angles,3) == round(max(dps_at_angles),3))[ceiling(length(which(round(dps_at_angles,3) == round(max(dps_at_angles),3)))/2)]]
   
    #plot the result
    #plot(dps_at_angles,x=xaxis)
    #abline(v=optimumangle)
   
    #the usual - calculate ship cell upper bound angles
    target_angle = target_radius / (2 * math.pi * distance)
    cell_angle = target_angle / cell_count
    angle_ranges = [-target_angle/2]
    for _ in range(cell_count):
        angle_ranges.append(angle_ranges[-1] + cell_angle)
    #convert to pixels
    upper_bound_distances = [angle_range * 2 * math.pi * distance
                             for angle_range in angle_ranges]
    print("upper bound distances")
    for upper_bound_distance in upper_bound_distances:
        print(round(upper_bound_distance, 3))
   
    #print results
    #for weapon in weapons: print(hit_distribution_at_optimum_angle(weapon))
main()
[close]
Result
spread angle 5
spread distance 87
minimum mean -17.5
maximum mean -2.5

spread angle 5
spread distance 87
minimum mean 2.5
maximum mean 17.5

spread angle 0
spread distance 0
minimum mean -170.0
maximum mean -150.0

spread angle 0
spread distance 0
minimum mean 170.0
maximum mean 190.0

spread angle 0
spread distance 0
minimum mean 150.0
maximum mean 170.0

spread angle 5
spread distance 87
minimum mean 87.5
maximum mean 92.5

damage per second totals expected
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
1.68e-320
1.7984e-318
1.8769455e-316
1.9301718415e-314
1.955645083155e-312
1.95224214685523e-310
1.920113422803395e-308
1.860671099208667e-306
1.7764862222750104e-304
1.6711042820479386e-302
1.5487977556896792e-300
1.4142796501932532e-298
1.2724047162089782e-296
1.127884468491377e-294
9.850387215885852e-293
8.476006901797427e-291
7.1858572296518e-289
6.002264394761905e-287
4.939703538991761e-285
4.005307322938467e-283
3.1997789025996644e-281
2.518565400302636e-279
1.9531500122516618e-277
1.4923374696689427e-275
1.1234339129504213e-273
8.33253272650974e-272
6.089133649450087e-270
4.3841250430303616e-268
3.10999201232515e-266
2.1736243799289552e-264
1.4967820560327995e-262
1.0155035689732907e-260
6.788176413625958e-259
4.4706794553624815e-257
2.9009661075659753e-255
1.8546435457670188e-253
1.1682260581186694e-251
7.250065477445754e-250
4.43307965009108e-248
2.670654262992146e-246
1.585178942136948e-244
9.27016305392818e-243
5.34127410964571e-241
3.032150368090636e-239
1.695918349341388e-237
9.345605393201283e-236
5.074090341537414e-234
2.7142957434541383e-232
1.4305541742259278e-230
7.428473582254883e-229
3.800519332326511e-227
1.915730238377673e-225
9.51423259658171e-224
4.655444274477199e-222
2.2443801225658372e-220
1.066054880729841e-218
4.988965884376434e-217
2.300325433849941e-215
1.0449987171963941e-213
4.6772427956596835e-212
2.0625842890201398e-210
8.961507662422936e-209
3.8361712896866514e-207
1.6179399029849109e-205
6.723171905662245e-204
2.7525386697391285e-202
1.1102990629375278e-200
4.412592232877422e-199
1.7278054693569646e-197
6.665658563911062e-196
2.5336020402048966e-194
9.488133733667092e-193
3.500823371401459e-191
1.272642873976318e-189
4.558162364061525e-188
1.6084955501795387e-186
5.5923804585751036e-185
1.915668102949035e-183
6.465326751503901e-182
2.1498456455676497e-180
7.043209530812415e-179
2.2734237998172184e-177
7.229971682795947e-176
2.265369901001839e-174
6.993392848328534e-173
2.127075605689095e-171
6.374176189848741e-170
1.8819638058238247e-168
5.4745000792802185e-167
1.569002342515636e-165
4.430457470071053e-164
1.2325919685631307e-162
3.378592419604738e-161
9.124265341093612e-160
2.427758968553293e-158
6.364418195100678e-157
1.6438313282919252e-155
4.183127685389295e-154
1.0487940934579873e-152
2.5907440342315568e-151
6.305272496685117e-150
1.5119179035212525e-148
3.5718850183814047e-147
8.31402938664258e-146
1.9066473642824586e-144
4.307981681755567e-143
9.590072196229647e-142
2.1033636878590383e-140
4.545181614723387e-139
9.676813924241621e-138
2.0298209130884376e-136
4.194953411762568e-135
8.541625954548258e-134
1.713554195252012e-132
3.3868731097185966e-131
6.59543873026592e-130
1.2654122443501598e-128
2.3920147102548325e-127
4.4549109063291234e-126
8.174429510981928e-125
1.47781144295818e-123
2.632228730546359e-122
4.61924649653465e-121
7.986591045979641e-120
1.360487113727282e-118
2.283336789525175e-117
3.77561713775355e-116
6.151035469470356e-115
9.873035420896787e-114
1.561331820562775e-112
2.4326613071321607e-111
3.7343057338882287e-110
5.64780569808393e-109
8.415717271117996e-108
1.2355032719284477e-106
1.7870548965167943e-105
2.546671766038838e-104
3.575600515998586e-103
4.946133077640703e-102
6.740983094567758e-101
9.051512565843067e-100
1.1974555490883665e-98
1.5607660284447277e-97
2.0042698946351503e-96
2.535796701556749e-95
3.1609113959285636e-94
3.881947871581435e-93
4.6970630249628994e-92
5.599409220640267e-91
6.576532617242853e-90
7.610102609687114e-89
8.676061489021745e-88
9.745252849369077e-87
1.078454402117796e-85
1.1758405912066996e-84
1.2630859096932532e-83
1.3367645063359865e-82
1.3938443607755822e-81
1.4318937861428266e-80
1.449253145758513e-79
1.4451549000108416e-78
1.4197798872680754e-77
0.2418961213169546
29.996587013377187
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
30.0
29.996587013377187
0.2418961213169546
1.4991116428805962e-37
7.09593774142861e-37
3.309030942062574e-36
1.520219541184936e-35
6.880586516038982e-35
3.068014929709199e-34
1.3477256847126815e-33
5.832526393685685e-33
2.486693210558521e-32
1.0444726745871608e-31
4.32195654392927e-31
1.7618591269494974e-30
7.075684138669341e-30
2.7994398197665187e-29
1.0911343058990557e-28
4.18975436260889e-28
1.5848970376041243e-27
5.906286522007786e-27
2.16834169296565e-26
7.842227284587854e-26
2.794141042904383e-25
9.80738160773965e-25
3.391191224571326e-24
1.1551662656770964e-23
3.876396847119168e-23
1.2814507805887118e-22
4.1731498818709e-22
1.3387908031147398e-21
4.2310329480534504e-21
1.317234940760227e-20
4.0398138371418646e-20
1.220502829527552e-19
3.632398862135388e-19
1.064935892665098e-18
3.075575522453953e-18
8.749838469250917e-18
2.4521215714963397e-17
-6.646468896442241e-13
-6.492908588293843e-13
-1.2790797227773436e-12
-3.1380860653137063e-12
-8.034184193826569e-12
-2.1299957117075116e-11
-5.388233288862089e-11
-1.336700889730223e-10
-3.259624929413722e-10
-7.848195108253327e-10
-1.862356501726464e-09
-4.351827655384214e-09
-1.0015928199182324e-08
-2.2707102724315948e-08
-5.0700930919721016e-08
-1.1149665874237174e-07
-2.414866988375981e-07
-5.151127320553465e-07
-1.0821353900156805e-06
-2.238836863705453e-06
-4.561590382423426e-06
-9.152788442715428e-06
-1.8085222176942212e-05
-3.518978185853949e-05
-6.742481889085898e-05
-0.0001272098556891641
-0.00023632284176821244
-0.0004322745506023445
-0.000778514009044628
-0.001380410671886001
-0.0024097166847313646
-0.004141124775956328
-0.007005547529764231
-0.011665724401468312
-0.019120486599228104
-0.030844123515256047
-0.04896630339437285
-0.07649527955091989
-0.11758195374529357
-0.17781408626199557
-0.2645180504707112
-0.38702997738115286
-0.5568795971602777
-0.7878102500739048
-1.0955403381800757
-1.4971591462483518
-2.010048780474864
-2.6502397621322813
-3.4301458905761857
-4.355687842831922
-5.422904705325763
-6.614263475125245
-7.894997887667614
-9.209923092577029
-10.481260212027562
-11.60804089637524
-12.467624502335086
-12.919733779190596
-12.813194617301495
-11.995262373211105
-10.3230594145218
-7.6762794608347855
-3.9699887833568037
0.833869524573667
6.717732508727897
13.603781944746341
21.354446613045873
29.778577471609246
38.64320746846566
47.69013005572814
56.655887531590544
65.29324260300382
73.39189226052191
80.79612616456768
87.41735366182569
93.23990969399314
98.319251352707
102.77249666064472
106.76213950987052
110.47459603963351
114.09589835978076
117.78726645219496
121.77820584947385
126.81297770576012
133.0045385429884
140.29214700205156
148.55559219162413
157.61971808021588
167.26390796625003
177.23577060956427
187.2677156429852
197.0946980785962
206.47121773156698
215.1857166763416
223.07082655884622
230.00843969924105
235.92924291402568
240.69226076383399
243.61219754098408
244.5840931308387
243.61219754098425
240.6922607638342
235.92924291402588
230.00843969924176
223.0708265588465
215.1857166763403
206.47121773156542
197.09469807859367
187.2677156429811
177.23577060955387
167.26390796622428
157.61971808015065
148.55559219146227
140.29214700165983
133.00453854204636
126.81297770352502
121.77820584425163
117.78726644017503
114.09589833253311
110.47459597879198
106.76213937607314
102.77249637086088
98.31925073457111
93.23990839543097
87.41735097522005
80.79612069066059
73.39188127717568
65.29322090073721
56.65584530385812
47.69004914595627
38.64305481666238
29.778293884265516
21.35392788374684
13.602847728326196
6.7160760168655775
0.8309778667862666
-3.974958127862838
-7.684686105851271
-10.337058256552226
-12.018206896287737
-12.850207431725574
-12.978493053478145
-12.55941821966023
-11.749137942305966
-10.69463442893678
-9.527339279231082
-8.35942287717691
-7.282497289450273
-6.368234777675518
-5.6702553388715335
-5.226584214272908
-5.062014711353521
-5.189817765730215
-5.612399998519431
-6.320709257712384
-7.292404238677017
-8.48902642279672
-9.85262079756485
-11.302426919528648
-12.732381817615682
-14.010218214972888
-14.978885408061565
-15.460853121094285
-15.26558061834486
-14.200061117540436
-12.081920835168315
-8.75411591101726
-4.099893850473491
1.9435637851594059
9.374466095844483
18.120198143387082
28.03667861032978
38.91382562834824
50.48696260457547
62.4532575615877
74.49162383673196
86.28401986402537
97.53585238002086
107.99325570494959
117.45538988670901
125.7805268118845
132.88549123664862
138.73888256899136
143.34930379517664
146.75045582780177
148.98533311979887
150.0918300961071
150.22959974071833
150.22959974320662
150.22959974428304
150.22959974474549
150.22959974493602
150.0918301060705
148.9853331424852
146.75045587849493
143.34930390667196
138.7388828104758
132.8854917517599
125.78052789402058
117.45539212554628
107.99326026653976
97.5358615328099
86.28403794924472
74.49165902651232
62.453324986406685
50.487089814430284
38.91406195118882
28.037110884877702
18.120976657391658
9.375846506516229
1.9459735018422908
-4.0957527257009385
-8.747110363491242
-12.07025511076842
-14.18094063094336
-15.234736494832282
-15.411886817703513
-14.902390128513865
-13.892636261228466
-12.554567731357142
-11.03790886905787
-9.465590820184673
-7.93214682564197
-6.504593988601215
-5.2251689195315265
-4.115240852273905
-3.1797689852556488
-2.4117749492211082
-1.7964383236965809
-1.314567496040162
-0.945330072354551
-0.6682338143237221
-0.46442498951035915
-0.31741618665436455
-0.21337421690837743
-0.14109704593162853
0.15010240399374486
29.937827739089762
29.962987185579564
29.977055476925194
29.986001157969202
29.99159335498682
29.99503065549435
29.99710834221566
29.99834350813736
29.999065783582523
29.99948127070067
29.999716412654955
29.99984734819933
29.999919090225895
29.99995777226701
29.999978297737265
29.999989016650716
29.999994526084507
29.999997313389187
29.999998701431213
30.24189550317524
59.99658672358842
59.99999986619598
59.999999939151564
59.999999972745066
59.99999998797361
59.99999999477203
59.99999999775797
59.99999999904965
59.999999999604626
59.999999999833236
59.999999999929
59.99999999996809
59.99999999998371
59.99999999998957
59.99999999999154
59.999999999988276
59.99999999998697
59.99999999998697
59.99999999998697
59.99999999998697
59.99999999999023
59.999999999993484
59.999999999993484
59.999999999993484
59.99658701337067
30.24189612131044
29.999999999993484
29.999999999993484
29.999999999993484
29.999999999993484
29.999999999993484
29.999999999993484
29.999999999993484
29.999999999993484
29.999999999993484
29.999999999993484
29.999999999993484
29.999999999993484
29.999999999993484
29.999999999993484
29.999999999996746
30.0
30.0
30.0
29.996587013377187
0.2418961213169546
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
-3.916866830877552e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-7.820410985459603e-12
-3.90354415458205e-12
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
6.517009154549669e-12
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.9539925233402755e-11
2.6045832157706172e-11
2.6045832157706172e-11
2.6045832157706172e-11
2.6045832157706172e-11
2.6045832157706172e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11
1.3022916078853086e-11

upper bound distances
-110.0
-91.667
-73.333
-55.0
-36.667
-18.333
0.0
18.333
36.667
55.0
73.333
91.667
110.0
[close]
« Last Edit: December 22, 2022, 11:11:24 AM by Liral »
Logged

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #316 on: December 22, 2022, 07:55:50 PM »

Well, I think I found another bug. The bad news is I am not sure this would be sufficient to generate negative dps, so it may not be the only one. See how it goes. I highly recommend generating graphical output as it is not easy to assess smoothness from numbers. Anyway


def weapon_adjustment_distance(
        weapon_facing: float,
        minimum_mean: float,
        maximum_mean: float,
        distance: float) -> float:
    """
   
    weapon_facing -
    minimum_mean -
    maximum_mean -
    distance -
    """
    #segment from the old angle plus the new angle plus the weapon's
    #angle.
    angle_difference = transformed_angle(weapon_facing, minimum_mean,
                                         maximum_mean)
    #location of the weapon's mean as it tries to track the target
    #from transformed_angle
    return angle_difference / 180 * math.pi * distance


If this is meant to be the part that calculates the ship's distance from the distribution mean - which I think it is - then shouldn't the calculation be "arc corresponding to ship angle minus distribution mean"?

Compare to mine:

transform_hit_coord <- function(angle, weapon) return(max(weapon$minmean, min(weapon$maxmean,angle)))

#then calculate the angle between the mean and target by subtraction. We refer to this as "transforming"
#since we are moving from expressing the angle wrt. ship to wrt. weapon.
transformed_angle <- function(angle, weapon) return(angle-transform_hit_coord(angle,weapon))



(Mine is in angles, but note we are doing angle ie. target's angle relative to us minus angle of distribution relative to us)

Edit: to clarify, it seems like you are using the weapon's facing when you should be using two angles here. Ie transformed angle should be ship angle relative to us minus angle of weapon distribution mean relative to us, while, if I understand your code, you currently calculate weapon facing minus max(minmean,min(maxmean, weapon facing)) of weapon facing when calling the transformed angle...?
« Last Edit: December 22, 2022, 08:24:39 PM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 718
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #317 on: December 22, 2022, 08:35:46 PM »

I suspect you have read an old version of the code because the last one I have posted does it your way.  Regardless, here is a new version without the function weapon_adjustment_distance.  Graphical output closely corresponds to yours.

Code
Code
"""
Calculate the optimum angle to place the enemy ship relative to ours.


Assume
- Angle exactly to the right is 0 degrees
- Possible angles are integers from -179 to 180
- Our ship is:
  - heading towards +90 degrees
  - pointlike.
- The enemy ship is:
  - at a constant range
  - a straight line of hittable armor cells
  - oriented tangentially to a circle defined by our ship's position
    and range.
- The ships are so far apart that the arc across that circle, from one
  point on the enemy ship to the other, approximates the secant between
  those points.


Weapon Arcs
A weapon cannot exceed its maximum turn angle and has constant spread;
therefore, the maximum mean points of the probability distribution of a
weapon are

{ maximum turn angle - spread / 2, minimum turn angle + spread/2 }

A weapon tracking a target tries to align the mean of the probability
distribution with the target. Therefore we can find the signed angle
of the target from the mean of the distribution by computing the
following in this order:

1. The mean point, that is, max mean, when target is above max mean
2. target, when target is between max mean
3. min mean, min mean, when target is below min mean.
4. Minimum and maximum means for the weapon and find the median of
its hit distribution ("transform hit coord" for coordinates of mean
of modified dist in terms of the original distribution)


Arcs vs Widths
Referring to the width of the enemy ship in pixels but to the placement
and tracking ability of turrets as angles is convenient. Therefore, we
must convert between the two. Note that we assume we can use the arc
to describe the enemy ship.


Methods
"""
import math
from statistics import NormalDist


def probability_hit_before_bound(
        x: float,
        standard_deviation: float,
        uniform_distribution_radius: float) -> float:
    """
    Return the probability that the hit coordinate is less than x.
   
    This probability equals the integral, from -inf to x, of the hit
    probability distribution, which is
   
    - normal if the parameter of the uniform distribution is 0
    - uniform if the standard deviation of the normal distribution is 0
    - trivial if both are 0, the hit distribution is trivial: the CDF
      equals a step function going from 0 to 1 where x = 0.
    - a normal distribution convolved with a uniform one if both exceed 0
   
    x - real number
    standard_deviation - standard deviation of normal distribution
    uniform_distribution_radius - radius of uniform distribution
    """
    def g(x: float, normal_distribution: object) -> float:
        """
        Return the cumulative distribution function of the convolved
        normal distribution.
       
        x - real number
        normal_distribution - a Python standard library NormalDist
        """
        return x * normal_distribution.cdf(x) + normal_distribution.pdf(x)

    def f(x: float,
          normal_distribution: float,
          uniform_distribution_parameter: float) -> float:
        a = x / normal_distribution.stdev
        b = uniform_distribution_parameter / normal_distribution.stdev
        return (normal_distribution.stdev
                / (2 * uniform_distribution_parameter)
                * (g(a + b, normal_distribution)
                   - g(a - b, normal_distribution)))
               
    if standard_deviation > 0:
        normal_distribution = NormalDist(0, standard_deviation)
        return (f(x, normal_distribution, uniform_distribution_radius)
                if uniform_distribution_radius > 0
                else normal_distribution.cdf(x))
    if b > 0: return (f(x, NormalDist(0, standard_deviation),
                        uniform_distribution_radius) if a > 0
                      else max(0, min(1, b / 2 + x)))
    return 0 if x < 0 else 1


def arc_measure(arc_length: float, radius: float) -> float:
    """
    Return the degree angle of an arc of some radius.
   
    arc -
    radius -
    """
    return arc_length / radius / 180 * math.pi


def arc_length(degrees: float, radius: float) -> float:
    """
    Return the arc of a degree angle of some radius.
   
    degrees -
    radius -
    """
    return degrees * math.pi / 180 * radius


def transformed_hit_coord(
        angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    """
    angle -
    minimum_mean -
    maximum_mean -
    """
    return max(minimum_mean, min(maximum_mean, angle))


def transformed_angle(
        angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    """
    Return the angle between the mean and target.
   
    We refer to this as "transforming" since we are moving from
    expressing the angle wrt. ship to wrt. weapon.
   
    angle -
    minimum_mean -
    maximum_mean -
    """
    return angle - transformed_hit_coord(angle, minimum_mean, maximum_mean)


def hit_distribution(
        bounds: tuple,
        standard_deviation: float,
        spread: float) -> tuple:
    """
    Hit distribution function.
   
    bounds -
    standard_deviation -
    spread -
    """
    if standard_deviation == 0:
        if spread == 0:
            #all shots hit 1 cell, even if the ship has evenly many
            #cells, to prevent such ships from appearing tougher
            return 0, + tuple(1 if bounds[i-1] < 0 <= bound else 0
                              for i, bound in enumerate(bounds[1:]))
        double_spread = 2 * spread
        return (min(1, max(0, (bounds[0] + spread)) / double_spread),
                + tuple(min(1, max(0, bounds[j] + spread) / double_spread)
                        - min(1, max(0, bounds[j-1] + spread) / double_spread)
                        for i, bound in enumerate(bounds[1:]))
                + (1 - min(1, max(0, bounds[-1] + spread) / double_spread)),)
    elif spread == 0:
        cdf = NormalDist(0, standard_deviation).cdf
        return ((cdf(bounds[0]),)
                + tuple(cdf(bound) - cdf(bounds[i-1]) for i, bound in
                        enumerate(bounds[1:]))
                + ((1 - cdf(bounds[-1])),))
    numbers = standard_deviation, spread
    return ((probability_hit_before_bound(bounds[0], *numbers),)
            + tuple(probability_hit_before_bound(bound, *numbers)
                    - probability_hit_before_bound(bounds[i-1], *numbers)
                    for i, bound in enumerate(bounds[1:]))
            + ((1 - probability_hit_before_bound(bounds[-1], *numbers)),))
   

def hit_distribution_at_optimum_angle(
        weapon_facing: float,
        spread_distance: float,
        error_distance: float,
        minimum_mean: float,
        maximum_mean: float,
        distance: float,
        bounds: tuple) -> tuple:
    """
   
    spread_distance -
    weapon_facing -
    minimum_mean -
    maximum_mean -
    upper_bounds -
    """
    adjustment_angle = transformed_angle(weapon_facing, minimum_mean,
                                         maximum_mean)
    adjustment_distance = arc_length(adjustment_angle, distance)
    adjusted_bounds = tuple(bound + adjustment_distance for bound in bounds)
    return hit_distribution(adjusted_bounds, error_distance, spread_distance)


def main():
    #Test against a Dominator. We wish to integrate this code
    #with the other modules eventually, so we will use the full ship
    #definition, even though only the width is needed.
    distance = 1000
    cell_count = 12
    target_radius = 220#(14000, 500, 10000, 1500, 220, 12, 440, 1.0, 200)
    target_arc_measure = arc_measure(target_radius, distance)
   
    #two weapons slightly angled from each other, arcs overlapping
    #in the middle, and rear-mounted point defense weapons to test
    #wraparound, extreme angles
    weapons = (
        {"damage" : 100, "facing" : -10, "arc" : 20, "spread" : 5},
        {"damage" : 100, "facing" : 10, "arc" : 20, "spread" : 5},
        {"damage" : 30, "facing" : -160, "arc" : 20, "spread" : 0},
        {"damage" : 30, "facing" : 180, "arc" : 20, "spread" : 0},
        {"damage" : 30, "facing" : 160, "arc" : 20, "spread" : 0},
        {"damage" : 120, "facing" : 90, "arc" : 0, "spread" : 5}
    )
    for weapon in weapons:
        #we have defined spread in degrees, so we must convert it to
        #pixels to be consistent
        half_difference = (weapon["arc"] - weapon["spread"]) / 2
        minimum_mean = weapon["facing"] - half_difference
        maximum_mean = weapon["facing"] + half_difference
        weapon["minimum mean"] = min(minimum_mean, maximum_mean)
        weapon["maximum mean"] = max(minimum_mean, maximum_mean)
        weapon["spread distance"] = arc_length(weapon["spread"], distance)
        print("spread angle", weapon["spread"])
        print("spread distance", round(weapon["spread distance"]))
        print("minimum mean", weapon["minimum mean"])
        print("maximum mean", weapon["maximum mean"])
        print()
    #same standard deviation as in other modules
    error_standard_deviation = 0.05
    error_distance = error_standard_deviation * distance
   
    #Map signed to unsigned angles
   
    #Define a vector from -360 to 360, encompassing all possible
    #firing angles because a weapon can face from -180 to 180
    #degrees off the ship's facing and can track at most 360
    #degrees total.
    damage_per_second_totals_expected = []
    for target_facing in range(-359,361):
        damage_per_second_total_expected = 0
        for weapon in weapons:
            weapon_facing = transformed_angle(target_facing,
                                              weapon["minimum mean"],
                                              weapon["maximum mean"])
            weapon_arc = arc_length(weapon_facing, distance)
            lower_bound = arc_length(weapon_arc - target_radius, distance)
            upper_bound = arc_length(weapon_arc + target_radius, distance)
            probability = (
                probability_hit_before_bound(upper_bound, error_distance,
                                             weapon["spread distance"])
                - probability_hit_before_bound(lower_bound, error_distance,
                                               weapon["spread distance"]))
            damage_per_second_total_expected += weapon["damage"] * probability
        damage_per_second_totals_expected.append(
            damage_per_second_total_expected)

    #-180 corresponds to +180
    #-181 corresponds to +179 etc, so 360 + index for indices 1:180
    for i in range(180):
        damage_per_second_totals_expected[i+360] += (
            damage_per_second_totals_expected[i])
   
    #and +360 corresponds to 0, +359 to -1 etc.
    #so index - 360 for indices 540:720
    for i in range(540, 720):
        damage_per_second_totals_expected[i-360] += (
            damage_per_second_totals_expected[i])
   
    #finally, to get angles -179 to 180, select indices 181 to 540
    #of the new vector.
    damage_per_second_totals_expected = (
        damage_per_second_totals_expected[181:540])
   
    #note that vector indices no longer correspond to angles, rather
    #vector index 1 corresponds to -179. to get a correct plot add
   
    #the optimum angle is the midmost maximum
    damage_per_second_maximum = 0
    damage_per_second_maximum_index = 0
    for i, damage_per_second in enumerate(damage_per_second_totals_expected):
        if damage_per_second > damage_per_second_maximum:
            damage_per_second_maximum_index = i
    x_axis = tuple(i for i in range(-179,180))
    optimum_angle = x_axis[damage_per_second_maximum_index]
   
    #plot the result
    #plot(dps_at_angles,x=xaxis)
    #abline(v=optimumangle)
   
    #the usual - calculate ship cell upper bound angles
    target_angle = target_radius / (2 * math.pi * distance)
    cell_angle = target_angle / cell_count
    angle_ranges = [-target_angle/2]
    for _ in range(cell_count):
        angle_ranges.append(angle_ranges[-1] + cell_angle)
    #convert to pixels
    bound_distances = tuple(angle_range * 2 * math.pi * distance
                            for angle_range in angle_ranges)
    print("bound distances")
    for bound_distance in bound_distances:
        print(round(bound_distance, 3))
    print()
    #print results
    print("hit distribution at optimum angle")
    for i, weapon in enumerate(weapons):
        print("weapon", i)
        print(hit_distribution_at_optimum_angle(weapon["facing"],
                                                weapon["spread distance"],
                                                error_distance,
                                                weapon["minimum mean"],
                                                weapon["maximum mean"],
                                                distance,
                                                bound_distances))
main()
[close]
Result
spread angle 5
spread distance 87
minimum mean -17.5
maximum mean -2.5

spread angle 5
spread distance 87
minimum mean 2.5
maximum mean 17.5

spread angle 0
spread distance 0
minimum mean -170.0
maximum mean -150.0

spread angle 0
spread distance 0
minimum mean 170.0
maximum mean 190.0

spread angle 0
spread distance 0
minimum mean 150.0
maximum mean 170.0

spread angle 5
spread distance 87
minimum mean 87.5
maximum mean 92.5

bound distances
-110.0
-91.667
-73.333
-55.0
-36.667
-18.333
0.0
18.333
36.667
55.0
73.333
91.667
110.0

hit distribution at optimum angle
weapon 0
(0.46493696976183185, -0.06428797965951033, 0.011679301207214443, 0.01168495335116726, 0.011689350881235405, 0.011692492734030946, 0.011694378149718243, 0.01169500667228801, 0.011694378149718354, 0.011692492734030835, 0.011689350881235572, 0.01168495335116726, 0.011679301207214277, 0.46493696976183185)
weapon 1
(0.46493696976183185, -0.06428797965951033, 0.011679301207214443, 0.01168495335116726, 0.011689350881235405, 0.011692492734030946, 0.011694378149718243, 0.01169500667228801, 0.011694378149718354, 0.011692492734030835, 0.011689350881235572, 0.01168495335116726, 0.011679301207214277, 0.46493696976183185)
weapon 2
(0.01390344751349859, -0.9527200449016842, 0.057329929900487575, 0.10228955336156548, 0.1604441972208122, 0.221267775791116, 0.26832242536520173, 0.2861323265250028, 0.2683224253652018, 0.22126777579111578, 0.16044419722081205, 0.10228955336156553, 0.057329929900487464, 0.01390344751349859)
weapon 3
(0.01390344751349859, -0.9527200449016842, 0.057329929900487575, 0.10228955336156548, 0.1604441972208122, 0.221267775791116, 0.26832242536520173, 0.2861323265250028, 0.2683224253652018, 0.22126777579111578, 0.16044419722081205, 0.10228955336156553, 0.057329929900487464, 0.01390344751349859)
weapon 4
(0.01390344751349859, -0.9527200449016842, 0.057329929900487575, 0.10228955336156548, 0.1604441972208122, 0.221267775791116, 0.26832242536520173, 0.2861323265250028, 0.2683224253652018, 0.22126777579111578, 0.16044419722081205, 0.10228955336156553, 0.057329929900487464, 0.01390344751349859)
weapon 5
(0.46493696976183185, -0.06428797965951033, 0.011679301207214443, 0.01168495335116726, 0.011689350881235405, 0.011692492734030946, 0.011694378149718243, 0.01169500667228801, 0.011694378149718354, 0.011692492734030835, 0.011689350881235572, 0.01168495335116726, 0.011679301207214277, 0.46493696976183185)
[close]

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #318 on: December 22, 2022, 09:21:48 PM »

I'm a little confused as the numbers don't seem like mine. What does the graph look like?

Anyway there still seem to be some issues that need fixing as for example there is a -95% probability for weapon 3 to hit ship middle cell 1. I wonder if this is right?


adjustment_angle = transformed_angle(weapon_facing, minimum_mean,
                                         maximum_mean)


In

def hit_distribution_at_optimum_angle(
        weapon_facing: float,
        spread_distance: float,
        error_distance: float,
        minimum_mean: float,
        maximum_mean: float,
        distance: float,
        bounds: tuple) -> tuple:
    """
   
    spread_distance -
    weapon_facing -
    minimum_mean -
    maximum_mean -
    upper_bounds -
    """
    adjustment_angle = transformed_angle(weapon_facing, minimum_mean,
                                         maximum_mean)
    adjustment_distance = arc_length(adjustment_angle, distance)
    adjusted_bounds = tuple(bound + adjustment_distance for bound in bounds)
    return hit_distribution(adjusted_bounds, error_distance, spread_distance)


With the same reasoning as above. I think there must be something wrong with the probability calculation itself though, since it should never produce negative numbers when the calculation is correct since we are taking an integral in the positive direction over a strictly positive function.

I'm confused by this part. Let me comment what I think it does.

damage_per_second_total_expected = 0
        for weapon in weapons:
# calculate angle of target to dist mean - correct
            weapon_facing = transformed_angle(target_facing,
                                              weapon["minimum mean"],
                                              weapon["maximum mean"])
#convert it to an arc length
            weapon_arc = arc_length(weapon_facing, distance)
#take arc length of arc length - target radius?
            lower_bound = arc_length(weapon_arc - target_radius, distance)
# same with + radius
            upper_bound = arc_length(weapon_arc + target_radius, distance)
            probability = (
                probability_hit_before_bound(upper_bound, error_distance,
                                             weapon["spread distance"])
                - probability_hit_before_bound(lower_bound, error_distance,
                                               weapon["spread distance"]))
« Last Edit: December 22, 2022, 09:28:54 PM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #319 on: December 22, 2022, 10:13:44 PM »

I think the calculations here are relatively simple but it's very easy to get confused about the shifts in coordinates and signed angles and whatnot, leading to potentially no end of problems, so I tried to create a visual guide to what we're calculating here using MSPaint.

[attachment deleted by admin]
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 718
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #320 on: December 23, 2022, 09:05:37 AM »

My graph somewhat resembles yours: a very high flat-top peak on the left, followed by a sharp, shouldered peak, followed by a lower round-top peak, followed by a peak with a flat top but a single narrow pair of shoulder points high above with a single peak point high above them.  It has small negative probability valleys around the shouldered peak, and many of the probabilities near zero are very slightly negative or positive. 

On my graph, I have traced the source of negative probability  to the g helper function of my probability_hit_before_bound function and found that x was never greater than 0 when the probability ultimately determined by probability_hit_before_bound was less than 0 but that x could be less than 0 without the probability being less than 0.

In the hit_distribution function, I have found that an iteration error caused the negative probabilities.  It now yields a positive hit distribution.

Code
Code
"""
Calculate the optimum angle to place the enemy ship relative to ours.


Assume
- Angle exactly to the right is 0 degrees
- Possible angles are integers from -179 to 180
- Our ship is:
  - heading towards +90 degrees
  - pointlike.
- The enemy ship is:
  - at a constant range
  - a straight line of hittable armor cells
  - oriented tangentially to a circle defined by our ship's position
    and range.
- The ships are so far apart that the arc across that circle, from one
  point on the enemy ship to the other, approximates the secant between
  those points.


Weapon Arcs
A weapon cannot exceed its maximum turn angle and has constant spread;
therefore, the maximum mean points of the probability distribution of a
weapon are

{ maximum turn angle - spread / 2, minimum turn angle + spread/2 }

A weapon tracking a target tries to align the mean of the probability
distribution with the target. Therefore we can find the signed angle
of the target from the mean of the distribution by computing the
following in this order:

1. The mean point, that is, max mean, when target is above max mean
2. target, when target is between max mean
3. min mean, min mean, when target is below min mean.
4. Minimum and maximum means for the weapon and find the median of
its hit distribution ("transform hit coord" for coordinates of mean
of modified dist in terms of the original distribution)


Arcs vs Widths
Referring to the width of the enemy ship in pixels but to the placement
and tracking ability of turrets as angles is convenient. Therefore, we
must convert between the two. Note that we assume we can use the arc
to describe the enemy ship.


Methods
"""
import math
from statistics import NormalDist


def probability_hit_before_bound(
        x: float,
        standard_deviation: float,
        uniform_distribution_radius: float) -> float:
    """
    Return the probability that the hit coordinate is less than x.
   
    This probability equals the integral, from -inf to x, of the hit
    probability distribution, which is
   
    - normal if the parameter of the uniform distribution is 0
    - uniform if the standard deviation of the normal distribution is 0
    - trivial if both are 0, the hit distribution is trivial: the CDF
      equals a step function going from 0 to 1 where x = 0.
    - a normal distribution convolved with a uniform one if both exceed 0
   
    x - real number
    standard_deviation - standard deviation of normal distribution
    uniform_distribution_radius - radius of uniform distribution
    """
    def g(x: float, normal_distribution: object) -> float:
        """
        Return the cumulative distribution function of the convolved
        normal distribution.
       
        x - real number
        normal_distribution - a Python standard library NormalDist
        """
        return x * normal_distribution.cdf(x) + normal_distribution.pdf(x)

    def f(x: float,
          normal_distribution: float,
          uniform_distribution_parameter: float) -> float:
        a = x / normal_distribution.stdev
        b = uniform_distribution_parameter / normal_distribution.stdev
        c = normal_distribution.stdev / (2 * uniform_distribution_parameter)
        d = g(a + b, normal_distribution) - g(a - b, normal_distribution)
        p = c * d
        return p
    if standard_deviation > 0:
        normal_distribution = NormalDist(0, standard_deviation)
        p = (f(x, normal_distribution, uniform_distribution_radius)
                if uniform_distribution_radius > 0
                else normal_distribution.cdf(x))
        return p
    #if b > 0: return (f(x, NormalDist(0, standard_deviation),
    #                    uniform_distribution_radius) if a > 0
    #                  else max(0, min(1, b / 2 + x)))
    #return 0 if x < 0 else 1


def arc_measure(arc_length: float, radius: float) -> float:
    """
    Return the degree angle of an arc of some radius.
   
    arc -
    radius -
    """
    return arc_length / radius / 180 * math.pi


def arc_length(degrees: float, radius: float) -> float:
    """
    Return the arc of a degree angle of some radius.
   
    degrees -
    radius -
    """
    return degrees * math.pi / 180 * radius


def transformed_hit_coord(
        angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    """
    angle -
    minimum_mean -
    maximum_mean -
    """
    return max(minimum_mean, min(maximum_mean, angle))


def transformed_angle(
        angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    """
    Return the angle between the mean and target.
   
    We refer to this as "transforming" since we are moving from
    expressing the angle wrt. ship to wrt. weapon.
   
    angle -
    minimum_mean -
    maximum_mean -
    """
    return angle - transformed_hit_coord(angle, minimum_mean, maximum_mean)


def hit_distribution(
        bounds: tuple,
        standard_deviation: float,
        spread: float) -> tuple:
    """
    Return probability to hit between each pair of bounds.
   
    bounds -
    standard_deviation -
    spread -
    """
    if standard_deviation == 0:
        if spread == 0:
            #all shots hit 1 cell, even if the ship has evenly many
            #cells, to prevent such ships from appearing tougher
            return 0, + tuple(1 if bounds[i-1] < 0 <= bound else 0
                              for i, bound in enumerate(bounds[1:]))
        double_spread = 2 * spread
        return (min(1, max(0, (bounds[0] + spread)) / double_spread),
                + tuple(min(1, max(0, bounds[i+1] + spread) / double_spread)
                        - min(1, max(0, bounds[i] + spread) / double_spread)
                        for i, bound in enumerate(bounds[:-1]))
                + (1 - min(1, max(0, bounds[-1] + spread) / double_spread)),)
    elif spread == 0:
        cdf = NormalDist(0, standard_deviation).cdf
        return ((cdf(bounds[0]),)
                + tuple(cdf(bounds[i+1]) - cdf(bound) for i, bound in
                        enumerate(bounds[:-1]))
                + ((1 - cdf(bounds[-1])),))
    numbers = standard_deviation, spread
    return ((probability_hit_before_bound(bounds[0], *numbers),)
            + tuple(probability_hit_before_bound(bounds[i+1], *numbers)
                    - probability_hit_before_bound(bound, *numbers)
                    for i, bound in enumerate(bounds[:-1]))
            + ((1 - probability_hit_before_bound(bounds[-1], *numbers)),))
   

def hit_distribution_at_optimum_angle(
        weapon_facing: float,
        spread_distance: float,
        error_distance: float,
        minimum_mean: float,
        maximum_mean: float,
        distance: float,
        bounds: tuple) -> tuple:
    """
   
    spread_distance -
    weapon_facing -
    minimum_mean -
    maximum_mean -
    upper_bounds -
    """
    adjustment_angle = transformed_angle(weapon_facing, minimum_mean,
                                         maximum_mean)
    adjustment_distance = arc_length(adjustment_angle, distance)
    adjusted_bounds = tuple(bound + adjustment_distance for bound in bounds)
    return hit_distribution(adjusted_bounds, error_distance, spread_distance)


def main():
    #Test against a Dominator. We wish to integrate this code
    #with the other modules eventually, so we will use the full ship
    #definition, even though only the width is needed.
    distance = 1000
    cell_count = 12
    target_radius = 220#(14000, 500, 10000, 1500, 220, 12, 440, 1.0, 200)
    target_arc_measure = arc_measure(target_radius, distance)
   
    #two weapons slightly angled from each other, arcs overlapping
    #in the middle, and rear-mounted point defense weapons to test
    #wraparound, extreme angles
    weapons = (
        {"damage" : 100, "facing" : -10, "arc" : 20, "spread" : 5},
        {"damage" : 100, "facing" : 10, "arc" : 20, "spread" : 5},
        {"damage" : 30, "facing" : -160, "arc" : 20, "spread" : 0},
        {"damage" : 30, "facing" : 180, "arc" : 20, "spread" : 0},
        {"damage" : 30, "facing" : 160, "arc" : 20, "spread" : 0},
        {"damage" : 120, "facing" : 90, "arc" : 0, "spread" : 5}
    )
    for weapon in weapons:
        #we have defined spread in degrees, so we must convert it to
        #pixels to be consistent
        half_difference = (weapon["arc"] - weapon["spread"]) / 2
        minimum_mean = weapon["facing"] - half_difference
        maximum_mean = weapon["facing"] + half_difference
        weapon["minimum mean"] = min(minimum_mean, maximum_mean)
        weapon["maximum mean"] = max(minimum_mean, maximum_mean)
        weapon["spread distance"] = arc_length(weapon["spread"], distance)
        #print("spread angle", weapon["spread"])
        #print("spread distance", round(weapon["spread distance"]))
        #print("minimum mean", weapon["minimum mean"])
        #print("maximum mean", weapon["maximum mean"])
        #print()
    #same standard deviation as in other modules
    error_standard_deviation = 0.05
    error_distance = error_standard_deviation * distance
   
    #Map signed to unsigned angles
   
    #Define a vector from -360 to 360, encompassing all possible
    #firing angles because a weapon can face from -180 to 180
    #degrees off the ship's facing and can track at most 360
    #degrees total.
    damage_per_second_totals_expected = []
    for target_facing in range(-359,361):
        damage_per_second_total_expected = 0
        for weapon in weapons:
            weapon_facing = transformed_angle(target_facing,
                                              weapon["minimum mean"],
                                              weapon["maximum mean"])
            weapon_arc = arc_length(weapon_facing, distance)
            lower_bound = arc_length(weapon_arc, distance) - target_radius
            upper_bound = arc_length(weapon_arc, distance) + target_radius
            probability = (
                probability_hit_before_bound(upper_bound, error_distance,
                                             weapon["spread distance"])
                - probability_hit_before_bound(lower_bound, error_distance,
                                               weapon["spread distance"]))
            damage_per_second_total_expected += weapon["damage"] * probability
        damage_per_second_totals_expected.append(
            damage_per_second_total_expected)

    #-180 corresponds to +180
    #-181 corresponds to +179 etc, so 360 + index for indices 1:180
    for i in range(180):
        damage_per_second_totals_expected[i+360] += (
            damage_per_second_totals_expected[i])
   
    #and +360 corresponds to 0, +359 to -1 etc.
    #so index - 360 for indices 540:720
    for i in range(540, 720):
        damage_per_second_totals_expected[i-360] += (
            damage_per_second_totals_expected[i])
   
    #finally, to get angles -179 to 180, select indices 181 to 540
    #of the new vector.
    damage_per_second_totals_expected = (
        damage_per_second_totals_expected[181:540])
   
    #note that vector indices no longer correspond to angles, rather
    #vector index 1 corresponds to -179. to get a correct plot add
   
    #the optimum angle is the midmost maximum
    damage_per_second_maximum = 0
    damage_per_second_maximum_index = 0
    for i, damage_per_second in enumerate(damage_per_second_totals_expected):
        if damage_per_second > damage_per_second_maximum:
            damage_per_second_maximum_index = i
    x_axis = tuple(i for i in range(-179,180))
    optimum_angle = x_axis[damage_per_second_maximum_index]
   
    #plot the result
    #plot(dps_at_angles,x=xaxis)
    #abline(v=optimumangle)
   
    #the usual - calculate ship cell upper bound angles
    target_angle = target_radius / (2 * math.pi * distance)
    cell_angle = target_angle / cell_count
    angle_ranges = [-target_angle/2]
    for _ in range(cell_count):
        angle_ranges.append(angle_ranges[-1] + cell_angle)
    #convert to pixels
    bound_distances = tuple(angle_range * 2 * math.pi * distance
                            for angle_range in angle_ranges)
    #print("bound distances")
    #for bound_distance in bound_distances:
        #print(round(bound_distance, 3))
    print()
    #print results
    print("hit distribution at optimum angle")
    for i, weapon in enumerate(weapons):
        print("weapon", i)
        print(hit_distribution_at_optimum_angle(weapon["facing"],
                                                weapon["spread distance"],
                                                error_distance,
                                                weapon["minimum mean"],
                                                weapon["maximum mean"],
                                                distance,
                                                bound_distances))
main()
[close]
Result
hit distribution at optimum angle
weapon 0
(0.46493696976183185, 0.0058380808168259635, 0.00584122039038848, 0.00584373296077878, 0.005845617920456625, 0.005846874813574321, 0.005847503336143922, 0.005847503336144089, 0.005846874813574265, 0.0058456179204565695, 0.005843732960779002, 0.005841220390388258, 0.005838080816826019, 0.46493696976183185)
weapon 1
(0.46493696976183185, 0.0058380808168259635, 0.00584122039038848, 0.00584373296077878, 0.005845617920456625, 0.005846874813574321, 0.005847503336143922, 0.005847503336144089, 0.005846874813574265, 0.0058456179204565695, 0.005843732960779002, 0.005841220390388258, 0.005838080816826019, 0.46493696976183185)
weapon 2
(0.01390344751349859, 0.01947306007131866, 0.037856869829168915, 0.06443268353239656, 0.09601151368841565, 0.12525626210270036, 0.14306616326250138, 0.14306616326250143, 0.12525626210270036, 0.09601151368841543, 0.06443268353239662, 0.037856869829168915, 0.01947306007131855, 0.01390344751349859)
weapon 3
(0.01390344751349859, 0.01947306007131866, 0.037856869829168915, 0.06443268353239656, 0.09601151368841565, 0.12525626210270036, 0.14306616326250138, 0.14306616326250143, 0.12525626210270036, 0.09601151368841543, 0.06443268353239662, 0.037856869829168915, 0.01947306007131855, 0.01390344751349859)
weapon 4
(0.01390344751349859, 0.01947306007131866, 0.037856869829168915, 0.06443268353239656, 0.09601151368841565, 0.12525626210270036, 0.14306616326250138, 0.14306616326250143, 0.12525626210270036, 0.09601151368841543, 0.06443268353239662, 0.037856869829168915, 0.01947306007131855, 0.01390344751349859)
weapon 5
(0.46493696976183185, 0.0058380808168259635, 0.00584122039038848, 0.00584373296077878, 0.005845617920456625, 0.005846874813574321, 0.005847503336143922, 0.005847503336144089, 0.005846874813574265, 0.0058456179204565695, 0.005843732960779002, 0.005841220390388258, 0.005838080816826019, 0.46493696976183185)
[close]

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #321 on: December 23, 2022, 09:56:54 AM »

Great, making progress! The hit distributions are still quite off. Here are the correct ones from before:


> #print results
> hit_distribution_at_optimum_angle(weapons[1,])
 [1] 0.118751068 0.077981550 0.102383867 0.121027732 0.129001152 0.124055991 0.107596198 0.084057103
 [9] 0.059030566 0.037172868 0.020934021 0.010514600 0.004698446 0.002794840
> hit_distribution_at_optimum_angle(weapons[2,])
 [1] 0.002794840 0.004698446 0.010514600 0.020934021 0.037172868 0.059030566 0.084057103 0.107596198
 [9] 0.124055991 0.129001152 0.121027732 0.102383867 0.077981550 0.118751068
> hit_distribution_at_optimum_angle(weapons[3,])
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
> hit_distribution_at_optimum_angle(weapons[4,])
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
> hit_distribution_at_optimum_angle(weapons[5,])
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
> hit_distribution_at_optimum_angle(weapons[6,])
 [1] 8.351879e-250 1.799421e-244 3.390000e-239 5.584509e-234 8.044357e-229 1.013261e-223 1.116033e-218
 [8] 1.074884e-213 9.052691e-209 6.666963e-204 4.293542e-199 2.417929e-194 1.190735e-189  1.000000e+00


Even if there were some kind of an error in my code, we know the real ones must look something like this since the first two guns are symmetrically reaching for a target just a little outside their ability to align the probability distribution, and the others are far out of range.

By contrast, your guns 2 to 5 can hit and 0 to 1 have a flat distribution. Was there anything to what I commented above about calculating the arc corresponding to an arc?

Also, is this fixed?
Result
spread angle 5
spread distance 87
minimum mean -17.5
maximum mean -2.5

spread angle 5
spread distance 87
minimum mean 2.5
maximum mean 17.5

spread angle 0
spread distance 0
minimum mean -170.0
maximum mean -150.0

spread angle 0
spread distance 0
minimum mean 170.0
maximum mean 190.0

spread angle 0
spread distance 0
minimum mean 150.0
maximum mean 170.0

spread angle 5
spread distance 87
minimum mean 87.5
maximum mean 92.5


[close]
Specifically, about weapon 5: when it is the case that spread is greater than or equal to tracking angle (or tracking angle is 0 which is a subset of this) then it must be min mean = max mean = facing since the weapon cannot track. In that print it seems like weapon 5 can track over 5 degrees yet has an arc of 0. This must be special cased for all weapons with spread >= tracking arc.
« Last Edit: December 23, 2022, 09:58:25 AM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 718
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #322 on: December 23, 2022, 02:25:41 PM »

Great, making progress! The hit distributions are still quite off. Here are the correct ones from before:


> #print results
> hit_distribution_at_optimum_angle(weapons[1,])
 [1] 0.118751068 0.077981550 0.102383867 0.121027732 0.129001152 0.124055991 0.107596198 0.084057103
 [9] 0.059030566 0.037172868 0.020934021 0.010514600 0.004698446 0.002794840
> hit_distribution_at_optimum_angle(weapons[2,])
 [1] 0.002794840 0.004698446 0.010514600 0.020934021 0.037172868 0.059030566 0.084057103 0.107596198
 [9] 0.124055991 0.129001152 0.121027732 0.102383867 0.077981550 0.118751068
> hit_distribution_at_optimum_angle(weapons[3,])
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
> hit_distribution_at_optimum_angle(weapons[4,])
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
> hit_distribution_at_optimum_angle(weapons[5,])
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
> hit_distribution_at_optimum_angle(weapons[6,])
 [1] 8.351879e-250 1.799421e-244 3.390000e-239 5.584509e-234 8.044357e-229 1.013261e-223 1.116033e-218
 [8] 1.074884e-213 9.052691e-209 6.666963e-204 4.293542e-199 2.417929e-194 1.190735e-189  1.000000e+00


Even if there were some kind of an error in my code, we know the real ones must look something like this since the first two guns are symmetrically reaching for a target just a little outside their ability to align the probability distribution, and the others are far out of range.

I definitely don't have those distributions.  The hit_distribution function of my code cannot return any of the three one-and-zeros distributions shown unless the standard deviation (declared as error_distance in the main method) is 0, and the standard deviation is not zero.

Code
def hit_distribution(
        bounds: tuple,
        standard_deviation: float,
        spread: float) -> tuple:
    """
    Return probability to hit between each pair of bounds.
   
    bounds -
    standard_deviation -
    spread -
    """
    if standard_deviation == 0:
        if spread == 0:
            #all shots hit 1 cell, even if the ship has evenly many
            #cells, to prevent such ships from appearing tougher
            return 0, + tuple(1 if bounds[i-1] < 0 <= bound else 0
                              for i, bound in enumerate(bounds[1:]))
        double_spread = 2 * spread
        return (min(1, max(0, (bounds[0] + spread)) / double_spread),
                + tuple(min(1, max(0, bounds[i+1] + spread) / double_spread)
                        - min(1, max(0, bounds[i] + spread) / double_spread)
                        for i, bound in enumerate(bounds[:-1]))
                + (1 - min(1, max(0, bounds[-1] + spread) / double_spread)),)
    elif spread == 0:
        cdf = NormalDist(0, standard_deviation).cdf
        return ((cdf(bounds[0]),)
                + tuple(cdf(bounds[i+1]) - cdf(bound) for i, bound in
                        enumerate(bounds[:-1]))
                + ((1 - cdf(bounds[-1])),))
    numbers = standard_deviation, spread
    return ((probability_hit_before_bound(bounds[0], *numbers),)
            + tuple(probability_hit_before_bound(bounds[i+1], *numbers)
                    - probability_hit_before_bound(bound, *numbers)
                    for i, bound in enumerate(bounds[:-1]))
            + ((1 - probability_hit_before_bound(bounds[-1], *numbers)),))
Code
def main():
    #same standard deviation as in other modules
    error_standard_deviation = 0.05
    error_distance = error_standard_deviation * distance
    ...

Also, my hit_probability_before_bound function does not cover cases where standard deviation is zero, but we have a non-zero one, so I suppose that's alright.

Quote
By contrast, your guns 2 to 5 can hit and 0 to 1 have a flat distribution. Was there anything to what I commented above about calculating the arc corresponding to an arc?

Would you please rephrase both of these sentences?  I don't understand them.

Also, is this fixed?
Result
spread angle 5
spread distance 87
minimum mean -17.5
maximum mean -2.5

spread angle 5
spread distance 87
minimum mean 2.5
maximum mean 17.5

spread angle 0
spread distance 0
minimum mean -170.0
maximum mean -150.0

spread angle 0
spread distance 0
minimum mean 170.0
maximum mean 190.0

spread angle 0
spread distance 0
minimum mean 150.0
maximum mean 170.0

spread angle 5
spread distance 87
minimum mean 87.5
maximum mean 92.5

[close]
Specifically, about weapon 5: when it is the case that spread is greater than or equal to tracking angle (or tracking angle is 0 which is a subset of this) then it must be min mean = max mean = facing since the weapon cannot track. In that print it seems like weapon 5 can track over 5 degrees yet has an arc of 0. This must be special cased for all weapons with spread >= tracking arc.
[/quote]

I have now fixed that.

Code
    for weapon in weapons:
        #we have defined spread in degrees, so we must convert it to
        #pixels to be consistent
        if weapon["spread"] >= weapon["arc"] or weapon["arc"] == 0:
            minimum_mean = weapon["facing"]
            maximum_mean = weapon["facing"]
        else:
            half_difference = (weapon["arc"] - weapon["spread"]) / 2
            minimum_mean = weapon["facing"] - half_difference
            maximum_mean = weapon["facing"] + half_difference

Code
Code
"""
Calculate the optimum angle to place the enemy ship relative to ours.


Assume
- Angle exactly to the right is 0 degrees
- Possible angles are integers from -179 to 180
- Our ship is:
  - heading towards +90 degrees
  - pointlike.
- The enemy ship is:
  - at a constant range
  - a straight line of hittable armor cells
  - oriented tangentially to a circle defined by our ship's position
    and range.
- The ships are so far apart that the arc across that circle, from one
  point on the enemy ship to the other, approximates the secant between
  those points.


Weapon Arcs
A weapon cannot exceed its maximum turn angle and has constant spread;
therefore, the maximum mean points of the probability distribution of a
weapon are

{ maximum turn angle - spread / 2, minimum turn angle + spread/2 }

A weapon tracking a target tries to align the mean of the probability
distribution with the target. Therefore we can find the signed angle
of the target from the mean of the distribution by computing the
following in this order:

1. The mean point, that is, max mean, when target is above max mean
2. target, when target is between max mean
3. min mean, min mean, when target is below min mean.
4. Minimum and maximum means for the weapon and find the median of
its hit distribution ("transform hit coord" for coordinates of mean
of modified dist in terms of the original distribution)


Arcs vs Widths
Referring to the width of the enemy ship in pixels but to the placement
and tracking ability of turrets as angles is convenient. Therefore, we
must convert between the two. Note that we assume we can use the arc
to describe the enemy ship.


Methods
"""
import math
from statistics import NormalDist


def probability_hit_before_bound(
        x: float,
        standard_deviation: float,
        uniform_distribution_radius: float) -> float:
    """
    Return the probability that the hit coordinate is less than x.
   
    This probability equals the integral, from -inf to x, of the hit
    probability distribution, which is
   
    - normal if the parameter of the uniform distribution is 0
    - uniform if the standard deviation of the normal distribution is 0
    - trivial if both are 0, the hit distribution is trivial: the CDF
      equals a step function going from 0 to 1 where x = 0.
    - a normal distribution convolved with a uniform one if both exceed 0
   
    x - real number
    standard_deviation - standard deviation of normal distribution
    uniform_distribution_radius - radius of uniform distribution
    """
    def g(x: float, normal_distribution: object) -> float:
        """
        Return the cumulative distribution function of the convolved
        normal distribution.
       
        x - real number
        normal_distribution - a Python standard library NormalDist
        """
        return x * normal_distribution.cdf(x) + normal_distribution.pdf(x)

    def f(x: float,
          normal_distribution: float,
          uniform_distribution_parameter: float) -> float:
        a = x / normal_distribution.stdev
        b = uniform_distribution_parameter / normal_distribution.stdev
        return (normal_distribution.stdev
                / (2 * uniform_distribution_parameter)
                * (g(a + b, normal_distribution)
                   - g(a - b, normal_distribution)))
    if standard_deviation > 0:
        normal_distribution = NormalDist(0, standard_deviation)
        return (f(x, normal_distribution, uniform_distribution_radius)
                if uniform_distribution_radius > 0
                else normal_distribution.cdf(x))
    #if b > 0: return (f(x, NormalDist(0, standard_deviation),
    #                    uniform_distribution_radius) if a > 0
    #                  else max(0, min(1, b / 2 + x)))
    #return 0 if x < 0 else 1


def arc_measure(arc_length: float, radius: float) -> float:
    """
    Return the degree angle of an arc of some radius.
   
    arc -
    radius -
    """
    return arc_length / radius / 180 * math.pi


def arc_length(degrees: float, radius: float) -> float:
    """
    Return the arc of a degree angle of some radius.
   
    degrees -
    radius -
    """
    return degrees * math.pi / 180 * radius


def transformed_hit_coord(
        angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    """
    angle -
    minimum_mean -
    maximum_mean -
    """
    return max(minimum_mean, min(maximum_mean, angle))


def transformed_angle(
        angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    """
    Return the angle between the mean and target.
   
    We refer to this as "transforming" since we are moving from
    expressing the angle wrt. ship to wrt. weapon.
   
    angle -
    minimum_mean -
    maximum_mean -
    """
    return angle - transformed_hit_coord(angle, minimum_mean, maximum_mean)


def hit_distribution(
        bounds: tuple,
        standard_deviation: float,
        spread: float) -> tuple:
    """
    Return probability to hit between each pair of bounds.
   
    bounds -
    standard_deviation -
    spread -
    """
    if standard_deviation == 0:
        if spread == 0:
            #all shots hit 1 cell, even if the ship has evenly many
            #cells, to prevent such ships from appearing tougher
            return 0, + tuple(1 if bounds[i-1] < 0 <= bound else 0
                              for i, bound in enumerate(bounds[1:]))
        double_spread = 2 * spread
        return (min(1, max(0, (bounds[0] + spread)) / double_spread),
                + tuple(min(1, max(0, bounds[i+1] + spread) / double_spread)
                        - min(1, max(0, bounds[i] + spread) / double_spread)
                        for i, bound in enumerate(bounds[:-1]))
                + (1 - min(1, max(0, bounds[-1] + spread) / double_spread)),)
    elif spread == 0:
        cdf = NormalDist(0, standard_deviation).cdf
        return ((cdf(bounds[0]),)
                + tuple(cdf(bounds[i+1]) - cdf(bound) for i, bound in
                        enumerate(bounds[:-1]))
                + ((1 - cdf(bounds[-1])),))
    numbers = standard_deviation, spread
    return ((probability_hit_before_bound(bounds[0], *numbers),)
            + tuple(probability_hit_before_bound(bounds[i+1], *numbers)
                    - probability_hit_before_bound(bound, *numbers)
                    for i, bound in enumerate(bounds[:-1]))
            + ((1 - probability_hit_before_bound(bounds[-1], *numbers)),))
   

def hit_distribution_at_optimum_angle(
        weapon_facing: float,
        spread_distance: float,
        error_distance: float,
        minimum_mean: float,
        maximum_mean: float,
        distance: float,
        bounds: tuple) -> tuple:
    """
   
    spread_distance -
    weapon_facing -
    minimum_mean -
    maximum_mean -
    upper_bounds -
    """
    adjustment_angle = transformed_angle(weapon_facing, minimum_mean,
                                         maximum_mean)
    adjustment_distance = arc_length(adjustment_angle, distance)
    adjusted_bounds = tuple(bound + adjustment_distance for bound in bounds)
    return hit_distribution(adjusted_bounds, error_distance, spread_distance)


def main():
    #same standard deviation as in other modules
    error_standard_deviation = 0.05
    error_distance = error_standard_deviation * distance
   
    #Test against a Dominator. We wish to integrate this code
    #with the other modules eventually, so we will use the full ship
    #definition, even though only the width is needed.
    distance = 1000
    cell_count = 12
    target_radius = 220#(14000, 500, 10000, 1500, 220, 12, 440, 1.0, 200)
    target_arc_measure = arc_measure(target_radius, distance)
   
    #two weapons slightly angled from each other, arcs overlapping
    #in the middle, and rear-mounted point defense weapons to test
    #wraparound, extreme angles
    weapons = (
        {"damage" : 100, "facing" : -10, "arc" : 20, "spread" : 5},
        {"damage" : 100, "facing" : 10, "arc" : 20, "spread" : 5},
        {"damage" : 30, "facing" : -160, "arc" : 20, "spread" : 0},
        {"damage" : 30, "facing" : 180, "arc" : 20, "spread" : 0},
        {"damage" : 30, "facing" : 160, "arc" : 20, "spread" : 0},
        {"damage" : 120, "facing" : 90, "arc" : 0, "spread" : 5}
    )
    for weapon in weapons:
        #we have defined spread in degrees, so we must convert it to
        #pixels to be consistent
        if weapon["spread"] >= weapon["arc"] or weapon["arc"] == 0:
            minimum_mean = weapon["facing"]
            maximum_mean = weapon["facing"]
        else:
            half_difference = (weapon["arc"] - weapon["spread"]) / 2
            minimum_mean = weapon["facing"] - half_difference
            maximum_mean = weapon["facing"] + half_difference
        weapon["minimum mean"] = min(minimum_mean, maximum_mean)
        weapon["maximum mean"] = max(minimum_mean, maximum_mean)
        weapon["spread distance"] = arc_length(weapon["spread"], distance)
        print("spread angle", weapon["spread"])
        print("spread distance", round(weapon["spread distance"]))
        print("minimum mean", weapon["minimum mean"])
        print("maximum mean", weapon["maximum mean"])
        print()
   
    #Map signed to unsigned angles
   
    #Define a vector from -360 to 360, encompassing all possible
    #firing angles because a weapon can face from -180 to 180
    #degrees off the ship's facing and can track at most 360
    #degrees total.
    damage_per_second_totals_expected = []
    for target_facing in range(-359,361):
        damage_per_second_total_expected = 0
        for weapon in weapons:
            weapon_facing = transformed_angle(target_facing,
                                              weapon["minimum mean"],
                                              weapon["maximum mean"])
            weapon_arc = arc_length(weapon_facing, distance)
            lower_bound = arc_length(weapon_arc, distance) - target_radius
            upper_bound = arc_length(weapon_arc, distance) + target_radius
            probability = (
                probability_hit_before_bound(upper_bound, error_distance,
                                             weapon["spread distance"])
                - probability_hit_before_bound(lower_bound, error_distance,
                                               weapon["spread distance"]))
            damage_per_second_total_expected += weapon["damage"] * probability
        damage_per_second_totals_expected.append(
            damage_per_second_total_expected)

    #-180 corresponds to +180
    #-181 corresponds to +179 etc, so 360 + index for indices 1:180
    for i in range(180):
        damage_per_second_totals_expected[i+360] += (
            damage_per_second_totals_expected[i])
   
    #and +360 corresponds to 0, +359 to -1 etc.
    #so index - 360 for indices 540:720
    for i in range(540, 720):
        damage_per_second_totals_expected[i-360] += (
            damage_per_second_totals_expected[i])
   
    #finally, to get angles -179 to 180, select indices 181 to 540
    #of the new vector.
    damage_per_second_totals_expected = (
        damage_per_second_totals_expected[181:540])
   
    #note that vector indices no longer correspond to angles, rather
    #vector index 1 corresponds to -179. to get a correct plot add
   
    #the optimum angle is the midmost maximum
    damage_per_second_maximum = 0
    damage_per_second_maximum_index = 0
    for i, damage_per_second in enumerate(damage_per_second_totals_expected):
        if damage_per_second > damage_per_second_maximum:
            damage_per_second_maximum_index = i
    x_axis = tuple(i for i in range(-179,180))
    optimum_angle = x_axis[damage_per_second_maximum_index]
   
    #plot the result
    #plot(dps_at_angles,x=xaxis)
    #abline(v=optimumangle)
   
    #the usual - calculate ship cell upper bound angles
    target_angle = target_radius / (2 * math.pi * distance)
    cell_angle = target_angle / cell_count
    angle_ranges = [-target_angle/2]
    for _ in range(cell_count):
        angle_ranges.append(angle_ranges[-1] + cell_angle)
    #convert to pixels
    bound_distances = tuple(angle_range * 2 * math.pi * distance
                            for angle_range in angle_ranges)
    #print("bound distances")
    #for bound_distance in bound_distances:
        #print(round(bound_distance, 3))
    #print results
    print("hit distribution at optimum angle")
    for i, weapon in enumerate(weapons):
        print("weapon", i)
        print(hit_distribution_at_optimum_angle(weapon["facing"],
                                                weapon["spread distance"],
                                                error_distance,
                                                weapon["minimum mean"],
                                                weapon["maximum mean"],
                                                distance,
                                                bound_distances))
main()
[close]
Result
spread angle 5
spread distance 87
minimum mean -17.5
maximum mean -2.5

spread angle 5
spread distance 87
minimum mean 2.5
maximum mean 17.5

spread angle 0
spread distance 0
minimum mean -170.0
maximum mean -150.0

spread angle 0
spread distance 0
minimum mean 170.0
maximum mean 190.0

spread angle 0
spread distance 0
minimum mean 150.0
maximum mean 170.0

spread angle 5
spread distance 87
minimum mean 90
maximum mean 90

hit distribution at optimum angle
weapon 0
(0.46493696976183185, 0.0058380808168259635, 0.00584122039038848, 0.00584373296077878, 0.005845617920456625, 0.005846874813574321, 0.005847503336143922, 0.005847503336144089, 0.005846874813574265, 0.0058456179204565695, 0.005843732960779002, 0.005841220390388258, 0.005838080816826019, 0.46493696976183185)
weapon 1
(0.46493696976183185, 0.0058380808168259635, 0.00584122039038848, 0.00584373296077878, 0.005845617920456625, 0.005846874813574321, 0.005847503336143922, 0.005847503336144089, 0.005846874813574265, 0.0058456179204565695, 0.005843732960779002, 0.005841220390388258, 0.005838080816826019, 0.46493696976183185)
weapon 2
(0.01390344751349859, 0.01947306007131866, 0.037856869829168915, 0.06443268353239656, 0.09601151368841565, 0.12525626210270036, 0.14306616326250138, 0.14306616326250143, 0.12525626210270036, 0.09601151368841543, 0.06443268353239662, 0.037856869829168915, 0.01947306007131855, 0.01390344751349859)
weapon 3
(0.01390344751349859, 0.01947306007131866, 0.037856869829168915, 0.06443268353239656, 0.09601151368841565, 0.12525626210270036, 0.14306616326250138, 0.14306616326250143, 0.12525626210270036, 0.09601151368841543, 0.06443268353239662, 0.037856869829168915, 0.01947306007131855, 0.01390344751349859)
weapon 4
(0.01390344751349859, 0.01947306007131866, 0.037856869829168915, 0.06443268353239656, 0.09601151368841565, 0.12525626210270036, 0.14306616326250138, 0.14306616326250143, 0.12525626210270036, 0.09601151368841543, 0.06443268353239662, 0.037856869829168915, 0.01947306007131855, 0.01390344751349859)
weapon 5
(0.46493696976183185, 0.0058380808168259635, 0.00584122039038848, 0.00584373296077878, 0.005845617920456625, 0.005846874813574321, 0.005847503336143922, 0.005847503336144089, 0.005846874813574265, 0.0058456179204565695, 0.005843732960779002, 0.005841220390388258, 0.005838080816826019, 0.46493696976183185)
[close]

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #323 on: December 23, 2022, 09:00:51 PM »

Oh, the 1s come from rounding. A normal pdf or CDF is never completely zero (or 1, for the CDF) due to the definition of the function. R will round things like 1-1*10^-350 to 1, if it even can calculate the very small number at all. For example for a standard normal distribution, the pdf of value 100 is 1/sqrt(2pi)*e^(-5000). You can see this in the other gun where there is a 1*10^-189 chance to hit cell 12 but 100% chance to miss.

I'll rephrase: according to the hit distribution, your guns 0 and 1 seem to have an approximately flat hit distribution, which is not correct, since they have a small spread. Your guns 2 to 5 have a nonzero (and non-minuscule, in fact significant) probability to hit the target despite being located on the opposite side of the ship and only having a 20 degree tracking arc in case of guns 2 to 4 and not being able to track and not being aimed at target in case of gun 5. So these results are not correct. (Additionally, it appears your guns 0 and 1 are identical, when they should be mirror images of each other, and gun 5 is identical to 0 and 1, despite having very different parameters).

It seems like at


weapon_arc = arc_length(weapon_facing, distance)
            lower_bound = arc_length(weapon_arc, distance) - target_radius


You are calculating lower bound as arc length of ( arc length of (weapon facing)) - target radius'when it should probably be arc length of ( weapon facing ) - target radius. So lower_bound = weapon_arc - target_radius . And likewise for upper bound.

Merry Christmas/happy holidays by the way!
« Last Edit: December 23, 2022, 09:49:38 PM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 718
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #324 on: December 23, 2022, 10:42:12 PM »

Oh, the 1s come from rounding. A normal pdf or CDF is never completely zero (or 1, for the CDF) due to the definition of the function. R will round things like 1-1*10^-350 to 1, if it even can calculate the very small number at all. For example for a standard normal distribution, the pdf of value 100 is 1/sqrt(2pi)*e^(-5000). You can see this in the other gun where there is a 1*10^-189 chance to hit cell 12 but 100% chance to miss.

Ah, thanks.  Python does not automatically round floats.

Quote
I'll rephrase: according to the hit distribution, your guns 0 and 1 seem to have an approximately flat hit distribution, which is not correct, since they have a small spread. Your guns 2 to 5 have a nonzero (and non-minuscule, in fact significant) probability to hit the target despite being located on the opposite side of the ship and only having a 20 degree tracking arc in case of guns 2 to 4 and not being able to track and not being aimed at target in case of gun 5. So these results are not correct. (Additionally, it appears your guns 0 and 1 are identical, when they should be mirror images of each other, and gun 5 is identical to 0 and 1, despite having very different parameters).

Thanks, now I understand the error you see.

Quote
It seems like at


weapon_arc = arc_length(weapon_facing, distance)
            lower_bound = arc_length(weapon_arc, distance) - target_radius


You are calculating lower bound as arc length of ( arc length of (weapon facing)) - target radius'when it should probably be arc length of ( weapon facing ) - target radius. So lower_bound = weapon_arc - target_radius . And likewise for upper bound.

Fixing that error only makes the hit distribution funky.  So many times has fixing each bug in this code only exposed another that I wonder if I mangled something when initially translating your code, which I had struggled to understand because of its structure.  Would you please reorganize your code per my requirements for me to translate it over again without rearrangement?  Here are the most important requirements again:

1. Put constants at the top in ALL CAPS
2. Put everything else into functions
3. Specify what each function should take as arguments, accomplish, and return
4. Make one function called main to call the other functions

If you would, then I could translate your R line-by-line into Python, which I could test for equivalence and only then substantially re-organize for elegance; else, I can only poke my Python code to do what I guess what your R code does and hope that I guessed right.

In other news, I have written a "Hello, world" Python desktop app and frozen it into an executable, which I ran on my machine with one click.  I now know that, in principle, I can create a portable application to run my Python code and display its results.  The next step would be making the executable into one clickable file and adding a Console Commands binding to call it while playing Starsector to avoid breaking workflow.  8)

Quote
Merry Christmas/happy holidays by the way!

Let the festivities begin!  ;D

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #325 on: December 23, 2022, 11:02:42 PM »

Will do, though probably not until after 25th! Shouldn't take very long when I have the chance, since it's just a matter of migrating from mathematical writing style and organization to programmer style and organization.

Great news about the executable, this is going to be quite nice I think.
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Goumindong

  • Admiral
  • *****
  • Posts: 1896
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #326 on: December 24, 2022, 06:49:17 PM »

Is spread actually normal? Just looking at how weapons shoot in game seems like it’s a uniform distribution rather than a normal one. (Granted this isn’t a scientific examination but the tails seem awful heavy for a normal distribution)

Now this will eventually approximate a normal distribution but since we’re looking for TTK and doing a sequence by individual shots starting with a normal distribution will be inaccurate if the actual distribution is uniform
Logged

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #327 on: December 24, 2022, 09:00:48 PM »

Oh, spread is uniform (or more accurately we assume the angle distribution of shots as they leave the gun, which we call spread, is uniform with parameter spread/2). That is the assumption. However, a model based on spread only will underestimate time to kill significantly, due to among other things ship movement and rotation which we are not modeling. That is why we added another parameter, the "fudge factor" ie. a normal distribution on top to model difference from the ideal situation of immobile ships and perfectly aligned gunfire. The distribution we are using is a convolution of these two since the hit location is assumed to be shot angle, from a uniform distribution, + error, from a normal distribution. The normal distribution is calibrated with sim data.

There are some nice graphs of the distributions we are using on page 4 of this thread. https://fractalsoftworks.com/forum/index.php?topic=25536.45 . I also advocate that in the final version it should be possible to turn the normal error off if you want to test the ideal situation - our code permits this.
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Goumindong

  • Admiral
  • *****
  • Posts: 1896
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #328 on: December 24, 2022, 10:18:47 PM »

Ahh that makes sense. I was reading the normal distribution as the spread when catching up when it was error.
Logged

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #329 on: December 26, 2022, 02:19:05 AM »

All right, getting back to this, here you go, this is in my approximation of programmer style going upward in levels of abstraction instead of in order of math. I feel like I don't understand my own code anymore, now that it is presented this way as opposed to the logical order. However, here it is and I even found a small error in the process (a lack of division by 2 for radius). I also included sample runs with randomly generated guns so that you can test that the implementation is correct. Code is here and also as an attachment. (edit: fix some typos and add underscore to a thing)

Code
#Objective: calculate the optimum angle to place the enemy ship, with regard to our ship
#Definition: angle exactly to the right is 0 degrees, so the ship is heading towards +90 degrees.
#Possible angles are integers, from -179 to 180.
#Assumptions: our ship is pointlike. The enemy ship is at a constant range. The enemy ship's orientation
#is tangential to the circle defined by our ship's position and range. The enemy ship is represented
#by a single line of hittable armor cells.
#Additionally, we assume sufficient range that we do not need to compensate for curvature (that range
#to the enemy ship's cells varies) In short: we assume that it is the case that the secant such that
#the average range is range is approximately the same as the arc and that inaccuracies that result
#from using the average instead of cell-specific range are insignificant.


#Section 0. Test ship and weapons. We will test against a ship formatted in the normal format
ship <- c(14000, 500, 10000, 1500, 220, 12, 440, 1.0, 200)

#We will create a data frame containing 18 weapons for testing.
#Weapons are formatted as "name", dps, facing, tracking arc, spread
#first, 6 weapons with easy parameters to parse
weapon1 <- c("right phaser", 100, -10, 20, 5)
weapon2 <- c("left phaser", 100, 10, 20, 5)
weapon3 <- c("pd gun 1", 30, -160, 20, 0)
weapon4 <- c("pd gun 2",30, 180, 20, 0)
weapon5 <- c("pd gun 3",30, 160, 20, 0)
weapon6 <- c("photon torpedo", 120, 90, 0, 5)

#then, 12 weapons with randomly generated data
#the generation process was c(round(100*runif(1)),round(180*runif(1,-1,1)),round(180*(1-log10(runif(1,1,10)))),round(30*runif(1)))
weapon7 <- c("bb gun",5,-150,11,20)
weapon8 <- c("space marine teleporter", 78,69,173,29)
weapon9 <- c("turbolaser", 92,122,111,9)
weapon10 <- c("hex bolter", 24,-136,38,20)
weapon11 <- c("singularity projector", 95,28,122,25)
weapon12 <- c("subspace resonance kazoo", 68,-139,12,2)
weapon13 <- c("left nullspace projector", 10,28,54,0)
weapon14 <- c("telepathic embarrassment generator", 30,-31,35,8)
weapon15 <- c("perfectoid resonance torpedo", 34,72,10,17)
weapon16 <- c("entropy inverter gun",78,-60,13,24)
weapon17 <- c("mini-collapsar rifle", 27,28,16,13)
weapon18 <- c("false vacuum tunneler", 32,78,157,20)

#store all weapons in a data frame - this part should be replaced with however the final integrated program
#handles weapons
no_weapons <- 18
weapons <- data.frame()
for (i in 1:no_weapons) weapons <- rbind(weapons,get(paste("weapon",i,sep="")))
colnames(weapons) <- c("name","damage","facing","trackingrange","spread")

#Section 1. functions of variables

#In this and following sections, say
#1. name of function
#2. input of function
#3. mathematical statement
#4. output of function


#1. G
#2. a real number y
#3. y*Phi(y)+phi(y), where Phi is the cdf and phi is the PDF of a standard normal dist
#4. =#3.
G <- function(y) return(y*pnorm(y) + dnorm(y))

#1. Hit probability of coordinate less than x
#2. A real number z, a standard deviation a of a normal distribution N(0,a),
#a parameter b of a symmetric uniform distribution (-b/2,b/2)
#3. if a > 0 and b > 0, a/2b * ( G(z/a + b/a) - G(z/a - b/a) )
#if a > 0 and b = 0, Phi(z)
#if b > 0 and a = 0, the integral of -infinity to x over a
#function corresponding to a rectangle with base from -b/2 to b/2 of area 1 ie. max(0, min(1, b/2 + z))
#if a = 0 and b = 0, 0 if z < 0 and 1 otherwise
#4. cumulative distribution function of the probability distribution of hits at z (CDF(z))

hit_probability_coord_lessthan_x <- function(z, a, b){
  if(a > 0 & b > 0) return(a/2/b*(G(z/a+b/a)-G(z/a-b/a)))
  if(a > 0 & b == 0) return(pnorm(z,0,a))
  if(a == 0 & b > 0) return(max(0,min(1,b/2+z)))
  if(a == 0 & b == 0) {
    if(z < 0) return(0) else return(1)
  }
}

#1. Degrees to arc
#2. A radius and an angle
#3. 2*pi*r * degrees/360
#4. the arc corresponding to the central angle of the circle defined by radius
deg_to_arc <- function(deg, radius) return(deg*2*radius/360*pi)


#1. Arc to degrees
#2. An arc and a radius
#3. arc / (2*pi*r) * 360
#4. The central angle corresponding to an arc of the circle defined by radius

arc_to_deg <- function(arc, radius) return(arc/2/radius*360/pi)

#1. Min and max mean
#2. A weapon, containing the columns facing, spread and tracking_range at [3],[4],[5]
#3. If spread < tracking_range,
#a vector ( facing + tracking_range/2 - spread / 2, facing - tracking_range/2 + spread / 2 )
#Else the vector ( facing, facing )
#4. Given a weapon, the maximum and minimum means of the probability distribution that it can achieve
#by rotating

min_max_mean <- function(weapon){
  if(weapon[5] < weapon[4]){
    return(c(
      weapon[3]-weapon[4]/2+weapon[5]/2,
      weapon[3]+weapon[4]/2-weapon[5]/2
    ))
  } else {
    return(c(
      weapon[3], weapon[3]
    ))
  }
}


#1. Transform hit coordinate
#2. A weapon, containing the columns min_mean and max_mean, and an angle
#3. max( minmean, min( maxmean, angle ) )
#4. Given that angle is the angle of the target relative to our ship, output is the angle that the weapon
#will assume as it tries to target the target

transform_hit_coord <- function(angle, weapon) return(max(weapon$min_mean, min(weapon$max_mean,angle)))

#1. Generate ship upper bounds
#2. A vector representing a ship, with width stored at [5] and number of cells stored at [6], and a range
#3. algorithm: 1. calculate width of a ship cell, in angles
#              2. the lowest bound is -ship width
#              3. the next bound after any given bound is previous bound + increment
#              4. there are equally many such next bounds as the ship has cells
#                 calculate them successively by applying 3.
#              5. convert the vector to pixels multiplying it by 2*pi*range
#4. A vector with the lower edge of the ship in index 1 and upper bounds of all its armor cells at
#successive indices
generate_ship_upper_bounds <- function(ship, range){
  ship_angle <- ship[5]/(2* pi *range)
  cell_angle <- ship_angle/ship[6]
  angles <- vector(mode="double", length = ship[6]+1)
  angles[1] <- -ship_angle/2
  for (i in 1:(length(angles)-1)) angles[i+1] <- angles[i]+cell_angle
  return(angles * 2 * pi * range)
}

#Section 2. functions of functions and variables

#1. Transformed angle
#2. A weapon, containing the columns min_mean and max_mean, and an angle
#3. angle - transform_hit_coord(angle, weapon)
#4. Given that angle is the angle of the target relative to our ship (beta in illustration),
#output is the angle of the target relative to to the weapon (angle alpha in illustration)

transformed_angle <- function(angle, weapon) return(angle-transform_hit_coord(angle,weapon))

#1. Weapon adjustment (pixels)
#2. A weapon and an angle (should be used for the optimum angle)
#3. deg_to_arc(transformed_angle)
#4. Returns the arc from weapon distribution mean to target

weaponadjustment_px <- function(weapon, optimum_angle,range){
  angle_difference <- transformed_angle(optimum_angle,weapon)
  arc <- deg_to_arc(angle_difference,range)
  return(arc)
}


#1. Hit distribution
#2. A vector of upper bounds, a standard deviation a of a normal distribution N(0,a),
#a parameter b of a symmetric uniform distribution (-b/2,b/2)
#3. will not present the detailed calculations for this function, as this section is to be replaced
#by the hit distribution function of the integrated code and is not intended for translation
#4. A vector of probability masses, such that cell 1 is chance to hit below lowest upper bound,
#the last cell is chance to hit above highest upper bound, and the others are the probabilities for
#hits between upper bounds


hit_distribution <- function(upper_bounds, standard_deviation, spread){
  vector <- vector(mode="double", length = length(upper_bounds)+1)
  if (standard_deviation == 0){
    if (spread == 0){
      vector[1] <- 0
      for (j in 2:(length(upper_bounds))) {
        #if both spread and standard deviation are 0 then all shots hit 1 cell. this should be so even if
        #the ship has an even number of cells to prevent ships with even no. cells appearing tougher which is not
        #the case in the real game most likely
        if ((upper_bounds[j] >= 0) & (upper_bounds[j-1] < 0)) vector[j] <- 1
      }
      #return part of a box
    } else {
      vector[1] <- min(1,max(0,(upper_bounds[1]+spread))/(2*spread))
      for (j in 2:(length(upper_bounds))) vector[j] <- min(1,max(0,(upper_bounds[j]+spread))/(2*spread)) - min(1,max(0,(upper_bounds[j-1]+spread))/(2*spread))
      vector[length(upper_bounds)+1] <- 1-min(1,max(0,(upper_bounds[length(upper_bounds)]+spread))/(2*spread))
    }
  } else {
    if (spread != 0){
      vector[1] <- hit_probability_coord_lessthan_x(upper_bounds[1], standard_deviation, spread)
      for (j in 2:(length(upper_bounds))) vector[j] <- (hit_probability_coord_lessthan_x(upper_bounds[j], standard_deviation, spread)-hit_probability_coord_lessthan_x(upper_bounds[j-1], standard_deviation, spread))
      vector[length(upper_bounds)+1] <- (1-hit_probability_coord_lessthan_x(upper_bounds[length(upper_bounds)], standard_deviation, spread))
    } else {
      #if spread is 0 but standard deviation is not 0 we have a normal distribution
      vector[1] <- pnorm(upper_bounds[1],0,standard_deviation)
      for (j in 2:(length(upper_bounds))) vector[j] <- pnorm(upper_bounds[j],0,standard_deviation) - pnorm(upper_bounds[j-1], mean=0, sd=standard_deviation)
      vector[length(upper_bounds)+1] <- 1-pnorm(upper_bounds[length(upper_bounds)], mean=0, sd=standard_deviation)
    }
   
  }
  return(vector)
}

#1. Hit distribution at optimum angle
#2. The optimum angle, the standard error in pixels, a weapon with spread at index 5, a vector of upper bounds
#3. hit distribution for vector of upper bounds + arc from weapon to upper bound
#4. A vector of probability masses, such that cell 1 is chance to hit below lowest upper bound,
#the last cell is chance to hit above highest upper bound, and the others are the probabilities for
#hits between upper bounds, adjusted for ship location

hit_distribution_at_optimum_angle <- function(angle, sd, upper_bounds, weapon, range){
  #convert spread to pixels
  px_spread <- deg_to_arc(weapon[,5], range)
  #adjust upper bound vector
  adj_ubs <- upper_bounds + weaponadjustment_px(weapon, angle,range)
  return(hit_distribution(adj_ubs,sd,px_spread))
}

#1. Summed area under curve
#2. An angle, the standard deviation of the normal distribution in pixels, a set of weapons, and a ship
# s.t. ship width is stored at index 5, weapon damage at index 2 and weapon spread at index 5
#3. Algorithm: convert ship width to degrees and
#              convert standard error to degrees, for consistency in units
#              for each weapon, calculate the angles of the ship's lower bound and upper bound
#              relative to that gun
#              then calculate dps * (CDF of probability distribution of that weapon at upper bound
#              - CDF of probability distribution of that weapon at lower bound)
#4. Output: the sum of expected dps from this set of weapons to target ship at that angle relative to our ship

sum_auc <- function(angle, sd, weapons, ship, range) {
  summed_auc <- 0
  #convert the ship's width from segment to degrees
  shipwidth <- arc_to_deg(ship[5], range)/2
  d_error <- arc_to_deg(sd, range)
 
  for (i in 1:length(weapons[,1])){
    #angle of the ship's upper bound, in coordinates of the distribution mean
    #note that this is weapon specific
    #as a sanity check, furthermore, angle cannot be below -359 or above +360
    ship_upper_bound <- transformed_angle(angle,weapons[i,])+shipwidth/2
    ship_lower_bound <- transformed_angle(angle,weapons[i,])-shipwidth/2
   
    damage <- weapons[i,2]
    spread <- weapons[i,5]
    #now we calculate the sum
    summed_auc <- summed_auc + damage*(
      hit_probability_coord_lessthan_x(ship_upper_bound, d_error, spread) -
        hit_probability_coord_lessthan_x(ship_lower_bound, d_error, spread)
    )
  }
 
  return(summed_auc)
}

#Section 3. functions of functions of functions and variables

#1. main
#2. a ship, a range, a standard deviation, and a list of weapons
#3. described in comments in the function
#4. prints, for each weapon, the probability of a hit at each of the ship's cells, as well as
#of a miss due to hitting below ship's lowest bound in cell 1 and of a miss due to hitting above
#ship's highest bound in the last cell
main <- function(ship, range, sd, weapons){
  # 1. we were given a list of weapons with names etc. so formulate the list with proper types and
  # with room for min and max means
  weapons <- data.frame(name=weapons[,1], damage=as.double(weapons[ ,2]), facing=as.double(weapons[ ,3]),
                        tracking_arc=as.double(weapons[ ,4]),spread=as.double(weapons[ ,5]),min_mean=0,max_mean=0)
  # compute min and max means for weapons
 
  for (i in 1:length(weapons[,1])) {
    weapons[i, 6] <- min_max_mean(weapons[i, ])[1]
    weapons[i, 7] <- min_max_mean(weapons[i, ])[2]
  }

  #now, for angles -359 to 360 (all possible signed angles, calculate dps)
  angles <- seq(-359,360)
 
  dps_at_angles <- angles
  for (i in 1:720) {
    dps_at_angles[i] <- sum_auc(dps_at_angles[i], sd, weapons, ship, range)
  }
 
 
  #now, note that angle -180 is just angle 180, angle -359 is angle 1, and so on, so
  #these must be summed with angles -179 to 180
  for (i in 1:180) dps_at_angles[i+360] <- dps_at_angles[i+360]+dps_at_angles[i]
  #likewise note that angle 360 is just angle 0, angle 359 is angle -1, and so on
  for (i in 540:720) dps_at_angles[i-360] <- dps_at_angles[i-360]+dps_at_angles[i]
 
  #having summed, select angles -179 to 180
  dps_at_angles <- dps_at_angles[181:540]
 
  #we use a separate vector to keep track of angle, since vector index 1 corresponds to angle -179 now
  x_axis <- seq(-179,180)
 
  #find the optimum angle by selecting the midmost of those cells that have the highest dps,
  #and from the vector x_axis the angle corresponding to that cell
  #use rounding to avoid errors from numerical math
  optimum_angle <- x_axis[which(round(dps_at_angles,3) == round(max(dps_at_angles),3))
                        [ceiling(length(which(round(dps_at_angles,3) == round(max(dps_at_angles),3)))/2)]]
 
  #calculate ship upper bounds
  upper_bounds <- generate_ship_upper_bounds(ship, range)
 
  #calculate and report the distributions for weapons, round for human readability
  for (i in 1:length(weapons[,1])){
    print(paste0(weapons[i,1],":"))
    print(round(hit_distribution_at_optimum_angle(optimum_angle,sd,upper_bounds,weapons[i,],range),3))
  }
 
  #testing section - not to be implemented in final code
  #print a graph of the distribution and our choice of angle
  print(weapons)
  plot(dps_at_angles, x=x_axis)
  abline(v=optimum_angle)
 
}

#sample runs
main(ship, 1000, 50, weapons[1:6,])

main(ship, 1000, 50, weapons[7:12,])

main(ship, 1000, 50, weapons[13:18,])

Results for sample runs:
Run 1



> main(ship, 1000, 50, weapons[1:6,])
[1] "right phaser:"
 [1] 0.184 0.076 0.087 0.094 0.096 0.095 0.089 0.079 0.066 0.051 0.036 0.023 0.013 0.012
[1] "left phaser:"
 [1] 0.012 0.013 0.023 0.036 0.051 0.066 0.079 0.089 0.095 0.096 0.094 0.087 0.076 0.184
[1] "pd gun 1:"
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "pd gun 2:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
[1] "pd gun 3:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
[1] "photon torpedo:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
            name damage facing tracking_arc spread min_mean max_mean
1   right phaser    100    -10           20      5    -17.5     -2.5
2    left phaser    100     10           20      5      2.5     17.5
3       pd gun 1     30   -160           20      0   -170.0   -150.0
4       pd gun 2     30    180           20      0    170.0    190.0
5       pd gun 3     30    160           20      0    150.0    170.0
6 photon torpedo    120     90            0      5     90.0     90.0


Run 2



> main(ship, 1000, 50, weapons[7:12,])
[1] "bb gun:"
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "space marine teleporter:"
 [1] 0.391 0.018 0.018 0.018 0.018 0.018 0.018 0.018 0.018 0.018 0.018 0.018 0.018 0.391
[1] "turbolaser:"
 [1] 0.165 0.051 0.054 0.056 0.058 0.058 0.058 0.058 0.058 0.058 0.056 0.054 0.051 0.165
[1] "hex bolter:"
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "singularity projector:"
 [1] 0.444 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.304
[1] "subspace resonance kazoo:"
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
                      name damage facing tracking_arc spread min_mean max_mean
1                   bb gun      5   -150           11     20   -150.0   -150.0
2  space marine teleporter     78     69          173     29     -3.0    141.0
3               turbolaser     92    122          111      9     71.0    173.0
4               hex bolter     24   -136           38     20   -145.0   -127.0
5    singularity projector     95     28          122     25    -20.5     76.5
6 subspace resonance kazoo     68   -139           12      2   -144.0   -134.0



Run 3



> main(ship, 1000, 50, weapons[13:18,])
[1] "left nullspace projector:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
[1] "telepathic embarrassment generator:"
 [1] 0.115 0.048 0.055 0.060 0.063 0.064 0.065 0.065 0.065 0.064 0.063 0.059 0.054 0.160
[1] "perfectoid resonance torpedo:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
[1] "entropy inverter gun:"
 [1] 0.681 0.022 0.022 0.022 0.022 0.022 0.022 0.022 0.022 0.022 0.021 0.020 0.019 0.062
[1] "mini-collapsar rifle:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
[1] "false vacuum tunneler:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
                                name damage facing tracking_arc spread min_mean max_mean
1           left nullspace projector     10     28           54      0      1.0     55.0
2 telepathic embarrassment generator     30    -31           35      8    -44.5    -17.5
3       perfectoid resonance torpedo     34     72           10     17     72.0     72.0
4               entropy inverter gun     78    -60           13     24    -60.0    -60.0
5               mini-collapsar rifle     27     28           16     13     26.5     29.5
6              false vacuum tunneler     32     78          157     20      9.5    146.5



[attachment deleted by admin]
« Last Edit: December 26, 2022, 03:39:41 AM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge
Pages: 1 ... 20 21 [22] 23 24 ... 32