Skip to content

visualizations.bubbles

BubbleChart(instance, printing=None)

Initialize an "AFSC/Cadet Bubble" chart and animation object.

This class is designed to construct a graphical representation of the "AFSC/Cadet Bubble," showing the placement of cadets in a solution and their movement through various algorithms. The problem instance is the only required parameter.

Args: instance: A CadetCareerProblem instance, containing various attributes and parameters necessary for constructing the bubble chart. printing (bool, None): A flag to control whether to print information during chart creation and animation. If set to True, the class will print progress and debugging information. If set to False, it will suppress printing. If None, the class will use the default printing setting from the instance.

Notes: - This constructor extracts various attributes from the CadetCareerProblem instance provided as instance. - The solution_iterations attribute of the problem instance is expected to be a dictionary of a particular set of solutions used in the figure. - The 'b' dictionary contains hyperparameters for the animation/plot, as defined in afccp.core.data.ccp_helping_functions.py.

Attributes: - p: A dictionary containing parameters extracted from the instance. - vp: A dictionary containing value parameters extracted from the instance. - b: A dictionary containing hyperparameters for the bubble chart, populated from the instance. - data_name: The name of the data used for the chart. - data_version: The version of the data used for the chart. - solution: The solution data extracted from the instance. - mdl_p: Model parameters extracted from the instance. - paths: Export paths from the instance. - printing: A boolean flag for controlling printing behavior during chart creation and animation. - v_hex_dict: A dictionary mapping value parameter values to their corresponding hexadecimal colors. - ...

Source code in afccp/visualizations/bubbles.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def __init__(self, instance, printing=None):
    """
    Initialize an "AFSC/Cadet Bubble" chart and animation object.

    This class is designed to construct a graphical representation of the "AFSC/Cadet Bubble," showing the placement
    of cadets in a solution and their movement through various algorithms. The problem instance is the only required
    parameter.

    Args:
        instance: A CadetCareerProblem instance, containing various attributes and parameters necessary for constructing
            the bubble chart.
        printing (bool, None): A flag to control whether to print information during chart creation and animation. If set
            to True, the class will print progress and debugging information. If set to False, it will suppress printing.
            If None, the class will use the default printing setting from the instance.

    Notes:
    - This constructor extracts various attributes from the `CadetCareerProblem` instance provided as `instance`.
    - The `solution_iterations` attribute of the problem instance is expected to be a dictionary of a particular set of
      solutions used in the figure.
    - The 'b' dictionary contains hyperparameters for the animation/plot, as defined in `afccp.core.data.ccp_helping_functions.py`.

    Attributes:
    - p: A dictionary containing parameters extracted from the `instance`.
    - vp: A dictionary containing value parameters extracted from the `instance`.
    - b: A dictionary containing hyperparameters for the bubble chart, populated from the `instance`.
    - data_name: The name of the data used for the chart.
    - data_version: The version of the data used for the chart.
    - solution: The solution data extracted from the `instance`.
    - mdl_p: Model parameters extracted from the `instance`.
    - paths: Export paths from the `instance`.
    - printing: A boolean flag for controlling printing behavior during chart creation and animation.
    - v_hex_dict: A dictionary mapping value parameter values to their corresponding hexadecimal colors.
    - ...
    """

    # Initialize attributes that we take directly from the CadetCareerProblem instance
    self.p, self.vp = instance.parameters, instance.value_parameters
    self.b, self.data_name, self.data_version = instance.mdl_p, instance.data_name, instance.data_version
    self.solution, self.mdl_p = instance.solution, instance.mdl_p
    self.paths = instance.export_paths
    self.printing = printing

    # Load in hex values/colors
    filepath = afccp.globals.paths['files'] + 'value_hex_translation.xlsx'
    if self.mdl_p['use_rainbow_hex']:
        hex_df = afccp.globals.import_data(filepath, sheet_name='Rainbow')
    else:
        hex_df = afccp.globals.import_data(filepath)
    self.v_hex_dict = {hex_df.loc[i, 'Value']: hex_df.loc[i, 'Hex'] for i in range(len(hex_df))}

    # Figure Height
    self.b['fh'] = self.b['fw'] * self.b['fh_ratio']

    # Border Widths
    for i in ['t', 'l', 'r', 'b', 'u']:
        self.b['bw^' + i] = self.b['fw'] * self.b['bw^' + i + '_ratio']

    # AFSC border/buffer widths
    self.b['abw^lr'] = self.b['fw'] * self.b['abw^lr_ratio']
    self.b['abw^ud'] = self.b['fw'] * self.b['abw^ud_ratio']

    # Legend width/height
    if self.b['add_legend']:
        self.b['lw'] = self.b['fw'] * self.b['lw_ratio']
        self.b['lh'] = self.b['fw'] * self.b['lh_ratio']
    else:
        self.b['lw'], self.b['lh'] = 0, 0

    # Set up "solutions" properly
    if 'iterations' in self.solution:
        self.b['solutions'] = copy.deepcopy(self.solution['iterations']['matches'])
        self.b['last_s'] = self.solution['iterations']['last_s']
        if 'rejections' in self.solution['iterations']:  # OTS specific alg functionality
            self.b['rejections'] = copy.deepcopy(self.solution['iterations']['rejections'])
            self.b['new_match'] = copy.deepcopy(self.solution['iterations']['new_match'])
    else:
        self.b['solutions'] = {0: self.solution['j_array']}
        self.b['last_s'] = 0

    # Basic information about this sequence for the animation
    self.b['afscs'] = self.mdl_p['afscs']

    # Determine which cadets were solved for in this solution
    if self.b['cadets_solved_for'] is None:
        self.b['cadets_solved_for'] = self.solution['cadets_solved_for']

    # Rated cadets only
    if 'Rated' in self.b['cadets_solved_for']:
        for soc in self.p['SOCs']:
            if soc.upper() in self.b['cadets_solved_for']:
                self.b['cadets'] = self.p['Rated Cadets'][soc]
                self.b['max_afsc'] = self.p[f'{soc}_quota']
                self.b['min_afsc'] = self.p[f'{soc}_quota']
                self.b['afscs'] = determine_soc_rated_afscs(soc, all_rated_afscs=self.p['afscs_acc_grp']["Rated"])
                self.soc = soc
                break

    # Specific SOC cadets!
    elif 'USAFA' in self.b['cadets_solved_for'] or 'ROTC' in self.b['cadets_solved_for'] or \
            'OTS' in self.b['cadets_solved_for']:
        for soc in self.p['SOCs']:
            if soc.upper() in self.b['cadets_solved_for']:
                if soc.upper() in self.b['cadets_solved_for']:
                    self.b['cadets'] = self.p[f'I^{soc.upper()}']
                    self.b['max_afsc'] = self.p[f'{soc}_quota']
                    self.b['min_afsc'] = self.p[f'{soc}_quota']
                    self.soc = soc
                    break

    # All the cadets!
    else:
        self.b['cadets'] = self.p['I']
        if 'max_bubbles' in self.p:
            self.b['max_afsc'] = self.p['max_bubbles']
        else:
            self.b['max_afsc'] = self.p['quota_max']
        self.b['min_afsc'] = self.p['pgl']
        self.soc = 'both'

    # Correct cadet parameters
    self.b['N'] = len(self.b['cadets'])

    # Correct AFSC parameters
    self.b['M'] = len(self.b['afscs'])
    self.b['J'] = np.array([np.where(afsc == self.p['afscs'])[0][0] for afsc in self.b['afscs']])

    # These are attributes to use in the title of each iteration
    self.num_unmatched = self.b['N']
    self.average_afsc_choice = None
    self.average_cadet_choice = None

    # Setup OTS algorithm specifics
    if self.solution.get('iterations', {}).get('type') == 'OTS Status Quo Algorithm':
        self.b['max_assigned'] = self.p['ots_quota'] + 1

        # Precompute intersection for each solution and AFSC
        self.b['cadets_matched'] = {
            s: {
                j: np.intersect1d(np.where(self.b['solutions'][s] == j)[0], self.p['I^OTS'])
                for j in self.b['J']
            }
            for s in self.b['solutions']
        }

    # Initialize Figure
    self.fig, self.ax = plt.subplots(figsize=self.b['b_figsize'], dpi=self.b['dpi'],
                                     facecolor=self.b['figure_color'], tight_layout=True)
    self.ax.set_facecolor(self.b['figure_color'])
    self.ax.set_aspect('equal', adjustable='box')
    self.ax.set(xlim=(-self.b['x_ext_left'], self.b['fw'] + self.b['x_ext_right']))
    self.ax.set(ylim=(-self.b['y_ext_left'], self.b['fh'] + self.b['y_ext_right']))

    # Remove tick marks
    self.ax.tick_params(left=False, bottom=False, labelleft=False, labelbottom=False)

    if printing:
        print('Bubble Chart initialized.')

