Fractal Softworks Forum

Please login or register.

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

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

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #300 on: December 20, 2022, 06:33:46 AM »

Well, this fleet is working fine, admittedly the frigates do take losses and the station did tank for the first fights before being pounded to space dust.




Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

intrinsic_parity

  • Admiral
  • *****
  • Posts: 3071
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #301 on: December 20, 2022, 08:13:13 AM »

It was never valid to model a ship as a tangential line. I always assumed that was just a simplifying assumption to easily make sure things work while we were developing code. Ships have weird geometry that results in corners and stuff, so regardless of what assumptions you have about the firing vessel, you're going to have to deal with non-trivial armor grids at some point.
Logged

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #302 on: December 20, 2022, 09:34:52 AM »

I mean there are degrees of valid. If the ship is pointlike then the inaccuracy is not in fact that the target ship corresponds to a line (at, say, range 1000, a difference of 10 px in range would increase our sd of 50 px to 50.5 px, not huge) but rather that even if you project the more complex structure of a ship to a line - which you might, it's essentially just find the right widths for hittable cells - then more complex rules than just adjacent in line might exist between those cells to pool damage.

To completely realistically model the target ship we would need the ship's exact shape, then trace the radii from each gun to see which cells are hittable, and have a map of how the ship's armor works to see which cells distribute damage to which other cells. This is highly ambitious.

A more limited solution is project the ship's shape to the arc of the circle defined by weapon and range. Then we still account for shape but not the relationship of cells to one another.

A still more limited solution is make the ship a box with two dimensions and use the solution above, in which case we do not need the ship's shape.

Finally there's this.

How deep we want to go depends again on our goals and what is valid is relative. For example, it is not in and of itself invalid to test a weapon against a slab of armor to see how it does vs. a tank, just so long as you are aware of your assumptions.

I'd like to hear what Liral thinks - Liral, since you are doing the heavy lifting wrt programming, let me know what you think is best, the mathematical model for all is doable. Programming might be non trivial though.
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 #303 on: December 20, 2022, 03:28:04 PM »

Thanks for the comments and code reorganization.  My remaining requests for your next code release are to organize it not from beginning to end (as a proof would be) but from top to bottom level of abstraction as a directed acyclic graph would be, if each node could nest a directed acyclic graph with a single point of entry and single point of exit.  It is not only easier to translate but also to then call from other code, treating the whole program as one node on a larger graph, and so on, in an elegant hierarchical network of nodes and edges.

1. Put all 'driver' code that defines variables or calls functions in a main() function at the bottom of the program.
Code: Right
impress = function(argument) { ... }


procrastinate = function(otherArgument) { ... }


main = function() {
    argument = "professor"
    otherArgument = "writing a grant proposal"
    impress(argument)
    procrastinate(otherArgument)
}
Code: Wrong
impress = function(argument) { ... }


argument = "professor"
impress(argument)


procrastinate = function(otherArgument) { ... }


otherArgument = "writing a grant proposal"
procrastinate(otherArgument)

2. Global constants but no global variables.
Code: Right
SUBMISSION_DURATION = 20#minutes to drop off papers (ALL CAPS means constant)


isEnoughTimeLeft = function(assignmentDuration, currentTime, deadline) {
    return(deadline - assignmentDuration - SUBMISSION_DURATION > currentTime)
}


main = function() {
    assignmentDuration = 10#minutes
    currentTime = 180#3:00AM
    deadline = 540#9:00AM
    print(isEnoughTimeLeft(assignmentDuration, currentTime, deadline))
}
Code: Wrong
submission_duration = 20#minutes to drop off papers (not ALL CAPS means variable)


isEnoughTimeLeft = function(assignmentDuration, currentTime, deadline) {
    return(deadline - assignmentDuration - submission_duration > currentTime)
}


main = function() {
    assignmentDuration = 10#minutes
    currentTime = 180#3:00AM
    deadline = 540#9:00AM
    print(isEnoughTimeLeft(assignmentDuration, currentTime, deadline))
}
Code: Especially Wrong
distance = 2#kilometers from dorm to offices
speed = 0.1#kilometer per minute sleepy trudge in bad weather


submissionDuration = function(){
    return distance / speed
}


submission_duration = submissionDuration()#minutes to drop off papers


isEnoughTimeLeft = function(assignmentDuration, currentTime, deadline) {
    return(deadline - assignmentDuration - submission_duration > currentTime)
}


main = function() {
    assignmentDuration = 10#minutes
    currentTime = 180#3:00AM
    deadline = 540#9:00AM
    print(isEnoughTimeLeft(assignmentDuration, currentTime, deadline))
}

Also, please
- write names out rather than abbreviating them
- use the plural 'somethings' rather than 'vector' or 'something_vector'
- use two names for two things; e.g., spread_distance for distance and spread_angle for angle vs spread for both
- do not writemultiwordnameswithoutcamelcaseorunderscoresbecausetheyarehardtounderstandw henwrittenthus. :p

The results seem better this time, though I still can't make the full code work.  I hope you could look it over.

Code
Code
import math
from statistics import NormalDist
"""
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.
"""
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 the normal distribution
    uniform_distribution_radius - radius of the 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 probability_hit_between_bounds(
        lower_bound: float,
        upper_bound: float,
        error_distance: float,
        spread_distance: float) -> float:
    """
    The probability to hit between the left and right edges of the
    ship is the integral, from the lower edge of the ship to the upper one,
    of the hit probability distribution. By the fundamental theorem of
    calculus, this integral equals
   
    CDF(upper edge) - CDF(lower edge).
    """
    return (probability_hit_before_bound(upper_bound, error_distance,
                                         spread_distance)
            - probability_hit_before_bound(lower_bound, error_distance,
                                           spread_distance))

"""
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.
"""
def arc_to_deg(arc: float, distance: float) -> float:
    """
    Return the degree angle of an arc.
    """
    return arc / distance * 360 / math.pi


def deg_to_arc(degrees: float, distance:  float) -> float:
    """
    Return the arc of a degree angle.
    """
    return degrees * distance / 360 * math.pi
   

def probability_hit_at_facing(
        weapon_facing: float,
        spread_distance: float,
        error_distance: float,
        target_facing: float,
        target_radius: float,
        distance: float) -> float:
    upper_bound = deg_to_arc(weapon_facing + target_radius, distance)
    lower_bound = deg_to_arc(weapon_facing - target_radius, distance)
    return probability_hit_between_bounds(upper_bound, lower_bound,
                                          error_distance, spread_distance)

"""
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)
"""