main()

Main method to call all other methods based on what parameters the user provides

Source code in afccp/visualizations/bubbles.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
def main(self):
    """
    Main method to call all other methods based on what parameters the user provides
    """

    # Run through some initial preprocessing (calculating 'n' for example)
    self.preprocessing()
    x_y_initialized = self.import_board_parameters()  # Potentially import x and y

    # If we weren't able to initialize x and y coordinates for the board, determine that here
    if not x_y_initialized:

        # Determine x and y coordinates (and potentially 's')
        if self.b['use_pyomo_model']:
            self.calculate_afsc_x_y_s_through_pyomo()
        else:
            self.calculate_afsc_x_y_through_algorithm()

    # Redistribute the AFSCs along each row by spacing out the x coordinates
    if self.b['redistribute_x']:
        self.redistribute_x_along_row()

    # Only saving one image for a single solution
    if 'iterations' not in self.solution:
        self.b['save_iteration_frames'] = False
        self.b['build_orientation_slides'] = False
        self.b['save_board_default'] = False

    # Create the rest of the main figure
    self.calculate_cadet_box_x_y()

    # Save the board parameters
    self.export_board_parameters()

    # Build out the orientation slides
    if self.b['build_orientation_slides']:

        # Orientation slides first
        self.orientation_slides()
    else:

        # Initialize the board!
        self.initialize_board(include_surplus=self.b['show_white_surplus_boxes'])

    # Just making one picture
    if 'iterations' not in self.solution:
        self.solution_iteration_frame(0, cadets_to_show='cadets_matched', kind='Final Solution')

        # Save frame to solution sub-folder with solution name
        filepath = self.paths['Analysis & Results'] + self.solution['name'] + '/' + self.solution['name'] + ' ' +\
                   self.b['chart_filename'] + '.png'
        self.fig.savefig(filepath)

        if self.printing:
            print('Done.')

    # Create all the iteration frames
    if self.b['save_iteration_frames']:

        # Make the "focus" directory if needed
        folder_path = self.paths['Analysis & Results'] + 'Cadet Board/' + self.solution['iterations']['sequence']
        if self.b['focus'] not in os.listdir(folder_path):
            os.mkdir(folder_path + '/' + self.b['focus'])

        # ROTC Rated Board
        if self.solution['iterations']['type'] in ['ROTC Rated Board']:

            if self.printing:
                print("Creating " + str(len(self.solution['iterations']['matches'])) + " animation images...")

            # Loop through each solution
            for s in self.b['solutions']:
                self.solution_iteration_frame(s, cadets_to_show='cadets_matched')

        # Matching Algorithm Proposals & Rejections
        elif self.solution['iterations']['type'] in ['HR', 'Rated SOC HR']:

            if self.printing:  # "Plus 2" to account for orientation and final solution frames
                print("Creating " + str(len(self.b['solutions']) + 2) + " animation images...")

            # Save the orientation slide
            filepath = folder_path + '/' + self.b['focus'] + '/0 (Orientation).png'
            self.fig.savefig(filepath)

            # Loop through each iteration
            for s in self.b['solutions']:
                self.solution_iteration_frame(s, cadets_to_show='cadets_proposing', kind='Proposals')
                self.rejections_iteration_frame(s, kind='Rejections')

            # Final Solution
            self.solution_iteration_frame(s, cadets_to_show='cadets_matched', kind='Final Solution')
            if self.printing:
                print('Done.')

        elif self.solution['iterations']['type'] == 'OTS Status Quo Algorithm':

            if self.printing:  # "Plus 2" to account for orientation and final solution frames
                print("Creating " + str(len(self.b['solutions'].keys()) + 2) + " animation images...")

            # Save the frames
            self.build_ots_alg_frames()

preprocessing()

This method preprocesses the different specs for this particular figure instance

Source code in afccp/visualizations/bubbles.py
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
def preprocessing(self):
    """
    This method preprocesses the different specs for this particular figure instance
    """

    # Default AFSC fontsize and whether they're on two lines or not (likely overwritten later)
    self.b['afsc_fontsize'] = {j: self.b['afsc_title_size'] for j in self.b['J']}
    self.b['afsc_title_two_lines'] = {j: False for j in self.b['J']}

    # Proposal iterations
    if 'iterations' in self.solution:
        if 'proposals' in self.solution['iterations']:
            self.b['cadets_proposing'] = {}

    # Determine maximum number of assigned cadets to each AFSC
    if 'max_assigned' not in self.b:

        # Maximum number of cadets assigned to each AFSC across solutions
        self.b['max_assigned'] = {j: 0 for j in self.b["J"]}

        # Subset of cadets assigned to the AFSC in each solution
        self.b['counts'] = {}
        self.b['cadets_matched'] = {}

        # Loop through each solution (iteration)
        for s in self.b['solutions']:

            self.b['cadets_matched'][s], self.b['counts'][s] = {}, {}

            # Proposal iterations
            if 'iterations' in self.solution:
                if 'proposals' in self.solution['iterations']:
                    self.b['cadets_proposing'][s] = {}

            # Loop through each AFSC
            for j in self.b['J']:

                # The cadets that were matched have to be taken from the cadets we're showing too
                all_cadets_matched = np.where(self.b['solutions'][s] == j)[0]  # cadets assigned to this AFSC
                self.b['cadets_matched'][s][j] = np.intersect1d(all_cadets_matched, self.b['cadets'])
                self.b['counts'][s][j] = len(self.b['cadets_matched'][s][j])  # number of cadets assigned to AFSC
                max_count = self.b['counts'][s][j]

                # Proposal iterations
                if 'iterations' in self.solution:
                    if 'proposals' in self.solution['iterations']:
                        self.b['cadets_proposing'][s][j] = \
                            np.where(self.solution['iterations']['proposals'][s] == j)[0]
                        proposal_counts = len(self.b['cadets_proposing'][s][j])  # number of proposing cadets
                        max_count = max(self.b['counts'][s][j], proposal_counts)

                # Update maximum number of cadets assigned if necessary
                if max_count > self.b['max_assigned'][j]:
                    self.b['max_assigned'][j] = max_count

    # Get number of unassigned cadets at the end of the iterations
    if 'last_s' in self.b:  # cadets left unmatched
        self.b['unassigned_cadets'] = np.where(self.b['solutions'][self.b['last_s']] == self.p['M'])[0]
        self.b['N^u'] = len(self.b['unassigned_cadets'])  # number of cadets left unmatched

    # Determine number of cadet boxes for AFSCs based on nearest square
    squares_required = [max(self.b['max_assigned'][j], self.b['max_afsc'][j]) for j in self.b['J']]
    n = np.ceil(np.sqrt(squares_required)).astype(int)
    n2 = (np.ceil(np.sqrt(squares_required)) ** 2).astype(int)
    self.b['n'] = {j: n[idx] for idx, j in enumerate(self.b['J'])}
    self.b['n^2'] = {j: n2[idx] for idx, j in enumerate(self.b['J'])}

    # Number of boxes in row of unmatched box
    self.b['n^u'] = int((self.b['fw'] - self.b['bw^r'] - self.b['bw^l']) / self.b['s'])

    # Number of rows in unmatched box
    self.b['n^urow'] = int(self.b['N^u'] / self.b['n^u'])

    # Sort the AFSCs by 'n' (descending), then by AFSC name (ascending)
    n = np.array([self.b['n'][j] for j in self.b['J']])  # Convert dictionary to numpy array
    afsc_names = np.array([self.b['afscs'][j] for j in self.b['J']])

    # Use lexsort: secondary key goes first (name), then primary (negated size for descending)
    indices = np.lexsort((afsc_names, -n))
    sorted_J = self.b['J'][indices]  # J Array sorted by n
    sorted_n = n[indices]  # n Array sorted by n
    self.b['J^sorted'] = {index: sorted_J[index] for index in range(self.b['M'])}  # Translate 'new j' to 'real j'
    self.b['n^sorted'] = {index: sorted_n[index] for index in range(self.b['M'])}  # Translate 'new n' to 'real n'
    self.b['J^translated'] = {sorted_J[index]: index for index in range(self.b['M'])}  # Translate 'real j' to 'new j'

    if self.printing:
        print('Bubble Chart preprocessed.')