def transformed_hit_coord(
        angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    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.
    """
    return angle - transformed_hit_coord(angle, minimum_mean, maximum_mean)


def total_hit_probability(
        minimum_mean: float,
        maximum_mean: 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.
   
    Return the sum of areas under the probability distribution curve across
    the target, equivalent to integrating, from ship lower angle to ship
    greater angle, the probability distribution times damage for each
    weapon. We use the CDF we described above.
   
    We refer to auc (area under curve, ie. the curve of damage times
    probability distribution, giving total damage) for short.
   
    target_facing - target ship orientation
    Output: total damage to target ship
    """
    #we have defined spread in degrees, so we must convert it to
    #pixels to be consistent
   
    #angle of the ship's upper bound, in coordinates of the
    #distribution mean; note that this is weapon specific
    weapon_facing = transformed_angle(target_facing, minimum_mean, maximum_mean)
    return probability_hit_at_facing(weapon_facing, spread_distance,
                                     error_distance, target_facing,
                                     target_radius, distance)


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


def hit_distribution(bounds, standard_deviation, spread):
    """Our normal hit distribution function."""
    if standard_deviation == 0:
        if spread == 0:
            #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
            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:
        #if spread is 0 but standard deviation is not 0 we have a normal distribution
        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:
    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():
    #Section 1. general considerations
    #We will 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.
   
    target_radius = 220 #(14000, 500, 10000, 1500, 220, 12, 440, 1.0, 200)
    cell_count = 12
    distance = 1000
   
    #For testing, we will give these parameters for weapons:
    #damage, facing (deg), tracking range (deg), spread
    #collect them in a data frame
   
    #for this test we will add two weapons that are slightly angled
    #away from each other and able to overlap in the exact middle
    # and some rear mounted point defense weapons to make sure the
    #wraparound, extreme angles are correct
    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:
        weapon["spread distance"] = deg_to_arc(weapon["spread"], distance)
        weapon["minimum_mean"] = (
            weapon["facing"] + (weapon["arc"] + weapon["spread distance"]) / 2)
        weapon["maximum_mean"] = (
            weapon["facing"] + (weapon["arc"] - weapon["spread distance"]) / 2)

    #We will use the same parameter for the normal distribution as
    #we do in the other modules. Note that this results in the SD of
    #the normal distribution being in pixels.
   
    error_standard_deviation = 0.05
    error_distance = error_standard_deviation * distance
   
    #Now that we have all the pieces, we can perform the calculation.
    #A special consideration is that we are using signed angles for the
    #probability distritbutions, but in reality, the ship's guns wrap
    #around, so we must devise a way to map -359 to 1, etc.
   
    #So first, we define a vector from -360 to 360. These all possible
    #angles of fire, since a gun can only have a position from -180 to
    #180 degrees on the ship, and can only track 360 degrees at most.
    damage_per_second_total_at_angles = [
        sum(weapon["damage"]
            * total_hit_probability(weapon["minimum_mean"],
                                    weapon["maximum_mean"],
                                    weapon["spread distance"],
                                    error_distance,
                                    target_facing,
                                    target_radius,
                                    distance) for weapon in weapons)
        for target_facing in range(-359,361)]
   
    i = 0
    for target_facing, damage_per_second_total in zip(range(-359,360),
        damage_per_second_total_at_angles):
        print(i, target_facing, damage_per_second_total)
        i += 1
   
    #next, we go back to one loop of the ship by noting that
    #-180 corresponds to +180
    #-181 corresponds to +179 etc, so 360 + index for indices 1:180
    for i in range(180):
        damage_per_second_total_at_angles[i+360] += (
            damage_per_second_total_at_angles[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_total_at_angles[i-360] += (
        damage_per_second_total_at_angles[i])
   
    #finally, to get angles -179 to 180, we select indices 181 to 540 of the new vector.
    damage_per_second_total_at_angles = damage_per_second_total_at_angles[181:540]
    #note that vector indices no longer correspond to angles, rather vector index 1 corresponds to -179.
    #to get a correct plot add this
   
    xaxis = range(-179,180)
   
    #the problem is solved. Now we can find the optimum angle quite simply by selecting 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)
   
    #print results
    #for weapon in weapons: print(hit_distribution_at_optimum_angle(weapon))
main()
[close]
Result
0 -359 -58.45736069719945
1 -358 -59.369949169358
2 -357 -60.200197697714025
3 -356 -60.97017782938062
4 -355 -61.69793197100172
5 -354 -62.397492777221025
6 -353 -63.079284709317186
7 -352 -63.75072482594253
8 -351 -64.41687770529128
9 -350 -65.08106701768881
10 -349 -65.74539113596782
11 -348 -66.41112517767839
12 -347 -67.07901479604112
13 -346 -67.74947901771868
14 -345 -68.42274327167173
15 -344 -69.09892258032747
16 -343 -69.77807130504617
17 -342 -70.46021164938345
18 -341 -71.14534933453314
19 -340 -71.83348188450461
20 -339 -72.52460283929805
21 -338 -73.2187038180251
22 -337 -73.915775492474
23 -336 -74.61580803009397
24 -335 -75.31879128844636
25 -334 -76.02471489756628
26 -333 -76.73356829362154
27 -332 -77.44534073218146
28 -331 -78.16002129324735
29 -330 -78.87759888308253
30 -329 -79.59806223483477
31 -328 -80.3213999087391
32 -327 -81.04760029216706
33 -326 -81.77665159964289
34 -325 -82.50854187284878
35 -324 -83.24325898063478
36 -323 -83.98079061904369
37 -322 -84.72112431133871
38 -321 -85.46424740805138
39 -320 -86.21014708702822
40 -319 -86.95881035350219
41 -318 -87.71022404016581
42 -317 -88.4643748072612
43 -316 -89.22124914267398
44 -315 -89.98083336204925
45 -314 -90.74311360890879
46 -313 -91.50807585478374
47 -312 -92.27570589936325
48 -311 -93.04598937064402
49 -310 -93.81891172510204
50 -309 -94.59445824786832
51 -308 -95.37261405291996
52 -307 -96.1533640832804
53 -306 -96.93669311123327
54 -305 -97.72258573854823
55 -304 -98.51102639671251
56 -303 -99.30199934718382
57 -302 -100.09548868164558
58 -301 -100.89147832228139
59 -300 -101.68995202205245
60 -299 -102.49089336499591
61 -298 -103.29428576652893
62 -297 -104.10011247376839
63 -296 -104.90835656585223
64 -295 -105.71900095429191
65 -294 -106.53202838331481
66 -293 -107.34742143023345
67 -292 -108.16516250582131
68 -291 -108.98523385469647
69 -290 -109.8076175557257
70 -289 -110.6322955224343
71 -288 -111.459249503426
72 -287 -112.2884610828195
73 -286 -113.11991168069714
74 -285 -113.95358255355853
75 -284 -114.78945479479384
76 -283 -115.62750933516183
77 -282 -116.46772694328584
78 -281 -117.31008822616023
79 -280 -118.15457362966131
80 -279 -119.00116343908174
81 -278 -119.84983777966727
82 -277 -120.70057661717132
83 -276 -121.55335975841447
84 -275 -122.40816685186486
85 -274 -123.2649773882205
86 -273 -124.12377070101286
87 -272 -124.98452596721174
88 -271 -125.8472222078524
89 -270 -126.71183828866641
90 -269 -127.57835292072713
91 -268 -128.446744661108
92 -267 -129.31699191355037
93 -266 -130.18907292914292
94 -265 -131.06296580701405
95 -264 -131.93864849503768
96 -263 -132.81609879054324
97 -262 -133.6952943410456
98 -261 -134.5762126449803
99 -260 -135.45883105245719
100 -259 -136.34312676601658
101 -258 -137.22907684140225
102 -257 -138.1166581883493
103 -256 -139.00584757137256
104 -255 -139.89662161057836
105 -254 -140.78895678247764
106 -253 -141.6828294208199
107 -252 -142.5782157174279
108 -251 -143.47509172305405
109 -250 -144.37343334823848
110 -249 -145.27321636418537
111 -248 -146.1744164036494
112 -247 -147.07700896182453
113 -246 -147.98096939726236
114 -245 -148.88627293277833
115 -244 -149.7928946563884
116 -243 -150.7008095222451
117 -242 -151.60999235159187
118 -241 -152.5204178337192
119 -240 -153.43206052694512
120 -239 -154.344894859588
121 -238 -155.2588951309698
122 -237 -156.17403551241628
123 -236 -157.0902900482721
124 -235 -158.007632656928
125 -234 -158.92603713185605
126 -233 -159.84547714265724
127 -232 -160.7659262361189
128 -231 -161.68735783728175
129 -230 -162.6097452505166
130 -229 -163.53306166061626
131 -228 -164.45728013389117
132 -227 -165.38237361927887
133 -226 -166.3083149494608
134 -225 -167.23507684199754
135 -224 -168.162631900459
136 -223 -169.09095261557832
137 -222 -170.02001136641212
138 -221 -170.94978042150268
139 -220 -171.88023194006425
140 -219 -172.81133797316508
141 -218 -173.74307046492513
142 -217 -174.67540125372966
143 -216 -175.6083020734377
144 -215 -176.54174455461347
145 -214 -177.47570022576
146 -213 -178.4101405145656
147 -212 -179.3450367491547
148 -211 -180.28036015935646
149 -210 -181.2160818779714
150 -209 -182.15217294205715
151 -208 -183.08860429421733
152 -207 -184.02534678389958
153 -206 -184.9623711687059
154 -205 -185.89964811570957
155 -204 -186.83714820277822
156 -203 -187.77484191991044
157 -202 -188.71269967057765
158 -201 -189.65069177307478
159 -200 -190.58878846188088
160 -199 -191.52695988902494
161 -198 -192.46517612546444
162 -197 -193.4034071624682
163 -196 -194.3416229130076
164 -195 -195.27979321315752
165 -194 -196.21788782350583
166 -193 -197.15587643056745
167 -192 -198.09372864820662
168 -191 -199.03141401907124
169 -190 -199.9689020160291
170 -189 -200.90616204361555
171 -188 -201.8431634394852
172 -187 -202.77987547587483
173 -186 -203.71626736107004
174 -185 -204.65230824088053
175 -184 -205.5879672001215
176 -183 -206.52321326410356
177 -182 -207.45801540012926
178 -181 -208.39234251899381
179 -180 -209.32616347649386
180 -179 -210.25944707494688
181 -178 -211.1921620647103
182 -177 -212.1242771457081
183 -176 -213.05576096897218
184 -175 -213.98658213817538
185 -174 -214.9167092111859
186 -173 -215.84611070161384
187 -172 -216.77475508037523
188 -171 -217.7026107772519
189 -170 -218.62964618246718
190 -169 -219.55582964825598
191 -168 -220.4811294904512
192 -167 -221.40551399006725
193 -166 -222.32895139489335
194 -165 -223.2514099210904
195 -164 -224.1728577547926
196 -163 -225.09326305371442
197 -162 -226.01259394876467
198 -161 -226.9308185456617
199 -160 -227.8479049265531
200 -159 -228.76382115164574
201 -158 -229.67853526083272
202 -157 -230.59201527532798
203 -156 -231.50422919930892
204 -155 -232.41514502155488
205 -154 -233.32473071709532
206 -153 -234.2329542488634
207 -152 -235.13978356934743
208 -151 -236.04518662224956
209 -150 -236.9491313441512
210 -149 -237.85158566617193
211 -148 -238.75251751564429
212 -147 -239.6518948177836
213 -146 -240.5496854973605
214 -145 -241.44585748038142
215 -144 -242.34037869577122
216 -143 -243.23321707705148
217 -142 -244.12434056403214
218 -141 -245.0137171044976
219 -140 -245.90131465589857
220 -139 -246.78710118704538
221 -138 -247.67104467980528
222 -137 -248.55311313079756
223 -136 -249.43327455309586
224 -135 -250.31149697792904
225 -134 -251.18774845638532
226 -133 -252.06199706111573
227 -132 -252.934210888042
228 -131 -253.8043580580638
229 -130 -254.6724067187691
230 -129 -255.53832504614314
231 -128 -256.4020812462807
232 -127 -257.2636435570989
233 -126 -258.12298025005003
234 -125 -258.98005963183226
235 -124 -259.8348500461096
236 -123 -260.6873198752245
237 -122 -261.5374375419091
238 -121 -262.38517151100757
239 -120 -263.2304902911843
240 -119 -264.0733624366455
241 -118 -264.9137565488511
242 -117 -265.7516412782292
243 -116 -266.58698532589324
244 -115 -267.4197574453538
245 -114 -268.24992644423236
246 -113 -269.07746118597527
247 -112 -269.90233059156435
248 -111 -270.72450364122614
249 -110 -271.54394937614586
250 -109 -272.3606369001715
251 -108 -273.17453538152296
252 -107 -273.98561405449686
253 -106 -274.793842221171
254 -105 -275.5991892531067
255 -104 -276.4016245930478
256 -103 -277.2011177566212
257 -102 -277.99763833403154
258 -101 -278.7911559917558
259 -100 -279.58164047423514
260 -99 -280.36906160556373
261 -98 -281.15338929117655
262 -97 -281.9345935195331
263 -96 -282.7126443637979
264 -95 -283.48751198351863
265 -94 -284.2591666263037
266 -93 -285.02757862949204
267 -92 -285.7927184218238
268 -91 -286.5545565251076
269 -90 -287.31306355589055
270 -89 -288.0682102271426
271 -88 -288.8199673499848
272 -87 -289.5683058355769
273 -86 -290.3131966974334
274 -85 -291.05461105494965
275 -84 -291.7925201401383
276 -83 -292.52689531260904
277 -82 -293.2577080949452
278 -81 -293.9849302567924
279 -80 -294.7085340110321
280 -79 -295.42849245850766
281 -78 -296.1447805633294
282 -77 -296.8573772177405
283 -76 -297.5662694570394
284 -75 -298.27146074652353
285 -74 -298.97298665868675
286 -73 -299.6709433782487
287 -72 -300.3655374493967
288 -71 -301.05716896872656
289 -70 -301.74656461589177
290 -69 -302.43498049318185
291 -68 -303.1244959183351
292 -67 -303.81841546878246
293 -66 -304.52178459448515
294 -65 -305.24200119420374
295 -64 -305.98947054457227
296 -63 -306.7782061059833
297 -62 -307.62623111678045
298 -61 -308.5555980660002
299 -60 -309.5918317361554
300 -59 -310.7626335113653
301 -58 -312.0957715008698
302 -57 -313.6162220378177
303 -56 -315.3428055146567
304 -55 -317.28473798479627
305 -54 -319.43865243779976
306 -53 -321.7866833366129
307 -52 -324.2961236236918
308 -51 -326.92095276055716
309 -50 -329.60523051294604
310 -49 -332.28801748966595
311 -48 -334.9091975444911
312 -47 -337.41540842931806
313 -46 -339.7652751056342
314 -45 -341.933282073403
315 -44 -343.91187314163335
316 -43 -345.71165947979495
317 -42 -347.35987721042983
318 -41 -348.89741239740624
319 -40 -350.3747872088267
320 -39 -351.84749540248197
321 -38 -353.3710309095495
322 -37 -354.9959164182856
323 -36 -356.76303885944697
324 -35 -358.69963556915485
325 -34 -360.81631927708236
326 -33 -363.10553569898116
327 -32 -365.5417715879919
328 -31 -368.0836545050916
329 -30 -370.67782514784125
330 -29 -373.2641706616861
331 -28 -375.7817552929423
332 -27 -378.1746427904863
333 -26 -380.3968169444016
334 -25 -382.4155753708854
335 -24 -384.2130575515565
336 -23 -385.7859018393075
337 -22 -387.143329995243
338 -21 -388.30416846356104
339 -20 -389.2933999728871
340 -19 -390.1387993713174
341 -18 -390.8680751248394
342 -17 -391.50675944046475
343 -16 -392.07691257395294
344 -15 -392.5965658721769
345 -14 -393.0797412454191
346 -13 -393.53685276111116
347 -12 -393.9753074492379
348 -11 -394.40016023091937
349 -10 -394.81472549416185
350 -9 -395.2210927061679
351 -8 -395.6205284570263
352 -7 -396.0137702519554
353 -6 -396.4012293503031
354 -5 -396.78312379557406
355 -4 -397.1595616077663
356 -3 -397.53059052995707
357 -2 -397.89622653268134
358 -1 -398.2564694904493
359 0 -398.6113114679912
360 1 -398.96074093447305
361 2 -399.3047448276244
362 3 -399.643309528301
363 4 -399.9764213044391
364 5 -400.3040665064467
365 6 -400.62623165049035
366 7 -400.9429034530526
367 8 -401.25406884507055
368 9 -401.55971497782207
369 10 -401.85982922557884
370 11 -402.1543991870327
371 12 -402.44341268627477
372 13 -402.72685777360033
373 14 -403.00472272625564
374 15 -403.2769960491439
375 16 -403.54366647552115
376 17 -403.80472296767334
377 18 -404.06015471757644
378 19 -404.30995114755046
379 20 -404.55410191089175
380 21 -404.7925968924944
381 22 -405.0254262094589
382 23 -405.2525802116901
383 24 -405.4740494824693
384 25 -405.6898248390279
385 26 -405.89989733309756
386 27 -406.1042582514454
387 28 -406.302899116401
388 29 -406.4958116863619
389 30 -406.68298795628556
390 31 -406.86442015813816
391 32 -407.04010076129475
392 33 -407.2100224727528
393 34 -407.37417823690123
394 35 -407.5325612340615
395 36 -407.68516487580115
396 37 -407.83198279198365
397 38 -407.9730087973988
398 39 -408.10823680967184
399 40 -408.2376606550592
400 41 -408.3612736256739
401 42 -408.47906750611907
402 43 -408.59103051053205
403 44 -408.6971430695585
404 45 -408.797369545281
405 46 -408.89164255588275
406 47 -408.97983447243803
407 48 -409.06170767350454
408 49 -409.1368313539642
409 50 -409.20444849615876
410 51 -409.26327303208905
411 52 -409.31119605241116
412 53 -409.3448837641385
413 54 -409.35926188015844
414 55 -409.3469040465198
415 56 -409.29737692010485
416 57 -409.1966393777124
417 58 -409.02664095721696
418 59 -408.7653024688745
419 60 -408.38707314865053
420 61 -407.86422679474225
421 62 -407.16897261927227
422 63 -406.2763158142576
423 64 -405.1674259310149
424 65 -403.8330935652938
425 66 -402.2767247591195
426 67 -400.5162849684169
427 68 -398.5846918039269
428 69 -396.52837018437947
429 70 -394.4039915817457
430 71 -392.27375632169577
431 72 -390.19986497417733
432 73 -388.2389897439809
433 74 -386.43755677114484
434 75 -384.82848537613944
435 76 -383.42974321294275
436 77 -382.24473900981627
437 78 -381.26426653676344
438 79 -380.4694990071914
439 80 -379.8354457628118
440 81 -379.33432065280556
441 82 -378.9384025995037
442 83 -378.622146449773
443 84 -378.3634791112029
444 85 -378.14435670506316
445 86 -377.9507451772072
446 87 -377.77221873881456
447 88 -377.6013590750034
448 89 -377.43310042203507
449 90 -377.2641179940685
450 91 -377.09231237214397
451 92 -376.91640746130616
452 93 -376.7356566990117
453 94 -376.5496402166749
454 95 -376.35813181015686
455 96 -376.1610157479243
456 97 -375.958237024904
457 98 -375.7497728585449
458 99 -375.5356170126942
459 100 -375.3157715117283
460 101 -375.0902424266894
461 102 -374.8590378114777
462 103 -374.6221667285759
463 104 -374.37963880537643
464 105 -374.13146403901885
465 106 -373.87765271332535
466 107 -373.6182153644306
467 108 -373.35316276679384
468 109 -373.0825059274625
469 110 -372.8062560835172
470 111 -372.52442470072594
471 112 -372.2370234726183
472 113 -371.9440643197114
473 114 -371.6455593887588
474 115 -371.3415210520185
475 116 -371.03196190650635
476 117 -370.71689477323093
477 118 -370.39633269643656
478 119 -370.0702889428044
479 120 -369.7387770006675
480 121 -369.4018105791926
481 122 -369.0594036075667
482 123 -368.71157023415196
483 124 -368.3583248256431
484 125 -367.99968196620546
485 126 -367.6356564565987
486 127 -367.2662633132913
487 128 -366.8915177675632
488 129 -366.5114352645921
489 130 -366.12603146252786
490 131 -365.7353222315596
491 132 -365.33932365296283
492 133 -364.9380520181453
493 134 -364.53152382766245
494 135 -364.11975579023965
495 136 -363.70276482177684
496 137 -363.28056804433027
497 138 -362.85318278510056
498 139 -362.4206265753949
499 140 -361.9829171495787
500 141 -361.54007244403164
501 142 -361.0921105960647
502 143 -360.6390499428471
503 144 -360.18090902031895
504 145 -359.7177065620866
505 146 -359.2494614983036
506 147 -358.7761929545564
507 148 -358.29792025071674
508 149 -357.81466289980926
509 150 -357.32644060684163
510 151 -356.8332732676394
511 152 -356.3351809676727
512 153 -355.83218398086035
513 154 -355.32430276837033
514 155 -354.8115579774132
515 156 -354.29397044001394
516 157 -353.77156117179067
517 158 -353.2443513707037
518 159 -352.71236241580846
519 160 -352.1756158659938
520 161 -351.6341334587111
521 162 -351.08793710868696
522 163 -350.53704890664415
523 164 -349.9814911179917
524 165 -349.42128618151344
525 166 -348.8564567080596
526 167 -348.28702547920574
527 168 -347.71301544591745
528 169 -347.1344497272148
529 170 -346.5513516087981
530 171 -345.96374454169677
531 172 -345.37165214088856
532 173 -344.7750981839195
533 174 -344.17410660950924
534 175 -343.5687015161603
535 176 -342.958907160738
536 177 -342.34474795706444
537 178 -341.7262484744866
538 179 -341.10343343644536
539 180 -340.4763277190407
540 181 -339.84495634957534
541 182 -339.20934450509844
542 183 -338.5695175109528
543 184 -337.92550083928904
544 185 -337.2773201075922
545 186 -336.62500107720405
546 187 -335.9685696518112
547 188 -335.3080518759632
548 189 -334.6434739335536
549 190 -333.974862146309
550 191 -333.30224297226516
551 192 -332.625643004247
552 193 -331.945088968322
553 194 -331.2606077222687
554 195 -330.5722262540278
555 196 -329.8799716801441
556 197 -329.18387124421236
557 198 -328.48395231530867
558 199 -327.7802423864178
559 200 -327.07276907286035
560 201 -326.3615601107075
561 202 -325.6466433551894
562 203 -324.9280467791087
563 204 -324.2057984712372
564 205 -323.47992663470467
565 206 -322.75045958540545
566 207 -322.01742575036974
567 208 -321.28085366615153
568 209 -320.54077197720164
569 210 -319.7972094342457
570 211 -319.05019489264146
571 212 -318.29975731074944
572 213 -317.5459257482846
573 214 -316.78872936467724
574 215 -316.02819741742525
575 216 -315.2643592604283
576 217 -314.49724434234395
577 218 -313.72688220492125
578 219 -312.9533024813327
579 220 -312.17653489451345
580 221 -311.3966092554844
581 222 -310.6135554616837
582 223 -309.82740349528876
583 224 -309.0381834215261
584 225 -308.2459253870011
585 226 -307.45065961800304
586 227 -306.6524164188296
587 228 -305.8512261700755
588 229 -305.0471193269631
589 230 -304.2401264176268
590 231 -303.430278041432
591 232 -302.6176048672646
592 233 -301.80213763183053
593 234 -300.98390713796135
594 235 -300.16294425290823
595 236 -299.3392799066263
596 237 -298.51294509007374
597 238 -297.6839708535066
598 239 -296.85238830476476
599 240 -296.0182286075673
600 241 -295.1815229797917
601 242 -294.3423026917661
602 243 -293.5005990645651
603 244 -292.6564434682763
604 245 -291.8098673203093
605 246 -290.9609020836696
606 247 -290.1095792652395
607 248 -289.2559304140815
608 249 -288.3999871197082
609 250 -287.5417810103746
610 251 -286.6813437513602
611 252 -285.81870704326684
612 253 -284.9539026202973
613 254 -284.08696224854066
614 255 -283.2179177242736
615 256 -282.3468008722392
616 257 -281.4736435439452
617 258 -280.59847761594335
618 259 -279.7213349881438
619 260 -278.84224758209507
620 261 -277.9612473392815
621 262 -277.0783662194254
622 263 -276.1936361987861
623 264 -275.30708926845676
624 265 -274.4187574326769
625 266 -273.5286727071334
626 267 -272.6368671172587
627 268 -271.7433726965652
628 269 -270.8482214849314
629 270 -269.95144552693205
630 271 -269.05307687015363
631 272 -268.15314756351404
632 273 -267.25168965558845
633 274 -266.3487351929318
634 275 -265.44431621840613
635 276 -264.53846476952646
636 277 -263.6312128767871
637 278 -262.7225925619932
638 279 -261.81263583662377
639 280 -260.90137470016606
640 281 -259.9888411384684
641 282 -259.075067122096
642 283 -258.1600846046838
643 284 -257.24392552130973
644 285 -256.3266217868565
645 286 -255.4082052943786
646 287 -254.48870791348355
647 288 -253.56816148871786
648 289 -252.6465978379316
649 290 -251.7240487506918
650 291 -250.80054598666226
651 292 -249.876121274005
652 293 -248.95080630779265
653 294 -248.02463274841486
654 295 -247.0976322199794
655 296 -246.16983630875313
656 297 -245.2412765615797
657 298 -244.31198448430177
658 299 -243.3819915402164
659 300 -242.4513291485032
660 301 -241.5200286826793
661 302 -240.588121469056
662 303 -239.65563878518424
663 304 -238.72261185834395
664 305 -237.78907186400846
665 306 -236.85504992431584
666 307 -235.92057710656132
667 308 -234.98568442170222
668 309 -234.05040282282633
669 310 -233.11476320369178
670 311 -232.17879639721787
671 312 -231.2425331740081
672 313 -230.30600424089272
673 314 -229.36924023943376
674 315 -228.43227174449714
675 316 -227.49512926277623
676 317 -226.55784323136112
677 318 -225.6204440162947
678 319 -224.68296191115226
679 320 -223.745427135611
680 321 -222.80786983403442
681 322 -221.87032007408234
682 323 -220.9328078452852
683 324 -219.99536305768237
684 325 -219.05801554042074
685 326 -218.1207950403864
686 327 -217.1837312208367
687 328 -216.24685366005008
688 329 -215.3101918499689
689 330 -214.37377519485622
690 331 -213.43763300997392
691 332 -212.50179452024676
692 333 -211.56628885895913
693 334 -210.63114506643296
694 335 -209.6963920887531
695 336 -208.76205877645714
696 337 -207.8281738832593
697 338 -206.89476606479673
698 339 -205.961863877342
699 340 -205.02949577657353
700 341 -204.09769011631573
701 342 -203.16647514731926
702 343 -202.2358790160244
703 344 -201.30592976335333
704 345 -200.37665532349723
705 346 -199.44808352273344
706 347 -198.5202420782262
707 348 -197.5931585968528
708 349 -196.66686057403774
709 350 -195.7413753925822
710 351 -194.81673032150047
711 352 -193.8929525147925
712 353 -192.97006901009956
713 354 -192.0481067268994
714 355 -191.12709246352847
715 356 -190.20705289097697
716 357 -189.28801453846506
717 358 -188.3700037586132
718 359 -187.45304664391338
[-110.0, -91.66666666666666, -73.33333333333333, -54.99999999999999, -36.66666666666665, -18.333333333333318, 1.634938358441546e-14, 18.33333333333335, 36.666666666666686, 55.00000000000002, 73.33333333333336, 91.6666666666667, 110.00000000000003]
[close]
« Last Edit: December 20, 2022, 06:51:31 PM by Liral »
Logged

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #304 on: December 20, 2022, 10:08:38 PM »

Can do about the code! Also, let me just say I appreciate the examples I can relate to.

As for your output: well, it certainly still doesn't have the correct spectrum/periodicity. That makes me think there is a problem with the deg to arc or arc to deg conversions somewhere, either thar or something fundamentally wrong with the probability functions.

I wonder if all of these are in the same unit here?


for weapon in weapons:
        weapon["spread distance"] = deg_to_arc(weapon["spread"], distance)
        weapon["minimum_mean"] = (
            weapon["facing"] + (weapon["arc"] + weapon["spread distance"]) / 2)
        weapon["maximum_mean"] = (
            weapon["facing"] + (weapon["arc"] - weapon["spread distance"]) / 2)


It will be helpful if you can rig the functions to produce some prints of what they do with the input and also maybe print the list of min/max means. Also it will be very helpful for any later debugging if you make it print out a graph, since we want spikes at certain specific coordinates that are easy to grasp visually.

Here is what the spikes should look like before the cut and paste operation at the end. Note that the thing that we do with the angles is equivalent to printing this out, cutting at the lines, then pasting the left edge of the right piece to the left edge and the right edge of the left piece to the right edge, and summing the graphs that are now on top of each other.



This type of plot is also helpful because we know the spikes should be smooth other than at the point, and symmetrical before the cut and paste operation, so if that is not the case there is a problem somewhere. Also, they should have flat tops for all ranges where all weapons that can fire on the target can also track the target for some distance. In this example we get spikes for the overlap of weapons because they are so spaced that there is only one point where adjacent weapons can track the target, but a flat top for the one point defense gun before the summation happens (and two spikes for the pd guns after the summation, because there are two singular points where two of them are aimed at the target).

By the way, for any fans of graphs, here is a plot of dps per angle for 6 guns placed at 60 degree angles dealing 10 damage with 5 spread, as their tracking ability goes from 0 to 180 in increments of 10. Color denotes the tracking arc.


#let's make a pretty graph
matrix <- matrix(data=0, ncol = 3, nrow = 19*360)
for (j in seq(0,180,10)){
  weapon1 <- c(10, 0, j, 5)
  weapon2 <- c(10, 60, j, 5)
  weapon3 <- c(10, -60, j, 5)
  weapon4 <- c(10, 120, j, 5)
  weapon5 <- c(10, -120, j, 5)
  weapon6 <- c(10, 180, j, 5)
  noweapons <- 6
  weapons <- data.frame()
  for (i in 1:noweapons) weapons <- rbind(weapons,get(paste("weapon",i,sep="")))
  colnames(weapons) <- c("damage","facing","trackingrange","spread")
  weapons <- cbind(weapons, maxmean=(weapons$facing+weapons$trackingrange/2-weapons$spread/2))
  weapons <- cbind(weapons, minmean=(weapons$facing-weapons$trackingrange/2+weapons$spread/2))
 
  angles <- seq(-359,360)
  dps_at_angles <- sapply(angles,FUN=sumauc)
 
 
  for (i in 1:180) dps_at_angles[i+360] <- dps_at_angles[i+360]+dps_at_angles
  for (i in 540:720) dps_at_angles[i-360] <- dps_at_angles[i-360]+dps_at_angles
  dps_at_angles <- dps_at_angles[181:540]
  for (i in 1:360) {
    matrix[(1+j*36):(360+j*36),1] <- dps_at_angles
    matrix[(1+j*36):(360+j*36),2] <- seq(1:360)
    matrix[(1+j*36):(360+j*36),3] <- j
  }
}
plot(y=matrix[,1],x=matrix[,2]-180,col=matrix[,3],pch=20,xlab="angle",ylab="dps")




Same, but the guns are placed at 15, -15, 30, -30, 45, -45 instead

« Last Edit: December 21, 2022, 01:19:24 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 #305 on: December 21, 2022, 10:46:01 AM »

Can do about the code! Also, let me just say I appreciate the examples I can relate to.

Good!

Quote
As for your output: well, it certainly still doesn't have the correct spectrum/periodicity. That makes me think there is a problem with the deg to arc or arc to deg conversions somewhere, either thar or something fundamentally wrong with the probability functions.

I checked the arc_measure and arc_length and found that I had implemented them incorrectly. 

Quote
I wonder if all of these are in the same unit here?


for weapon in weapons:
        weapon["spread distance"] = deg_to_arc(weapon["spread"], distance)
        weapon["minimum_mean"] = (
            weapon["facing"] + (weapon["arc"] + weapon["spread distance"]) / 2)
        weapon["maximum_mean"] = (
            weapon["facing"] + (weapon["arc"] - weapon["spread distance"]) / 2)


These numbers are not all of the same unit: spread distance is a distance whereas the minimum and maximum means are angles.

Code
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 probability_hit_between_bounds(
        lower_bound: float,
        upper_bound: float,
        error_distance: float,
        spread_distance: float) -> float:
    """
    The probability to hit between the left and right edges of the
    ship is the integral, from the lower edge of the ship to the
    upper one, of the hit probability distribution. By the
    fundamental theorem of calculus, this integral equals
   
    CDF(upper edge) - CDF(lower edge).
   
    lower_bound -
    upper_bound -
    error_distance -
    spread_distance -
    """
    return (probability_hit_before_bound(upper_bound, error_distance,
                                         spread_distance)
            - probability_hit_before_bound(lower_bound, error_distance,
                                           spread_distance))


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 probability_hit_at_facing(
        weapon_facing: float,
        spread_distance: float,
        error_distance: float,
        target_facing: float,
        target_radius: float,
        distance: float) -> float:
    """
    weapon_facing -
    spread_distance -
    error_distance -
    target_facing -
    target_radius -
    distance -
    """
    lower_bound = arc_length(weapon_facing, distance) + target_radius
    upper_bound = arc_length(weapon_facing, distance) - target_radius
    return probability_hit_between_bounds(upper_bound, lower_bound,
                                          error_distance, spread_distance)


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(
        minimum_mean: float,
        maximum_mean: 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, the probability distribution times damage
    for each weapon.
   
    minimum_mean -
    maximum_mean -
    spread_distance -
    error_distance -
    target_facing -
    target_radius -
    distance -
    target_facing - target ship orientation
    """
    #we have defined spread in degrees, so we must convert it to
    #pixels to be consistent
   
    #angle of the ship's upper bound, in coordinates of the
    #distribution mean; note that this is weapon specific
    weapon_facing = transformed_angle(target_facing, minimum_mean, maximum_mean)
    return probability_hit_at_facing(weapon_facing, spread_distance,
                                     error_distance, target_facing,
                                     target_radius, distance)


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.
   
    target_radius = 220 #(14000, 500, 10000, 1500, 220, 12, 440, 1.0, 200)
    cell_count = 12
    distance = 1000
   
    #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:
        print("spread angle", weapon["spread"])
        weapon["spread distance"] = arc_length(weapon["spread"], distance)
        print("spread distance", weapon["spread distance"])
        weapon["minimum_mean"] = (
            weapon["facing"] + (weapon["arc"] + weapon["spread"]) / 2)
        print("minimum mean", weapon["minimum_mean"])
        weapon["maximum_mean"] = (
            weapon["facing"] + (weapon["arc"] - weapon["spread"]) / 2)
        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_total_at_angles = [
        sum(weapon["damage"]
            * total_hit_probability(weapon["minimum_mean"],
                                    weapon["maximum_mean"],
                                    weapon["spread distance"],
                                    error_distance,
                                    target_facing,
                                    target_radius,
                                    distance)
            for weapon in weapons)
        for target_facing in range(-359,361)]
   
    #-180 corresponds to +180
    #-181 corresponds to +179 etc, so 360 + index for indices 1:180
    for i in range(180):
        damage_per_second_total_at_angles[i+360] += (
            damage_per_second_total_at_angles[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_total_at_angles[i-360] += (
        damage_per_second_total_at_angles[i])
   
    #finally, to get angles -179 to 180, select indices 181 to 540
    #of the new vector.
    damage_per_second_total_at_angles = damage_per_second_total_at_angles[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]
spread angle 5
spread distance 87.26646259971648
minimum mean 2.5
maximum mean -2.5

spread angle 5
spread distance 87.26646259971648
minimum mean 22.5
maximum mean 17.5

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

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

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

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

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

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #306 on: December 21, 2022, 11:06:20 AM »

Right. And if you look at my code

weapons <- cbind(weapons, maxmean=(weapons$facing+weapons$trackingrange/2-weapons$spread/2))
weapons <- cbind(weapons, minmean=(weapons$facing-weapons$trackingrange/2+weapons$spread/2))



These are all angles. Need to be careful with these sums - all elements must be same unit.

I think there's also another error in that the minimum mean should be facing minus tracking angle/2 minus spread/2, not facing plus tracking angle/2 minus spread/2. This would explain the negative numbers I think, and is why your minimum means are higher than maximum even in the new code and output.
« Last Edit: December 21, 2022, 11:33:39 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 #307 on: December 21, 2022, 01:28:35 PM »

Right. And if you look at my code

weapons <- cbind(weapons, maxmean=(weapons$facing+weapons$trackingrange/2-weapons$spread/2))
weapons <- cbind(weapons, minmean=(weapons$facing-weapons$trackingrange/2+weapons$spread/2))



These are all angles. Need to be careful with these sums - all elements must be same unit.

Now I'm a little confused.  I have a degree angle number in each weapon called "spread" alongside an arc length number called "spread distance", and the minimum and maximum means are angles based on the former and two other angles.

Quote
I think there's also another error in that the minimum mean should be facing minus tracking angle/2 minus spread/2, not facing plus tracking angle/2 minus spread/2. This would explain the negative numbers I think, and is why your minimum means are higher than maximum even in the new code and output.

Ok, fixed that, but it still seems off.  :-[

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 probability_hit_between_bounds(
        lower_bound: float,
        upper_bound: float,
        error_distance: float,
        spread_distance: float) -> float:
    """
    The probability to hit between the left and right edges of the
    ship is the integral, from the lower edge of the ship to the
    upper one, of the hit probability distribution. By the
    fundamental theorem of calculus, this integral equals
   
    CDF(upper edge) - CDF(lower edge).
   
    lower_bound -
    upper_bound -
    error_distance -
    spread_distance -
    """
    return (probability_hit_before_bound(upper_bound, error_distance,
                                         spread_distance)
            - probability_hit_before_bound(lower_bound, error_distance,
                                           spread_distance))


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 probability_hit_at_facing(
        weapon_facing: float,
        spread_distance: float,
        error_distance: float,
        target_facing: float,
        target_radius: float,
        distance: float) -> float:
    """
    weapon_facing -
    spread_distance -
    error_distance -
    target_facing -
    target_radius -
    distance -
    """
    lower_bound = arc_length(weapon_facing, distance) + target_radius
    upper_bound = arc_length(weapon_facing, distance) - target_radius
    return probability_hit_between_bounds(upper_bound, lower_bound,
                                          error_distance, spread_distance)


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(
        minimum_mean: float,
        maximum_mean: 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, the probability distribution times damage
    for each weapon.
   
    minimum_mean -
    maximum_mean -
    spread_distance -
    error_distance -
    target_facing -
    target_radius -
    distance -
    target_facing - target ship orientation
    """
    #we have defined spread in degrees, so we must convert it to
    #pixels to be consistent
   
    #angle of the ship's upper bound, in coordinates of the
    #distribution mean; note that this is weapon specific
    weapon_facing = transformed_angle(target_facing, minimum_mean, maximum_mean)
    return probability_hit_at_facing(weapon_facing, spread_distance,
                                     error_distance, target_facing,
                                     target_radius, distance)


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.
   
    target_radius = 220 #(14000, 500, 10000, 1500, 220, 12, 440, 1.0, 200)
    cell_count = 12
    distance = 1000
   
    #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:
        print("spread angle", weapon["spread"])
        weapon["spread distance"] = arc_length(weapon["spread"], distance)
        print("spread distance", weapon["spread distance"])
        weapon["minimum_mean"] = (
            weapon["facing"] - (weapon["arc"] + weapon["spread"]) / 2)
        print("minimum mean", weapon["minimum_mean"])
        weapon["maximum_mean"] = (
            weapon["facing"] + (weapon["arc"] + weapon["spread"]) / 2)
        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_total_at_angles = [
        sum(weapon["damage"]
            * total_hit_probability(weapon["minimum_mean"],
                                    weapon["maximum_mean"],
                                    weapon["spread distance"],
                                    error_distance,
                                    target_facing,
                                    target_radius,
                                    distance)
            for weapon in weapons)
        for target_facing in range(-359,361)]

    #-180 corresponds to +180
    #-181 corresponds to +179 etc, so 360 + index for indices 1:180
    for i in range(180):
        damage_per_second_total_at_angles[i+360] += (
            damage_per_second_total_at_angles[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_total_at_angles[i-360] += (
        damage_per_second_total_at_angles[i])
   
    #finally, to get angles -179 to 180, select indices 181 to 540
    #of the new vector.
    damage_per_second_total_at_angles = damage_per_second_total_at_angles[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]
spread angle 5
spread distance 87.26646259971648
minimum mean -22.5
maximum mean 2.5

spread angle 5
spread distance 87.26646259971648
minimum mean -2.5
maximum mean 22.5

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

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

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

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

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

CapnHector

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

I didn't notice that you had fixed it for the new code when I posted. There is still one error here.

weapon["maximum_mean"] = (
            weapon["facing"] + (weapon["arc"] + weapon["spread"]) / 2)

This should read

weapon["maximum_mean"] = (
            weapon["facing"] + (weapon["arc"] - weapon["spread"]) / 2)

(The correct solution is 2.5 to 17.5 for gun 2: the turret can turn from 0 to 20, but has a spread of 5 degrees so can't align the mean exactly with its maximum angle)

What kind of output do you get for the angles now?
« Last Edit: December 21, 2022, 07:09:44 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 #309 on: December 21, 2022, 07:42:17 PM »

spread angle 5
spread distance 87.266
minimum mean -22.5
maximum mean -2.5

spread angle 5
spread distance 87.266
minimum mean -2.5
maximum mean 17.5

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

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

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

spread angle 5
spread distance 87.266
minimum mean 87.5
maximum mean 87.5

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #310 on: December 21, 2022, 08:06:40 PM »

All right, one more try. Your code will have slightly different signs than mine, because of the added brackets.

weapon["minimum_mean"] = (
            weapon["facing"] - (weapon["arc"] - weapon["spread"]) / 2)
       
        weapon["maximum_mean"] = (
            weapon["facing"] + (weapon["arc"] - weapon["spread"]) / 2)

That should be correct. Now they are symmetrical, too. My bad, I wrote this wrong in a comment above. There is a difference in pluses and minuses because I didn't use brackets.

I also notice something of an omission of mine. Specifically, this definition leads to guns without any tracking ability being able to slightly "track", to the extent that spread > arc. By our assumptions, guns with spread >= arc should not be able to track at all. For guns with no arc, this is of course because they are fixed. For guns with a positive arc and spread >= arc, this is because the recoil from a single shot is able to kick them to their other maximum angle (and therefore cause the shot location to be similarly random as if the gun had not tracked at all), so it does not matter how they are angled originally.

This is fixed like so:

weapons <- cbind(weapons, maxmean=(weapons$facing+weapons$trackingrange/2-weapons$spread/2))
weapons <- cbind(weapons, minmean=(weapons$facing-weapons$trackingrange/2+weapons$spread/2))
weapons[which(weapons$minmean >= weapons$maxmean),]$minmean <- weapons[which(weapons$minmean >= weapons$maxmean),]$facing
weapons[which(weapons$minmean >= weapons$maxmean),]$maxmean <- weapons[which(weapons$minmean >= weapons$maxmean),]$facing

(an alternative of course is to use a loop and do max(maxmean, facing) and min(minmean, facing)

Output:

> weapons
  damage facing trackingrange spread maxmean minmean
1    100    -10            20      5    -2.5   -17.5
2    100     10            20      5    17.5     2.5
3     30   -160            20      0  -150.0  -170.0
4     30    180            20      0   190.0   170.0
5     30    160            20      0   170.0   150.0
6    120     90             0      5    90.0    90.0


Before cut and paste:


After cut and paste:
« Last Edit: December 21, 2022, 10:38:43 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 #311 on: December 22, 2022, 01:32:34 AM »

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 probability_hit_between_bounds(
        lower_bound: float,
        upper_bound: float,
        error_distance: float,
        spread_distance: float) -> float:
    """
    The probability to hit between the left and right edges of the
    ship is the integral, from the lower edge of the ship to the
    upper one, of the hit probability distribution. By the
    fundamental theorem of calculus, this integral equals
   
    CDF(upper edge) - CDF(lower edge).
   
    lower_bound -
    upper_bound -
    error_distance -
    spread_distance -
    """
    return (probability_hit_before_bound(upper_bound, error_distance,
                                         spread_distance)
            - probability_hit_before_bound(lower_bound, error_distance,
                                           spread_distance))


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 probability_hit_at_facing(
        weapon_facing: float,
        spread_distance: float,
        error_distance: float,
        target_facing: float,
        target_radius: float,
        distance: float) -> float:
    """
    weapon_facing -
    spread_distance -
    error_distance -
    target_facing -
    target_radius -
    distance -
    """
    lower_bound = arc_length(weapon_facing, distance) + target_radius
    upper_bound = arc_length(weapon_facing, distance) - target_radius
    return probability_hit_between_bounds(upper_bound, lower_bound,
                                          error_distance, spread_distance)


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(
        minimum_mean: float,
        maximum_mean: 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, the probability distribution times damage
    for each weapon.
   
    minimum_mean -
    maximum_mean -
    spread_distance -
    error_distance -
    target_facing -
    target_radius -
    distance -
    target_facing - target ship orientation
    """
    #we have defined spread in degrees, so we must convert it to
    #pixels to be consistent
   
    #angle of the ship's upper bound, in coordinates of the
    #distribution mean; note that this is weapon specific
    weapon_facing = transformed_angle(target_facing, minimum_mean, maximum_mean)
    return probability_hit_at_facing(weapon_facing, spread_distance,
                                     error_distance, target_facing,
                                     target_radius, distance)


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.
    target_radius = 220#(14000, 500, 10000, 1500, 220, 12, 440, 1.0, 200)
    cell_count = 12
    distance = 1000
   
    #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:
        weapon["spread distance"] = arc_length(weapon["spread"], distance)
        half_difference = (weapon["arc"] - weapon["spread"]) / 2
        weapon["minimum mean"] = weapon["facing"] - half_difference
        weapon["maximum mean"] = weapon["facing"] + half_difference
        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_total_at_angles = [
        sum(weapon["damage"]
            * total_hit_probability(weapon["minimum mean"],
                                    weapon["maximum mean"],
                                    weapon["spread distance"],
                                    error_distance,
                                    target_facing,
                                    target_radius,
                                    distance)
            for weapon in weapons)
        for target_facing in range(-359,361)]

    #-180 corresponds to +180
    #-181 corresponds to +179 etc, so 360 + index for indices 1:180
    for i in range(180):
        damage_per_second_total_at_angles[i+360] += (
            damage_per_second_total_at_angles[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_total_at_angles[i-360] += (
            damage_per_second_total_at_angles[i])
   
    #finally, to get angles -179 to 180, select indices 181 to 540
    #of the new vector.
    damage_per_second_total_at_angles = damage_per_second_total_at_angles[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]
Results
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 92.5
maximum mean 87.5

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]

CapnHector

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

Those numbers now seem to be the same as mine, other than the last weapon which is suffering from the above mentioned omission.

What kind of results do you get for dps at angles with these?
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 #313 on: December 22, 2022, 07:51:21 AM »

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 probability_hit_between_bounds(
        lower_bound: float,
        upper_bound: float,
        error_distance: float,
        spread_distance: float) -> float:
    """
    The probability to hit between the left and right edges of the
    ship is the integral, from the lower edge of the ship to the
    upper one, of the hit probability distribution. By the
    fundamental theorem of calculus, this integral equals
   
    CDF(upper edge) - CDF(lower edge).
   
    lower_bound -
    upper_bound -
    error_distance -
    spread_distance -
    """
    return (probability_hit_before_bound(upper_bound, error_distance,
                                         spread_distance)
            - probability_hit_before_bound(lower_bound, error_distance,
                                           spread_distance))


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 probability_hit_at_facing(
        weapon_facing: float,
        spread_distance: float,
        error_distance: float,
        target_facing: float,
        target_radius: float,
        distance: float) -> float:
    """
    weapon_facing -
    spread_distance -
    error_distance -
    target_facing -
    target_radius -
    distance -
    """
    lower_bound = arc_length(weapon_facing, distance) + target_radius
    upper_bound = arc_length(weapon_facing, distance) - target_radius
    return probability_hit_between_bounds(upper_bound, lower_bound,
                                          error_distance, spread_distance)


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(
        minimum_mean: float,
        maximum_mean: 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, the probability distribution times damage
    for each weapon.
   
    minimum_mean -
    maximum_mean -
    spread_distance -
    error_distance -
    target_facing -
    target_radius -
    distance -
    target_facing - target ship orientation
    """
    #we have defined spread in degrees, so we must convert it to
    #pixels to be consistent
   
    #angle of the ship's upper bound, in coordinates of the
    #distribution mean; note that this is weapon specific
    weapon_facing = transformed_angle(target_facing, minimum_mean, maximum_mean)
    return probability_hit_at_facing(weapon_facing, spread_distance,
                                     error_distance, target_facing,
                                     target_radius, distance)


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.
    target_radius = 220#(14000, 500, 10000, 1500, 220, 12, 440, 1.0, 200)
    cell_count = 12
    distance = 1000
   
    #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:
        weapon["spread distance"] = arc_length(weapon["spread"], distance)
        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)
        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_total_at_angles = [
        sum(weapon["damage"]
            * total_hit_probability(weapon["minimum mean"],
                                    weapon["maximum mean"],
                                    weapon["spread distance"],
                                    error_distance,
                                    target_facing,
                                    target_radius,
                                    distance)
            for weapon in weapons)
        for target_facing in range(-359,361)]
   
    for damage_per_second in damage_per_second_total_at_angles:
        print(damage_per_second)
    print()
    #import matplotlib.pyplot as plt
    #plt.scatter(range(-359,361), damage_per_second_total_at_angles, s=1)

    #-180 corresponds to +180
    #-181 corresponds to +179 etc, so 360 + index for indices 1:180
    for i in range(180):
        damage_per_second_total_at_angles[i+360] += (
            damage_per_second_total_at_angles[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_total_at_angles[i-360] += (
            damage_per_second_total_at_angles[i])
   
    #finally, to get angles -179 to 180, select indices 181 to 540
    #of the new vector.
    damage_per_second_total_at_angles = damage_per_second_total_at_angles[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

-3.2820535971098606
-3.3135906944555766
-3.34514414268445
-3.376707177170404
-3.4082728973770395
-3.439834266258156
-3.4713841096844815
-3.502915115905303
-3.5344198350486358
-3.565890678649966
-3.5973199192219685
-3.6286996898577435
-3.660021983873083
-3.6912786544849823
-3.7224614145326727
-3.7535618362326355
-3.784571350978516
-3.8154812491828274
-3.8462826801564405
-3.8769666520307857
-3.907524031730425
-3.937945544980251
-3.9682217763647283
-3.9983431694274443
-4.028300026821866
-4.058082510503623
-4.087680641976378
-4.117084302580308
-4.146283233832568
-4.175267037820703
-4.204025177636347
-4.232546977871436
-4.260821625159095
-4.288838168769372
-4.316585521255061
-4.3440524591564715
-4.371227623753389
-4.3980995218773185
-4.424656526777004
-4.450886879036668
-4.4767786875579825
-4.502319930590731
-4.527498456825037
-4.552301986542926
-4.576718112823278
-4.6007343028104435
-4.624337899040363
-4.647516120825085
-4.670256065696661
-4.692544710918924
-4.714368915047655
-4.735715419564935
-4.756570850562816
-4.776921720501513
-4.7967544300179155
-4.816055269804524
-4.834810422546442
-4.8530059649270765
-4.8706278696887235
-4.887662007762808
-4.904094150462571
-4.919909971736657
-4.9350950504907924
-4.949634872967564
-4.963514835196875
-4.976720245504296
-4.989236327089034
-5.001048220658516
-5.0121409871347735
-5.022499610419795
-5.032109000228114
-5.040953994977563
-5.049019364749837
-5.056289814311637
-5.062749986198836
-5.068384463862393
-5.0731777748808575
-5.077114394232592
-5.080178747630633
-5.082355214918062
-5.08362813352756
-5.083981802000879
-5.0834004835677264
-5.0818684097862485
-5.0793697842426955
-5.075888786311489
-5.071409574972549
-5.0659162926845624
-5.059393069322407
-5.051824026164122
-5.043193279938437
-5.0334849469277785
-5.0226831471201185
-5.0107720084263985
-4.9977356709386065
-4.983558291248478
-4.968224046814518
-4.951717140381063
-4.934021804445714
-4.915122305775681
-4.895002949973522
-4.873648086087961
-4.851042111271008
-4.827169475482561
-4.802014686229808
-4.7755623133621725
-4.747796993896923
-4.718703436888821
-4.688266428338942
-4.656470836140956
-4.623301615062696
-4.588743811765502
-4.552782569852069
-4.515403134952209
-4.476590859837726
-4.436331209563721
-4.394609766642487
-4.351412236241536
-4.306724451408391
-4.260532378314952
-4.212822121528694
-4.163579929300067
-4.112792198875296
-4.060445481815902
-4.00652648934203
-3.95102209768703
-3.8939193534647494
-3.835205479044342
-3.7748678779367237
-3.712894140185099
-3.6492720477667504
-3.5839895799870245
-3.5170349188826853
-3.4483964546263066
-3.3780627909191763
-3.306022750387851
-3.232265379972368
-3.1567799563027985
-3.0795559910718695
-3.0005832363878433
-2.9198516901190197
-2.8373516012205173
-2.7530734750416723
-2.6670080786126276
-2.5791464459114906
-2.4894798831050857
-2.3979999737620274
-2.304698584040219
-2.2095678678421065
-2.112600271934773
-2.0137885410425236
-1.9131257228925511
-1.810605173229394
-1.7062205607851273
-1.599965872205455
-1.4918354169299815
-1.3818238320173286
-1.2699260868041773
-1.1561374864228573
-1.0404536659038104
-0.9228705140332427
-0.8033836369788241
-0.681985192072097
-0.5586476697515204
-0.4332516968550677
-0.30530779143291986
-0.17303382897957853
-0.030748578422636452
0.13730942355900844
0.36896072783760125
0.7420160102482036
1.3918045203622023
2.5122334301289255
4.319644758608188
6.971515858148949
10.465096896036219
14.573919942770068
18.87787927240069
22.89139398953491
26.227153712358053
28.706871559839705
30.369012048489534
31.391869382551068
31.992413565777795
32.35371580920003
32.59805644707593
32.79319212828767
32.97097165276466
33.14423982992319
33.31740595657078
33.49193920415485
33.66830205357945
33.846485428863595
34.026479800092055
34.208275181903005
34.39186113209997
34.57722675038761
34.76436067723418
34.95325109285854
35.143885716348244
35.336251804903966
35.53033615321892
35.72612509298679
35.923604492543085
36.12275975664425
36.32357582637856
36.526037179213326
36.73012782918302
36.935831327212476
37.14313076158033
37.35153765394254
37.55955682466322
37.76275295701745
37.948255208980605
38.084188734398126
38.10277082364171
37.881695066320496
37.237864159054524
35.95428198872228
33.8526282239425
30.894412202579932
27.25789464087836
23.330359736080567
19.59736217863843
16.478981468666095
14.2016592884509
12.768119201693791
12.020857564859506
11.743506045218234
11.74213113440937
11.881377223517491
12.0834072542994
12.31037427034324
12.546490379653854
12.786047899420874
13.027238230464777
13.269545393437733
13.512823495332091
13.757018484638412
14.002095955071983
14.248024928011791
14.494774686864389
14.742314241345722
14.990612252077113
15.239637025505806
15.489356518321822
15.739738343102214
15.990749774199212
16.242357753774755
16.494528897961402
16.747229503157712
17.000425552449475
17.25408272215823
17.508166388513747
17.762641634452145
18.01747325653442
18.2726257719845
18.528063425848753
18.783750198267686
19.039649811866617
19.295725739256383
19.551941210645616
19.808259221561656
20.06464254067921
20.321053717752253
20.57745509164933
20.833808798488107
21.090076779868824
21.34622079120235
21.602202410132847
21.857983045049174
22.11352394368748
22.36878620181757
22.623730772011328
22.87831847249686
23.13250999608564
23.38626591917749
23.639546710837163
23.89231274194401
24.144524294403745
24.39614157043013
24.647124701883225
24.897433759670925
25.14702876320241
25.395869689897758
25.64391648474382
25.891129069901158
26.13746735435099
26.382891243585373
26.627360649334292
26.870835499326866
27.11327574708475
27.354641381743434
27.59489243789921
27.833989005477008
28.071891239617894
28.308559370582103
28.54395371366303
28.77803467911201
29.010762782068117
29.242098652489084
29.47200304508352
29.700436849238297
29.927361098936625
30.152736982668937
30.376525853325266
30.598689238074634
30.819188848221046
31.037986589034965
31.25504456955943
31.47032511238488
31.683790763387837
31.89540430143564
32.10512874804794
32.31292737701511
32.51876372397008
32.72260159590799
32.9244050806546
33.124138556274076
33.32176670042082
33.51725449962376
33.7105672585072
33.901670608939206
34.09053051910873
34.277113302526075
34.46138562694359
34.64331452319422
34.82286739394458
35.00001202236037
35.1747165806786
35.34694963868604
35.516680172102056
35.68387757085677
35.848511647270925
36.01055264412556
36.1699712426252
36.32673857024854
36.4808262084842
36.63220620045057
36.78085105839363
36.92673377106532
37.06982781097359
37.21027705046748
37.349075348853205
37.48636728397101
37.622127739140645
37.75633181890341
37.88895485575978
38.01997241686243
38.14936031066048
38.27709459349329
38.40315157613202
38.52750783026435
38.650140194924745
38.77102578286064
38.89014198684078
39.007466485896614
39.122807342539
39.235123428740565
39.34422370725858
39.45008765737707
39.552695371642315
39.65219747034401
39.749594686983585
39.845038598157174
39.93851120876438
40.029994865966664
40.11947226410167
40.2069264495074
40.29234082525696
40.37569915580094
40.45698557151623
40.53618457315926
40.61328103622262
40.688260215193026
40.761107747708856
40.83180965861667
40.90018245496407
40.96519354972549
41.02666054082987
41.08457163457641
41.13891574000802
41.1896824720405
41.23686215438164
41.28044582224399
41.320425224847654
41.3567928277116
41.38954181473687
41.418666090074495
41.4441602797836
41.46601973327462
41.48424052454111
41.49881945317482
41.50975404516922
41.51704255350595
41.52068395852953
41.5206779681041
41.51702501755746
41.50972626941029
41.49878361289036
41.48419966323183
41.465977760762215
41.44412196977461
41.4186370771866
41.38952859098906
41.35680273847958
41.32046646428783
41.28052742818943
41.23699400271066
41.18987527052482
41.1391810216412
41.0849217503889
41.02710865219381
40.96575362015434
40.90086924141206
40.832468793325575
40.76056623944005
40.68517622526355
40.6063140738463
40.52399578116263
40.43823801130495
40.34905809148482
40.25647400684525
40.16050439508798
40.06116854091475
39.95848637028776
39.85247844450864
39.74316595412099
39.6305707126375
39.5147151500927
39.39562230642766
39.273315824704234
39.14781994415695
39.01915949307906
38.8873598815517
38.75244709401427
38.61444768168133
38.473388754807274
38.32929797480523
38.18220354621535
38.03213420853732
37.879119227918274
37.72318838870861
37.56437198488064
37.402700811321935
37.23820615499939
37.070919786001134
36.90107783921009
36.72993630125774
36.55773187087327
36.3844974457323
36.210266003913745
36.034866703872865
35.857109379027634
35.67682365885986
35.49404346343189
35.30880310321045
35.121137269556385
34.93108102513361
34.73866979423409
34.54393935303651
34.34692581978378
34.14766564489908
33.94619560103097
33.74255277304552
33.53677454795043
33.328898604776015
33.118962904394365
32.90700567929957
32.69306542334442
32.47718088142963
32.25939103916608
32.03973511249759
31.818252537299422
31.594982958945465
31.369966221861205
31.143242359061738
30.91485158179396
30.68483427025255
30.453230973652154
30.22008248048641
29.985430348953216
29.74932006717301
29.51181726518549
29.273079905542872
29.033636522328216
28.795308748708422
28.563817774667402
28.35496546637236
28.20661321510807
28.19661296826736
28.460335671669533
29.191730611688676
30.607182333443255
32.864211120376126
35.96011046927365
39.66845617188314
43.569186608656096
47.17676537022511
50.103926944213086
52.17242977638645
53.42078470148822
54.027336397272975
54.209125711857006
54.14938726561583
53.97093560965532
53.743354948205166
53.50422143618438
53.28218816035768
53.1195324694701
53.095582395738646
53.34617853516659
54.065268526466106
55.46923507525599
57.71559643513384
60.80164388424283
64.50095080847355
68.39345299674129
71.99361126295696
74.91415712401971
76.97684578201584
78.22018401598132
78.82250846712013
79.00083174562333
78.93826783348783
78.75713999554611
78.52524796595051
78.27448377970497
78.01773562622635
77.75945632239106
77.5006875903575
77.24045835214268
76.97438642178773
76.68965421426955
76.35444025920678
75.90101533405763
75.2071266193434
74.08973049895603
72.33188463591856
69.75532255638612
66.3216075296631
62.20905426985097
57.80500103097431
53.59505661170322
49.999354664318396
47.24439105972483
45.3329435792116
44.10756281831123
43.35193469735459
42.8721799675573
42.53252617402792
42.25371316411505
41.993531194404156
41.723394496609444
41.40579393486059
40.97121201206156
40.296900559868774
39.19969100394836
37.46260819319036
34.90737278560176
31.495538908795268
27.405412693893346
23.024323832701413
18.837872504646477
15.266183678161527
12.535744477462284
10.649323877179773
9.449463609182942
8.7198406761295
8.266566860516406
7.954331795255898
7.705343610393475
7.481800479755517
7.267959579028006
7.058158225996191
6.850632750487318
6.644912026022219
6.440894931816638
6.23857210338752
6.037953731749087
5.839053341289836
5.64188461959497
5.44646087716287
5.252794967425325
5.0608992770994154
4.870785726014708
4.682465768168513
4.495950393006338
4.3112501268531656
4.128375034447451
3.947334720617044
3.768138332056421
3.5907945592238333
3.4153116383679682
3.241697353651105
3.0699590393957754
2.900103582430331
2.7321374245597063
2.5660665651295034
2.4018965637018264
2.2396325428213038
2.0792791909088137
1.9208407652330095
1.764321094975707
1.6097235844124036
1.4570512161778115
1.3063065546126706
1.1574917492183712
1.0106085381797891
0.8656582519937173
0.7226418171680882
0.581559760003385
0.44241221045887347
0.3051989060932314
0.16991919608445372
0.03657204531140046
-0.0948439614735408
-0.22433061542358246
-0.351890079291044
-0.47752488307384766
-0.6012379195808926
-0.7230324399502752
-0.8429120491019937
-0.9608807011386977
-1.076942694687122
-1.1911026681868808
-1.3033655951331236
-1.4137367792748634
-1.5222218497511486
-1.6288267562000192
-1.7335577638178146
-1.8364214483816088
-1.937424691228471
-2.0365746742066815
-2.133878874586963
-2.229345059946697
-2.322981283024199
-2.4147958765336375
-2.504797447984699
-2.592994874440784
-2.6793972972839253
-2.764014116945419
-2.846854987629448
-2.9279298120105546
-3.0072487359268463
-3.0848221430574707
-3.160660649585645
-3.2347750988721513
-3.307176556086482
-3.3778763028817504
-3.446885832023745
-3.514216842054525
-3.5798812319273
-3.6438910956687165
-3.706258717025954
-3.7669965641408654
-3.8261172842170277
-3.883633698199116
-3.9395587954750244
-3.9939057285718604
-4.046687807891964
-4.097918496435131
-4.147611404574536
-4.195780284811557
-4.242439026599287
-4.287601651139243
-4.331282306240993
-4.373495261189619
-4.414254901639927
-4.453575724546486
-4.491472333115629
-4.527959431796176
-4.563051821297455
-4.596764393644013
-4.629112127254706
-4.660110082088051
-4.689773394777972
-4.718117273852145
-4.745156994965574
-4.770907896182757
-4.795385373302876
-4.818604875217991
-4.8405818993294325
-4.861331986999038
-4.880870719056336
-4.899213711331654
-4.9163766102644235
-4.932375088545244
-4.947224840800599
-4.960941579342193
-4.973541029963129
-4.985038927779555
-4.995451013131644
-5.004793027538312
-5.013080709694258
-5.020329791539537
-5.026555994373356
-5.031775025016492
-5.036002572053544
-5.039254302107152
-5.041545856176999
-5.042892846049751
-5.043310850748481
-5.042815413049673
-5.041422036059577
-5.039146179852136
-5.036003258154267
-5.032008635108598
-5.027177622082535
-5.021525474545587
-5.015067389002188
-5.007818499993539
-4.999793877145136
-4.991008522294029
-4.981477366671694
-4.971215268128719
-4.960237008461403
-4.948557290750428
-4.936190736810371
-4.9231518846628575

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]

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #314 on: December 22, 2022, 09:50:36 AM »

Oh, this has turned into quite the bug hunt. The output at least seems to exhibit a more reasonable spectrum, but something is still very wrong as long as you are getting negative numbers.

Maybe this function has an issue?

def probability_hit_at_facing(
        weapon_facing: float,
        spread_distance: float,
        error_distance: float,
        target_facing: float,
        target_radius: float,
        distance: float) -> float:
    """
    weapon_facing -
    spread_distance -
    error_distance -
    target_facing -
    target_radius -
    distance -
    """
    lower_bound = arc_length(weapon_facing, distance) + target_radius
    upper_bound = arc_length(weapon_facing, distance) - target_radius
    return probability_hit_between_bounds(upper_bound, lower_bound,
                                          error_distance, spread_distance)


I think it's supposed to do the same as my


    shipupperbound <- deg_to_arc(transformed_angle(angle,weapons[i,])+shipwidth/2)
    shiplowerbound <- deg_to_arc(transformed_angle(angle,weapons[i,])-shipwidth/2)
    #we have defined spread in degrees, so we must convert it to pixels to be consistent
    pxspread <- deg_to_arc(weapons[i,4])
   
    damage <- weapons[i,1]
    #now we calculate the sum
    summed_auc <- summed_auc + damage*(
      hit_probability_coord_lessthan_x(shipupperbound, error, pxspread) -
        hit_probability_coord_lessthan_x(shiplowerbound, error, pxspread)
 


I have two questions about this: 1) what does arc_length(weapon_facing, distance) do? Does it return the arc from 0 to weapon facing? If so, then that will be incorrect, because to use the probability distribution all coordinates must be relative to the probability distribution (ie. mean = 0, +50px = 50 px above mean, etc). The equations for the distribution assume the mean is 0, which is why we must transform to coordinates of the weapon (and more exactly the mean of the shot distribution from the weapon) for each weapon separately. And of course an integral from -1000 to -950 is not the same as an integral from 0 to 50. So arc length is the correct thing to use but it must be the arc length from the mean of the distribution to the coordinate, with sign. 2) Aren't the upper and lower bounds reversed in your code?

I'm starting to think maybe it could be easier to re-do it with an extremely literal (no new functions, making expressions more elegant, etc.) translation of these smaller functions since there aren't that many, than try to squash the apparently several bugs we are facing, but of course whatever you think is best.

Specifically to fix this if the code does what I think it does you should write

    lower_bound = weapon_adjustment_distance(args) - target_radius
    upper_bound =  weapon_adjustment_distance(args) + target_radius
« Last Edit: December 22, 2022, 10:37:10 AM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge
Pages: 1 ... 19 20 [21] 22 23 ... 32