orientation_slides()

Build out the orientation slides for a particular sequence (intended to be used on ONE AFSC)

Source code in afccp/visualizations/bubbles.py
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
def orientation_slides(self):
    """
    Build out the orientation slides for a particular sequence (intended to be used on ONE AFSC)
    """

    # Make the "orientation" directory if needed
    folder_path = self.paths['Analysis & Results'] + 'Cadet Board/' + self.solution['iterations']['sequence']
    if 'Orientation' not in os.listdir(folder_path):
        os.mkdir(folder_path + '/Orientation')

    # Save the "zero" slide (just black screen)
    filepath = folder_path + '/Orientation/0.png'
    self.fig.savefig(filepath)

    # Create first frame
    self.initialize_board(include_surplus=False)

    # Save the real first frame
    filepath = folder_path + '/Orientation/1.png'
    self.fig.savefig(filepath)

    # Reset Figure
    self.fig, self.ax = plt.subplots(figsize=self.b['b_figsize'], dpi=self.b['dpi'],
                                     facecolor=self.b['figure_color'], tight_layout=True)
    self.ax.set_facecolor(self.b['figure_color'])
    self.ax.set_aspect('equal', adjustable='box')
    self.ax.set(xlim=(-self.b['x_ext_left'], self.b['fw'] + self.b['x_ext_right']))
    self.ax.set(ylim=(-self.b['y_ext_left'], self.b['fh'] + self.b['y_ext_right']))

    # Create second frame
    self.initialize_board(include_surplus=True)

    # Save the second frame
    filepath = folder_path + '/Orientation/2.png'
    self.fig.savefig(filepath)

calculate_afsc_x_y_through_algorithm()

This method calculates the x and y locations of the AFSC boxes using a very simple algorithm.

Source code in afccp/visualizations/bubbles.py
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
def calculate_afsc_x_y_through_algorithm(self):
    """
    This method calculates the x and y locations of the AFSC boxes using a very simple algorithm.
    """

    # Determine x and y coordinates of bottom left corner of AFSC squares algorithmically
    self.b['x'], self.b['y'] = {j: 0 for j in self.b['J']}, {j: 0 for j in self.b['J']}
    n = np.array([self.b['n'][j] for j in self.b['J']])  # Convert dictionary to numpy array

    # Start at top left corner of main container (This is the algorithm)
    x, y = self.b['bw^l'], self.b['fh'] - self.b['bw^t']
    current_max_n = np.max(n)
    for j in self.b['J']:
        check_x = x + self.b['s'] * self.b['n'][j]

        if check_x > self.b['fw'] - self.b['bw^r'] - self.b['lw']:
            x = self.b['bw^l']  # move back to left-most column
            y = y - self.b['s'] * current_max_n - self.b['abw^ud']  # drop to next row
            current_max_n = self.b['n'][j]

        self.b['x'][j], self.b['y'][j] = x, y - self.b['s'] * self.b['n'][j]  # bottom left corner of box
        x += self.b['s'] * self.b['n'][j] + self.b['abw^lr']  # move over to next column

    if self.printing:
        print("Board parameters 'x' and 'y' determined through simple algorithm.")

calculate_afsc_x_y_s_through_pyomo()

This method calculates the x and y locations of the AFSC boxes, as well as the size (s) of the cadet boxes, using the pyomo optimization model to determine the optimal placement of all these objects

Source code in afccp/visualizations/bubbles.py
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
def calculate_afsc_x_y_s_through_pyomo(self):
    """
    This method calculates the x and y locations of the AFSC boxes, as well as the size (s) of the cadet boxes,
    using the pyomo optimization model to determine the optimal placement of all these objects
    """

    if not afccp.globals.use_pyomo:
        raise ValueError("Pyomo not installed.")

    # Build the model
    model = afccp.solutions.optimization.bubble_chart_configuration_model(self.b)

    # Get coordinates and size of boxes by solving the model
    self.b['s'], self.b['x'], self.b['y'] = afccp.solutions.optimization.solve_pyomo_model(
        self, model, "Bubbles", q=None, printing=self.printing)

    if self.printing:
        print("Board parameters 'x' and 'y' determined through pyomo model.")

redistribute_x_along_row()

This method re-calculates the x coordinates by spacing out the AFSCs along each row

Source code in afccp/visualizations/bubbles.py
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
def redistribute_x_along_row(self):
    """
    This method re-calculates the x coordinates by spacing out the AFSCs along each row
    """

    # Unique y coordinates
    y_unique = np.unique(np.array([round(self.b['y'][j], 4) for j in self.b['J']]))[::-1]

    # Need to get ordered list of AFSCs in each row
    sorted_J = np.array([j for j in self.b['J^translated']])
    rows = {row: [] for row in range(len(y_unique))}
    for j in sorted_J:
        y = round(self.b['y'][j], 4)
        row = np.where(y_unique == y)[0][0]
        rows[row].append(j)

    # Loop through each row to determine optimal spacing
    for row in rows:

        # Only adjust spacing for rows with more than one AFSC
        if len(rows[row]) > 1:

            # Calculate total spacing to play around with
            total_spacing = self.b['fw'] - self.b['bw^l'] - self.b['bw^r']
            for j in rows[row]:
                total_spacing -= (self.b['s'] * self.b['n'][j])

            # Spacing used to fill in the gaps
            new_spacing = total_spacing / (len(rows[row]) - 1)

            # Loop through each AFSC in this row to calculate the new x position
            for num, j in enumerate(rows[row]):

                # Calculate the appropriate x coordinate
                if num == 0:
                    x = self.b['x'][j] + (self.b['n'][j] * self.b['s']) + new_spacing
                else:
                    self.b['x'][j] = x
                    x += (self.b['n'][j] * self.b['s']) + new_spacing

calculate_cadet_box_x_y()

This method uses the x and y coordinates of the AFSC boxes, along with the size of the cadet boxes, to calculate the x and y coordinates of all the individual cadet boxes.

Source code in afccp/visualizations/bubbles.py
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
def calculate_cadet_box_x_y(self):
    """
    This method uses the x and y coordinates of the AFSC boxes, along with the size of the cadet boxes, to calculate
    the x and y coordinates of all the individual cadet boxes.
    """

    # Get coordinates of all cadet boxes
    self.b['cb_coords'] = {}
    for j in self.b['J']:
        self.b['cb_coords'][j] = {}

        # Bottom left corner of top left cadet square
        x, y = self.b['x'][j], self.b['y'][j] + self.b['s'] * (self.b['n'][j] - 1)

        # Loop through all cadet boxes to get individual coordinates of bottom left corner of each cadet box
        i = 0
        for r in range(self.b['n'][j]):
            for c in range(self.b['n'][j]):
                x_i = x + c * self.b['s']
                y_i = y - r * self.b['s']
                self.b['cb_coords'][j][i] = (x_i, y_i)
                i += 1

initialize_board(include_surplus=False)

This method takes all the necessary board parameters and constructs the board to then be manipulated in other algorithms based on what the user wants to do.

Source code in afccp/visualizations/bubbles.py
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
def initialize_board(self, include_surplus=False):
    """
    This method takes all the necessary board parameters and constructs the board to then be manipulated in other
    algorithms based on what the user wants to do.
    """

    # Is there only one row?
    max_y = np.max(np.array([self.b['y'][j] for j in self.b['J']]))
    only_one_row = max_y <= 0.04

    # Loop through each AFSC to add certain elements
    self.b['afsc_name_text'] = {}
    self.b['c_boxes'] = {}
    self.b['c_circles'] = {}
    self.b['c_rank_text'] = {}
    for j in self.b['J']:

        # AFSC names
        if self.b['afsc_names_sized_box']:

            # Calculate fontsize and put AFSC name in middle of box
            x = self.b['x'][j] + (self.b['n'][j] / 2) * self.b['s']
            y = self.b['y'][j] + (self.b['n'][j] / 2) * self.b['s']
            w, h = self.b['n'][j] * self.b['s'], self.b['n'][j] * self.b['s']
            self.b['afsc_fontsize'][j] = get_fontsize_for_text_in_box(self.ax, self.p['afscs'][j], (x, y), w, h,
                                                                   va='center')
            va = 'center'
            ha = 'center'
        else:

            # AFSC fontsize is given and put AFSC name above box
            x = self.b['x'][j] + (self.b['n'][j] / 2) * self.b['s']
            y = self.b['y'][j] + self.b['n'][j] * self.b['s'] + 0.02
            va = 'bottom'
            ha = 'center'

            self.b['x'] = {key: round(val, 4) for key, val in self.b['x'].items()}
            self.b['y'] = {key: round(val, 4) for key, val in self.b['y'].items()}

            # Are we on a bottom edge?
            row = np.array([j_p for j_p, val in self.b['y'].items() if val == self.b['y'][j]])
            x_coords = np.array([self.b['x'][j_p] for j_p in row])
            if not only_one_row:
                if self.b['x'][j] == np.max(x_coords) and self.b['y'][j] <= self.b['y_val_to_pin']:
                    x = self.b['x'][j] + (self.b['n'][j]) * self.b['s']  # We're at the right edge
                    ha = 'right'
                elif self.b['x'][j] == np.min(x_coords) and self.b['y'][j] <= self.b['y_val_to_pin']:
                    x = self.b['x'][j]  # We're at the left edge
                    ha = 'left'

        # AFSC text
        self.b['afsc_name_text'][j] = self.ax.text(x, y, self.p['afscs'][j], fontsize=self.b['afsc_fontsize'][j],
                                                   horizontalalignment=ha, verticalalignment=va,
                                                   color=self.b['text_color'])

        # Cadet box text size
        cb_s = get_fontsize_for_text_in_box(self.ax, "0", (0, 0), self.b['s'], self.b['s'], va='center')

        # Loop through each cadet to add the cadet boxes and circles
        self.b['c_boxes'][j] = {}
        self.b['c_circles'][j] = {}
        self.b['c_rank_text'][j] = {}
        for i in range(self.b['n^2'][j]):  # All cadet boxes

            # If we are under the maximum number of cadets allowed
            if i + 1 <= self.b['max_afsc'][j]:

                # Boxes based on SOC PGL Breakouts
                if 'SOC PGL' in self.b['focus']:

                    # If we are under the USAFA PGL
                    if i + 1 <= self.p['usafa_quota'][j]:
                        linestyle = self.b['pgl_linestyle']
                        color = self.b['usafa_pgl_color']
                        alpha = self.b['pgl_alpha']

                    # We're in the ROTC range
                    elif i + 1 <= self.p['usafa_quota'][j] + self.p['rotc_quota'][j]:
                        linestyle = self.b['pgl_linestyle']
                        color = self.b['rotc_pgl_color']
                        alpha = self.b['pgl_alpha']

                    # 'Surplus' Range
                    else:
                        linestyle = self.b['surplus_linestyle']
                        color = self.b['surplus_color']
                        alpha = self.b['surplus_alpha']

                else:

                    # If we are under the PGL
                    if i + 1 <= self.b['min_afsc'][j]:
                        linestyle = self.b['pgl_linestyle']
                        color = self.b['pgl_color']
                        alpha = self.b['pgl_alpha']

                    # 'Surplus' Range
                    else:
                        linestyle = self.b['surplus_linestyle']
                        color = self.b['surplus_color']
                        alpha = self.b['surplus_alpha']

                # Make the rectangle patch (cadet box)
                self.b['c_boxes'][j][i] = patches.Rectangle(self.b['cb_coords'][j][i], self.b['s'], self.b['s'],
                                                            linestyle=linestyle, linewidth=1, facecolor=color,
                                                            alpha=alpha, edgecolor=self.b['cb_edgecolor'])

                # Add the patch to the figure
                if include_surplus or linestyle == self.b['pgl_linestyle']:
                    self.ax.add_patch(self.b['c_boxes'][j][i])

            # If we are under the maximum number of cadets assigned to this AFSC across the solutions
            if i + 1 <= self.b['max_assigned'][j]:

                # Make the circle patch (cadet)
                x, y = self.b['cb_coords'][j][i][0] + (self.b['s'] / 2), \
                       self.b['cb_coords'][j][i][1] + (self.b['s'] / 2)
                self.b['c_circles'][j][i] = patches.Circle(
                    (x, y), radius = (self.b['s'] / 2) * self.b['circle_radius_percent'], linestyle='-', linewidth=1,
                    facecolor='black', alpha=1, edgecolor='black')

                # Add the patch to the figure
                self.ax.add_patch(self.b['c_circles'][j][i])

                # Hide the circle
                self.b['c_circles'][j][i].set_visible(False)

                # We may want to include rank text on the cadets
                if self.b['show_rank_text']:
                    self.b['c_rank_text'][j][i] = self.ax.text(x, y, '0', fontsize=cb_s, horizontalalignment='center',
                                                               verticalalignment='center',
                                                               color=self.b['rank_text_color'])
                    self.b['c_rank_text'][j][i].set_visible(False)

    # Remove tick marks
    self.ax.tick_params(left=False, bottom=False, labelleft=False, labelbottom=False)

    # Add the title
    if self.b['b_title'] is None:
        title = "Round 0 (Orientation)"
    else:
        title = self.b['b_title']
    self.fig.suptitle(title, fontsize=self.b['b_title_size'], color=self.b['text_color'])

    # Add the legend if necessary
    if self.b['b_legend']:
        self.create_legend()

    # Save the figure
    if self.b['save_board_default']:
        folder_path = self.paths['Analysis & Results'] + 'Cadet Board/'
        if self.solution['iterations']['sequence'] not in os.listdir(folder_path):
            os.mkdir(folder_path + self.b['sequence'])

        # Get the filepath and save the "default" graph
        filepath = folder_path + self.solution['iterations']['sequence'] + '/Default Board'
        if type(self.mdl_p['afscs_to_show']) == str:
            filepath += ' (' + self.mdl_p['afscs_to_show'] + ' Cadets).png'
        else:
            filepath += ' (M = ' + str(self.b['M']) + ').png'
        self.fig.savefig(filepath)

solution_iteration_frame(s, cadets_to_show='cadets_matched', kind=None)

This method reconstructs the figure to reflect the cadet/afsc state in this iteration

Source code in afccp/visualizations/bubbles.py
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
def solution_iteration_frame(self, s, cadets_to_show='cadets_matched', kind=None):
    """
    This method reconstructs the figure to reflect the cadet/afsc state in this iteration
    """

    # AFSC Normalized scores
    self.b['scores'] = {j: 0 for j in self.b['J']}

    # Loop through each AFSC
    for j in self.b['J']:

        # Sort the cadets based on whatever method we choose
        unsorted_cadets = self.b[cadets_to_show][s][j]
        cadets = self.sort_cadets(j, unsorted_cadets)

        # Make sure we have cadets assigned to this AFSC in this frame
        if len(cadets) > 0:

            # Change the colors of the circles based on the desired method
            self.change_circle_features(s, j, cadets)

            # Hide the circles/text that aren't in the solution
            for i in range(len(cadets), self.b['max_assigned'][j]):

                # Hide the circle
                self.b['c_circles'][j][i].set_visible(False)

                # If rank text is included
                if self.b['show_rank_text']:
                    self.b['c_rank_text'][j][i].set_visible(False)

            # Update the text above the AFSC square
            self.update_afsc_text(s, j)

        else:  # There aren't any assigned cadets yet!
            self.b['afsc_name_text'][j].set_color('white')
            self.b['afsc_name_text'][j].set_text(self.p['afscs'][j] + ": 0")

    # Update the title of the figure
    self.update_title_text(s, kind=kind)

    # Save the figure
    if self.b['save_iteration_frames']:
        self.save_iteration_frame(s, kind=kind)

build_ots_alg_frames()

This method reconstructs the figure to reflect the cadet/afsc state in this iteration

Source code in afccp/visualizations/bubbles.py
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
def build_ots_alg_frames(self):
    """
    This method reconstructs the figure to reflect the cadet/afsc state in this iteration
    """

    # AFSC Normalized scores
    self.b['scores'] = {j: 0 for j in self.b['J']}

    # Initialize AFSC text
    for j in self.b['J']:
        self.b['afsc_name_text'][j].set_color('white')
        self.b['afsc_name_text'][j].set_text(self.p['afscs'][j] + ": 0")

    iterations = [s for s in self.b['solutions']]
    for s in tqdm(iterations, desc="Animation Image"):

        # AFSC Normalized scores
        self.b['scores'] = {j: 0 for j in self.b['J']}

        # Loop through each AFSC
        for j in self.b['J']:

            # Sort the cadets based on whatever method we choose
            unsorted_cadets = self.b['cadets_matched'][s][j]
            cadets = self.sort_cadets(j, unsorted_cadets)

            # Make sure we have cadets assigned to this AFSC in this frame
            if len(cadets) > 0:

                # Change the colors of the circles based on the desired method
                self.change_circle_features(s, j, cadets)

                # Hide the circles/text that aren't in the solution
                for i in range(len(cadets), self.b['max_assigned'][j]):

                    # Hide the circle
                    self.b['c_circles'][j][i].set_visible(False)

                # Update the text above the AFSC square
                self.update_afsc_text(s, j)

            else:  # There aren't any assigned cadets yet!
                self.b['afsc_name_text'][j].set_color('white')
                self.b['afsc_name_text'][j].set_text(self.p['afscs'][j] + ": 0")

        # Get the location of this new cadet-AFSC pair
        i, j = self.b['new_match'][s]
        unsorted_cadets = self.b['cadets_matched'][s][j]
        cadets = self.sort_cadets(j, unsorted_cadets)
        idx = np.where(cadets == i)[0][0]

        # Highlight this new cadet-AFSC pair!
        self.b['c_circles'][j][idx].set_edgecolor('yellow')

        # Update the title of the figure
        self.update_title_text(s, kind='OTS Algorithm')

        # Save the figure
        self.save_iteration_frame(s, kind='Matched')

        # Is this a rejected match?
        if s in self.b['rejections']:

            # Get line coordinates
            x_values_1 = [self.b['cb_coords'][j][idx][0], self.b['cb_coords'][j][idx][0] + self.b['s']]
            y_values_1 = [self.b['cb_coords'][j][idx][1], self.b['cb_coords'][j][idx][1] + self.b['s']]
            x_values_2 = [self.b['cb_coords'][j][idx][0], self.b['cb_coords'][j][idx][0] + self.b['s']]
            y_values_2 = [self.b['cb_coords'][j][idx][1] + self.b['s'], self.b['cb_coords'][j][idx][1]]

            # Plot the 'Big Red X' lines
            line_1 = self.ax.plot(x_values_1, y_values_1, linestyle='-', c='red')[0]
            line_2 = self.ax.plot(x_values_2, y_values_2, linestyle='-', c='red')[0]

            # Save the figure
            self.save_iteration_frame(s, kind='Rejected')

            # Remove the lines and the cadet
            line_1.remove()
            line_2.remove()
            self.b['c_circles'][j][idx].set_visible(False)  # Hide the circle

        # Remove the outline around this cadet-AFSC pair
        self.b['c_circles'][j][idx].set_edgecolor('black')

rejections_iteration_frame(s, kind='Rejections')

This method reconstructs the figure to reflect the cadet/afsc state in this iteration

Source code in afccp/visualizations/bubbles.py
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
def rejections_iteration_frame(self, s, kind='Rejections'):
    """
    This method reconstructs the figure to reflect the cadet/afsc state in this iteration
    """

    # Rejection 'Xs' lines
    line_1, line_2 = {}, {}

    # Loop through each AFSC
    for j in self.b['J']:

        # Sort the cadets based on whatever method we choose
        unsorted_cadets = self.b['cadets_proposing'][s][j]
        cadets_proposing = self.sort_cadets(j, unsorted_cadets)

        # Rejection lines
        line_1[j], line_2[j] = {}, {}
        for i, cadet in enumerate(cadets_proposing):
            if cadet not in self.b['cadets_matched'][s][j]:

                # Get line coordinates
                x_values_1 = [self.b['cb_coords'][j][i][0], self.b['cb_coords'][j][i][0] + self.b['s']]
                y_values_1 = [self.b['cb_coords'][j][i][1], self.b['cb_coords'][j][i][1] + self.b['s']]
                x_values_2 = [self.b['cb_coords'][j][i][0], self.b['cb_coords'][j][i][0] + self.b['s']]
                y_values_2 = [self.b['cb_coords'][j][i][1] + self.b['s'], self.b['cb_coords'][j][i][1]]

                # Plot the 'Big Red X' lines
                line_1[j][i] = self.ax.plot(x_values_1, y_values_1, linestyle='-', c='red')
                line_2[j][i] = self.ax.plot(x_values_2, y_values_2, linestyle='-', c='red')

    # Update the title of the figure
    self.update_title_text(s, kind=kind)

    # Save the figure
    if self.b['save_iteration_frames']:
        self.save_iteration_frame(s, kind)

    # Remove the "Big Red X" lines
    for j in self.b['J']:
        for i in line_1[j]:
            line = line_1[j][i].pop(0)
            line.remove()
            line = line_2[j][i].pop(0)
            line.remove()

sort_cadets(j, cadets_unsorted)

This method sorts the cadets in this frame through some means

Source code in afccp/visualizations/bubbles.py
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
def sort_cadets(self, j, cadets_unsorted):
    """
    This method sorts the cadets in this frame through some means
    """

    # Sort the cadets by SOC
    if self.b['focus'] == 'SOC PGL':
        indices = np.argsort(self.p['usafa'][cadets_unsorted])[::-1]

    # Sort the cadets by AFSC preferences
    elif self.mdl_p['sort_cadets_by'] == 'AFSC Preferences':
        indices = np.argsort(self.p['a_pref_matrix'][cadets_unsorted, j])

    # Sort the cadets by order of merit (OM)
    elif self.mdl_p['sort_cadets_by'] == 'OM':
        indices = np.argsort(self.p['merit'][cadets_unsorted])[::-1]

    # Return the sorted cadets
    return cadets_unsorted[indices]

change_circle_features(s, j, cadets)

This method determines the color and edgecolor of the circles to show

Source code in afccp/visualizations/bubbles.py
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
def change_circle_features(self, s, j, cadets):
    """
    This method determines the color and edgecolor of the circles to show
    """

    # Colors based on cadet utility
    if self.b['focus'] == 'Cadet Utility':
        utility = self.p['cadet_utility'][cadets, j]

        # Change the cadet circles to reflect the appropriate colors
        for i, cadet in enumerate(cadets):

            # Change circle color
            color = self.v_hex_dict[round(utility[i], 2)]
            self.b['c_circles'][j][i].set_facecolor(color)

            # Show the circle
            self.b['c_circles'][j][i].set_visible(True)

    elif self.b['focus'] == 'Cadet Choice':
        choice = self.p['c_pref_matrix'][cadets, j]

        # Change the cadet circles to reflect the appropriate colors
        for i, cadet in enumerate(cadets):

            # Change circle color
            if choice[i] in self.mdl_p['choice_colors']:
                color = self.mdl_p['choice_colors'][choice[i]]
            else:
                color = self.mdl_p['all_other_choice_colors']
            self.b['c_circles'][j][i].set_facecolor(color)

            # Show the circle
            self.b['c_circles'][j][i].set_visible(True)

    elif self.b['focus'] == 'Cadet Choice Categories':
        choice = self.p['c_pref_matrix'][cadets, j]

        # Change the cadet circles to reflect the appropriate colors
        for i, cadet in enumerate(cadets):

            # If the AFSC was a top 6 choice, we use that color
            if choice[i] in [1, 2, 3, 4, 5, 6]:
                color = self.mdl_p['choice_colors'][choice[i]]

            # If the AFSC was at least selected, we make it that color
            elif j in self.p['J^Selected'][cadet]:

                # Use the color for the 8th choice
                color = self.mdl_p['choice_colors'][8]

            # If the AFSC was not in the bottom 3 choices, we make it that color
            elif j not in self.p['J^Bottom 2 Choices'][cadet] and j != self.p['J^Last Choice'][cadet]:

                # Use the color for the 9th choice
                color = self.mdl_p['choice_colors'][9]

            # Otherwise, it's a bottom 3 choice
            else:
                color = self.mdl_p['all_other_choice_colors']
            self.b['c_circles'][j][i].set_facecolor(color)

            # Show the circle
            self.b['c_circles'][j][i].set_visible(True)

    elif self.b['focus'] == 'AFSC Choice':
        choice = self.p['afsc_utility'][cadets, j]

        # Change the cadet circles to reflect the appropriate colors
        for i, cadet in enumerate(cadets):

            value = round(choice[i], 2)

            # Change circle color
            color = self.v_hex_dict[value]
            self.b['c_circles'][j][i].set_facecolor(color)

            # Show the circle
            self.b['c_circles'][j][i].set_visible(True)

    elif self.b['focus'] == 'ROTC Rated Interest':
        afsc_index = np.where(self.b['J'] == j)[0][0]

        # Change the cadet circles to reflect the appropriate colors
        for i, cadet in enumerate(cadets):
            idx = self.p['Rated Cadet Index Dict']['rotc'][cadet]
            interest = self.p['rr_interest_matrix'][idx, afsc_index]

            # Change circle color
            color = self.mdl_p['interest_colors'][interest]
            self.b['c_circles'][j][i].set_facecolor(color)

            # Show the circle
            self.b['c_circles'][j][i].set_visible(True)

    elif self.b['focus'] == 'Reserves':

        # Change the cadet circles to reflect the appropriate colors
        for i, cadet in enumerate(cadets):
            if cadet in self.solution['iterations']['matched'][s]:
                color = self.b['matched_slot_color']
            elif cadet in self.solution['iterations']['reserves'][s]:
                color = self.b['reserved_slot_color']
            else:
                color = self.b['unmatched_color']

            # Change circle color
            self.b['c_circles'][j][i].set_facecolor(color)

            # Show the circle
            self.b['c_circles'][j][i].set_visible(True)

    elif self.b['focus'] == 'SOC PGL':

        # Change the cadet circles to reflect the appropriate colors
        for i, cadet in enumerate(cadets):
            if cadet in self.p['usafa_cadets']:
                color = self.b['usafa_bubble']
            else:
                color = self.b['rotc_bubble']

            # Change circle color
            self.b['c_circles'][j][i].set_facecolor(color)

            # Show the circle
            self.b['c_circles'][j][i].set_visible(True)

    elif self.b['focus'] == 'Rated Choice':

        # Change the cadet circles to reflect the appropriate colors
        for i, cadet in enumerate(cadets):
            rated_choices = self.p['Rated Choices'][self.soc][cadet]

            # Get color of this choice
            if j in rated_choices:
                choice = np.where(rated_choices == j)[0][0] + 1
            else:
                choice = 100  # Arbitrary big number

            # Change circle color
            if choice in self.mdl_p['choice_colors']:
                color = self.mdl_p['choice_colors'][choice]
            else:
                color = self.mdl_p['all_other_choice_colors']
            self.b['c_circles'][j][i].set_facecolor(color)

            # Show the circle
            self.b['c_circles'][j][i].set_visible(True)

    elif 'Specific Choice' in self.b['focus']:

        # Get the AFSC we're highlighting
        j_focus = np.where(self.p['afscs'] == self.mdl_p['afsc'])[0][0]
        choice = self.p['c_pref_matrix'][cadets, j_focus]

        # Change the cadet circles to reflect the appropriate colors
        for i, cadet in enumerate(cadets):

            # Change circle color
            if choice[i] in self.mdl_p['choice_colors']:
                color = self.mdl_p['choice_colors'][choice[i]]
            elif choice[i] == 0:  # Ineligible
                color = self.mdl_p['unfocused_color']
            else:  # All other choices
                color = self.mdl_p['all_other_choice_colors']
            self.b['c_circles'][j][i].set_facecolor(color)

            # Show the circle
            self.b['c_circles'][j][i].set_visible(True)

    elif 'Tier 1' in self.b['focus']:
        choice = self.p['c_pref_matrix'][cadets, j]

        # Get the AFSC we're highlighting
        j_focus = np.where(self.p['afscs'] == self.mdl_p['afsc'])[0][0]

        # Change the cadet circles to reflect the appropriate colors
        for i, cadet in enumerate(cadets):

            # Change circle color
            if '1' in self.p['qual'][cadet, j_focus]:
                if choice[i] in self.mdl_p['choice_colors']:
                    color = self.mdl_p['choice_colors'][choice[i]]
                else:
                    color = self.mdl_p['all_other_choice_colors']
            else:
                color = self.mdl_p['unfocused_color']
            self.b['c_circles'][j][i].set_facecolor(color)

            # Edgecolor (Don't worry about exception anymore)
            if 'E' in self.p['qual'][cadet, j_focus]:# and j == j_focus:
                self.b['c_circles'][j][i].set_edgecolor(self.mdl_p['exception_edge'])
            else:
                self.b['c_circles'][j][i].set_edgecolor(self.mdl_p['base_edge'])

            # Show the circle
            self.b['c_circles'][j][i].set_visible(True)

    # Cadet rank text
    if self.b['show_rank_text']:
        choice = self.p['a_pref_matrix'][cadets, j]
        for i, cadet in enumerate(cadets):
            txt = str(choice[i])
            x, y = self.b['cb_coords'][j][i][0] + (self.b['s'] / 2), \
                   self.b['cb_coords'][j][i][1] + (self.b['s'] / 2)
            w, h = self.b['s'] * self.b['circle_radius_percent'], self.b['s'] * self.b['circle_radius_percent']
            fontsize = get_fontsize_for_text_in_box(self.ax, txt, (x, y), w, h, va='center')

            # Adjust fontsize for single digit ranks
            if int(txt) < 10:
                fontsize = int(fontsize * self.b['fontsize_single_digit_adj'])
            self.b['c_rank_text'][j][i].set_text(txt)
            self.b['c_rank_text'][j][i].set_fontsize(fontsize)
            self.b['c_rank_text'][j][i].set_visible(True)

update_afsc_text(s, j)

This method updates the text above the AFSC squares

Source code in afccp/visualizations/bubbles.py
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
def update_afsc_text(self, s, j):
    """
    This method updates the text above the AFSC squares
    """

    # Set of ranks for all the cadets "considered" in this solution for this AFSC
    cadets_considered = np.intersect1d(self.b['cadets'], self.p['I^E'][j])
    ranks = self.p['a_pref_matrix'][cadets_considered, j]
    achieved_ranks = self.p['a_pref_matrix'][self.b['cadets_matched'][s][j], j]

    # Calculate AFSC Norm Score and use it in the new text
    self.b['scores'][j] = round(afccp.solutions.handling.calculate_afsc_norm_score_general(
        ranks, achieved_ranks), 2)

    # If we want to put this AFSC title on two lines or not
    if self.b['afsc_title_two_lines'][j]:
        afsc_text = self.p['afscs'][j] + ":\n"
    else:
        afsc_text = self.p['afscs'][j] + ": "

    # Change the text for the AFSCs
    if self.b['focus'] == 'SOC PGL':
        more = 'neither'
        for soc, other_soc in {'usafa': 'rotc', 'rotc': 'usafa'}.items():
            soc_cadets = len(np.intersect1d(self.b['cadets_matched'][s][j], self.p[soc + '_cadets']))
            soc_pgl = self.p[soc + '_quota'][j]
            diff = soc_cadets - soc_pgl
            if diff > 0:
                more = soc
                afsc_text += '+' + str(diff)

        if more == 'neither':
            color = 'white'
            afsc_text += '+0'
        else:
            color = self.b[more + '_bubble']
        self.b['afsc_name_text'][j].set_color(color)

    elif self.b['afsc_text_to_show'] == 'Norm Score':
        color = self.v_hex_dict[self.b['scores'][j]]  # New AFSC color
        afsc_text += str(self.b['scores'][j])
        self.b['afsc_name_text'][j].set_color(color)

    # Determine average cadet choice and use it in the new text
    elif self.b['afsc_text_to_show'] == 'Cadet Choice':
        average_choice = round(np.mean(self.p['c_pref_matrix'][self.b['cadets_matched'][s][j], j]), 2)
        color = 'white'
        afsc_text += str(average_choice)
        self.b['afsc_name_text'][j].set_color(color)

    # Text shows number of cadets matched/proposing
    else:
        afsc_text += str(len(self.b['cadets_matched'][s][j]))

    # Update the text
    self.b['afsc_name_text'][j].set_text(afsc_text)

update_title_text(s, kind=None)

This method purely updates the text in the title of the figure

Source code in afccp/visualizations/bubbles.py
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
def update_title_text(self, s, kind=None):
    """
    This method purely updates the text in the title of the figure
    """

    # Change the text and color of the title
    if kind == 'Proposals':
        title_text = 'Round ' + str(s + 1) + ' (Proposals)'

        # Get the color of the title
        if s + 1 in self.b['choice_colors']:
            title_color = self.b['choice_colors'][s + 1]
        else:
            title_color = self.b['all_other_choice_colors']
    else:
        title_color = self.b['text_color']

        # Update the title text in a specific way
        if kind == 'Final Solution':
            title_text = 'Solution'
        elif kind == 'Rejections':
            title_text = 'Round ' + str(s + 1) + ' (Rejections)'

            # Get the color of the title
            if s + 1 in self.b['choice_colors']:
                title_color = self.b['choice_colors'][s + 1]
            else:
                title_color = self.b['all_other_choice_colors']
        else:
            title_text = self.solution['iterations']['names'][s]

    # All unmatched cadets in the solution (even the ones we're not considering)
    unmatched_cadets_all = np.where(self.b['solutions'][s] == self.p['M'])[0]

    # Unmatched cadets that we're concerned about in this solution (This really just applies to Rated)
    unmatched_cadets = np.intersect1d(unmatched_cadets_all, self.b['cadets'])
    self.num_unmatched = len(unmatched_cadets)
    matched_cadets = np.array([i for i in self.b['cadets'] if i not in unmatched_cadets])

    # Calculate average cadet choice based on matched cadets
    choices = np.zeros(len(matched_cadets))
    for idx, i in enumerate(matched_cadets):
        j = self.b['solutions'][s][i]
        choices[idx] = self.p['c_pref_matrix'][i, j]
    self.average_cadet_choice = round(np.mean(choices), 2)

    # Calculate AFSC weighted average score (and add number of unmatched cadets)
    counts = np.array([len(np.where(self.b['solutions'][s] == j)[0]) for j in self.b['J']])
    weights = counts / np.sum(counts)
    scores = np.array([self.b['scores'][j] for j in self.b['J']])
    self.average_afsc_choice = round(np.dot(weights, scores), 2)

    # Add title text
    if self.b['focus'] in ['Specific Choice', 'Tier 1']:
        title_text += ' Highlighting Results for ' + self.mdl_p['afsc']
    elif kind not in ['OTS Algorithm']:
        percent_text = str(np.around(self.solution['top_3_choice_percent'] * 100, 3)) + "%"
        title_text += ' Results: Cadet Top3: ' + percent_text
        title_text += ', AFSC Score: ' + str(np.around(self.average_afsc_choice, 2))
        if 'z^CASTLE (Values)' in self.solution:
            title_text += f', CASTLE: {round(self.solution["z^CASTLE (Values)"], 2)}'

    # Update the title
    if self.b['b_title'] is not None:  # We specified a title directly
        title_text = self.b['b_title']
    self.fig.suptitle(title_text, fontsize=self.b['b_title_size'], color=title_color)

save_iteration_frame(s, kind=None)

Saves the iteration frame to the appropriate folder

Source code in afccp/visualizations/bubbles.py
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
def save_iteration_frame(self, s, kind=None):
    """
    Saves the iteration frame to the appropriate folder
    """

    # Save the figure
    if self.b['save_iteration_frames']:

        # 'Sequence' Folder
        folder_path = self.paths['Analysis & Results'] + 'Cadet Board/'
        if self.solution['iterations']['sequence'] not in os.listdir(folder_path):
            os.mkdir(folder_path + self.solution['iterations']['sequence'])

        # 'Sequence Focus' Sub-folder
        sub_folder_name = self.b['focus']
        if sub_folder_name not in os.listdir(folder_path + self.solution['iterations']['sequence'] + '/'):
            os.mkdir(folder_path + self.solution['iterations']['sequence'] + '/' + sub_folder_name)
        sub_folder_path = folder_path + self.solution['iterations']['sequence']  + '/' + sub_folder_name + '/'
        if kind is None:
            filepath = sub_folder_path + str(s + 1) + '.png'
        elif kind == "Final Solution":
            filepath = sub_folder_path + str(s + 2) + ' (' + kind + ').png'
        else:
            filepath = sub_folder_path + str(s + 1) + ' (' + kind + ').png'

        # Save frame
        self.fig.savefig(filepath)

export_board_parameters()

This function exports the board parameters back to excel

Source code in afccp/visualizations/bubbles.py
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
def export_board_parameters(self):
    """
    This function exports the board parameters back to excel
    """

    if 'iterations' not in self.solution:

        # Solutions Folder
        filepath = self.paths['Analysis & Results'] + self.solution['name'] + '/Board Parameters.csv'
        if self.solution['name'] not in os.listdir(self.paths['Analysis & Results']):
            os.mkdir(self.paths['Analysis & Results'] + self.solution['name'] + '/')
    else:

        # 'Sequence' Folder
        folder_path = self.paths['Analysis & Results'] + 'Cadet Board/'
        filepath = folder_path + self.solution['iterations']['sequence'] + '/Board Parameters.csv'
        if self.solution['iterations']['sequence'] not in os.listdir(folder_path):
            os.mkdir(folder_path + self.solution['iterations']['sequence'])

    # Create dataframe
    df = pd.DataFrame({'J': [j for j in self.b['J']],
                       'AFSC': [self.p['afscs'][j] for j in self.b['J']],
                       'x': [self.b['x'][j] for j in self.b['J']],
                       'y': [self.b['y'][j] for j in self.b['J']],
                       'n': [self.b['n'][j] for j in self.b['J']],
                       's': [self.b['s'] for _ in self.b['J']],
                       'afsc_fontsize': [self.b['afsc_fontsize'][j] for j in self.b['J']],
                       'afsc_title_two_lines': [self.b['afsc_title_two_lines'][j] for j in self.b['J']]})

    # Export file
    df.to_csv(filepath, index=False)

    if self.printing:
        print("Sequence parameters (J, x, y, n, s) exported to", filepath)

import_board_parameters()

This method imports the board parameters from excel if applicable

Source code in afccp/visualizations/bubbles.py
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
def import_board_parameters(self):
    """
    This method imports the board parameters from excel if applicable
    """

    # 'Solutions' Folder
    if 'iterations' not in self.solution:
        folder_path = self.paths['Analysis & Results'] + self.solution['name']

        # Import the file if we have it
        if 'Board Parameters.csv' in os.listdir(folder_path):
            filepath = folder_path + '/Board Parameters.csv'
            df = afccp.globals.import_csv_data(filepath)

            # Load parameters
            self.b['J'] = np.array(df['J'])
            self.b['afscs'] = np.array(df['AFSC'])
            self.b['s'] = float(df.loc[0, 's'])
            for key in ['x', 'y', 'n', 'afsc_fontsize', 'afsc_title_two_lines']:
                self.b[key] = {j: df.loc[idx, key] for idx, j in enumerate(self.b['J'])}

            if self.printing:
                print("Sequence parameters (J, x, y, n, s) imported from", filepath)
            return True

        else:

            if self.printing:
                print("No Sequence parameters found in solution analysis sub-folder '" +
                      self.solution['name'] + "'.")
            return False


    # 'Sequence' Folder
    folder_path = self.paths['Analysis & Results'] + 'Cadet Board/'
    if self.solution['iterations']['sequence'] in os.listdir(folder_path):

        # Import the file if we have it
        if 'Board Parameters.csv' in os.listdir(folder_path + self.solution['iterations']['sequence']):
            filepath = folder_path + self.solution['iterations']['sequence'] + '/Board Parameters.csv'
            df = afccp.globals.import_csv_data(filepath)

            # Load parameters
            self.b['J'] = np.array(df['J'])
            self.b['afscs'] = np.array(df['AFSC'])
            self.b['s'] = float(df.loc[0, 's'])
            for key in ['x', 'y', 'n', 'afsc_fontsize', 'afsc_title_two_lines']:
                self.b[key] = {j: df.loc[idx, key] for idx, j in enumerate(self.b['J'])}

            if self.printing:
                print("Sequence parameters (J, x, y, n, s) imported from", filepath)
            return True

        else:

            if self.printing:
                print("Sequence folder '" + self.solution['iterations']['sequence'] + "' in 'Cadet Board' analysis sub-folder, but no "
                                                                 "board parameter file found within sequence folder.")
            return False

    else:
        if self.printing:
            print("No sequence folder '" + self.solution['iterations']['sequence'] + "' in 'Cadet Board' analysis sub-folder.")
        return False

get_fontsize_for_text_in_box(self, txt, xy, width, height, *, transform=None, ha='center', va='center', **kwargs)

Determines fontsize of the text that needs to be inside a specific box

Source code in afccp/visualizations/bubbles.py
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
def get_fontsize_for_text_in_box(self, txt, xy, width, height, *, transform=None,
                                 ha='center', va='center', **kwargs):
    """
    Determines fontsize of the text that needs to be inside a specific box
    """

    # Transformation
    if transform is None:
        if isinstance(self, plt.Axes):
            transform = self.transData
        if isinstance(self, plt.Figure):
            transform = self.transFigure

    # Align the x and y
    x_data = {'center': (xy[0] - width / 2, xy[0] + width / 2),
              'left': (xy[0], xy[0] + width),
              'right': (xy[0] - width, xy[0])}
    y_data = {'center': (xy[1] - height / 2, xy[1] + height / 2),
              'bottom': (xy[1], xy[1] + height),
              'top': (xy[1] - height, xy[1])}

    (x0, y0) = transform.transform((x_data[ha][0], y_data[va][0]))
    (x1, y1) = transform.transform((x_data[ha][1], y_data[va][1]))

    # Rectangle region size to constrain the text
    rect_width = x1 - x0
    rect_height = y1 - y0

    # Doing stuff
    fig = self.get_figure() if isinstance(self, plt.Axes) else self
    dpi = fig.dpi
    rect_height_inch = rect_height / dpi
    fontsize = rect_height_inch * 72

    # Put on the text
    if isinstance(self, plt.Axes):
        text = self.annotate(txt, xy, ha=ha, va=va, xycoords=transform,
                             **kwargs)

    # Adjust the fontsize according to the box size.
    text.set_fontsize(fontsize)
    bbox: Bbox = text.get_window_extent(fig.canvas.get_renderer())
    adjusted_size = fontsize * rect_width / bbox.width
    text.set_fontsize(adjusted_size)

    # Remove the text but return the font size
    text.remove()
    return text.get_fontsize()