• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

swryan / OpenMDAO / 16037118102

02 Jul 2025 10:28PM UTC coverage: 87.004% (-0.01%) from 87.016%
16037118102

push

github

swryan
pre_announce

36380 of 41814 relevant lines covered (87.0%)

4.13 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

17.86
/openmdao/visualization/realtime_opt_plot/realtime_opt_plot.py
1
"""A real-time plot monitoring the optimization process as an OpenMDAO script runs."""
2

3
import ctypes
2✔
4
import errno
2✔
5
import os
2✔
6
import sys
2✔
7
from collections import defaultdict
2✔
8
import sqlite3
2✔
9

10
try:
2✔
11
    from bokeh.models import (
2✔
12
        ColumnDataSource,
13
        LinearAxis,
14
        Range1d,
15
        Toggle,
16
        Button,
17
        Column,
18
        Row,
19
        CustomJS,
20
        Div,
21
        ScrollBox,
22
        SingleIntervalTicker
23
    )
24

25
    from bokeh.models.tools import (
2✔
26
        BoxZoomTool,
27
        ResetTool,
28
        HoverTool,
29
        PanTool,
30
        WheelZoomTool,
31
        SaveTool,
32
        ZoomInTool,
33
        ZoomOutTool,
34
    )
35
    from bokeh.plotting import figure
2✔
36
    from bokeh.server.server import Server
2✔
37
    from bokeh.application.application import Application
2✔
38
    from bokeh.application.handlers import FunctionHandler
2✔
39
    from bokeh.palettes import Category20, Colorblind
2✔
40
    bokeh_available = True
2✔
41
except ImportError:
×
42
    bokeh_available = False
×
43

44
import numpy as np
2✔
45

46
from openmdao.recorders.sqlite_reader import SqliteCaseReader
2✔
47
from openmdao.recorders.case import Case
2✔
48

49
try:
2✔
50
    from openmdao.utils.gui_testing_utils import _get_free_port
2✔
51
except ImportError:
2✔
52
    # If _get_free_port is unavailable, the default port will be used
53
    def _get_free_port():
2✔
54
        return 5000
×
55

56
# Constants
57
# the time between calls to the udpate method
58
_time_between_callbacks_in_ms = 1000
2✔
59
# Number of milliseconds for unused session lifetime
60
_unused_session_lifetime_milliseconds = 1000 * 60 * 10
2✔
61
# color of the plot line for the objective function
62
_obj_color = "black"
2✔
63
# color of the buttons for variables not being shown
64
_non_active_plot_color = "black"
2✔
65
_plot_line_width = 3
2✔
66
# how transparent is the area part of the plot for desvars that are vectors
67
_varea_alpha = 0.3
2✔
68
# the CSS for the toggle buttons to let user choose what variables to plot
69
_variable_list_header_font_size = "14"
2✔
70
_toggle_styles = """
2✔
71
    font-size: 22px;
72
    box-shadow:
73
        0 4px 6px rgba(0, 0, 0, 0.1),    /* Distant shadow */
74
        0 1px 3px rgba(0, 0, 0, 0.08),   /* Close shadow */
75
        inset 0 2px 2px rgba(255, 255, 255, 0.2);  /* Top inner highlight */
76
"""
77

78
# colors used for the plot lines and associated buttons and axes labels
79
# start with color-blind friendly colors and then use others if needed
80
if bokeh_available:
2✔
81
    _colorPalette = Colorblind[8] + Category20[20]
2✔
82

83
# This is the JavaScript code that gets run when a user clicks on
84
#   one of the toggle buttons that change what variables are plotted
85
callback_code = f"""
2✔
86
// The ColorManager provides color from a palette. When the
87
//   user turns off the plotting of a variable, the color is returned to the ColorManager
88
//   for later use with a different variable plot
89
if (typeof window.ColorManager === 'undefined') {{
90
    window.ColorManager = class {{
91
        constructor() {{
92
            if (ColorManager.instance) {{
93
                return ColorManager.instance;
94
            }}
95

96
            this.palette = colorPalette;
97
            this.usedColors = new Set();
98
            this.variableColorMap = new Map();
99
            ColorManager.instance = this;
100
        }} //  end of constructor
101

102
        getColor(variableName) {{
103
            if (this.variableColorMap.has(variableName)) {{
104
                return this.variableColorMap.get(variableName);
105
            }}
106

107
            const availableColor = this.palette.find(color => !this.usedColors.has(color));
108
            const newColor = availableColor ||
109
                                this.palette[this.usedColors.size % this.palette.length];
110

111
            this.usedColors.add(newColor);
112
            this.variableColorMap.set(variableName, newColor);
113
            return newColor;
114
        }} // end of getColor
115

116
        releaseColor(variableName) {{
117
            const color = this.variableColorMap.get(variableName);
118
            if (color) {{
119
                this.usedColors.delete(color);
120
                this.variableColorMap.delete(variableName);
121
            }}
122
        }} // end of releaseColor
123

124
    }}; // end of class definition
125

126
    window.colorManager = new window.ColorManager();
127
}}
128

129
// Get the toggle that triggered the callback
130
const toggle = cb_obj;
131
const index = toggles.indexOf(toggle);
132
// index value of 0 is for the objective variable whose axis
133
// is on the left. The index variable really refers to the list of toggle buttons.
134
// The axes list variable only is for desvars and cons, whose axes are on the right.
135
// The lines list variables includes all vars
136

137
// Set line visibility
138
lines[index].visible = toggle.active;
139

140
// Set axis visibility if it exists (all except first line)
141
if (index > 0 && index-1 < axes.length) {{
142
    axes[index-1].visible = toggle.active;
143
}}
144

145
let variable_name = cb_obj.label;
146
// if turning on, get a color and set the line, axis label, and toggle button to that color
147
if (toggle.active) {{
148
    let color = window.colorManager.getColor(variable_name);
149

150
    if (index > 0) {{
151
        axes[index-1].axis_label_text_color = color
152
    }}
153

154
    // using set_value is a workaround because of a bug in Bokeh.
155
    // see https://github.com/bokeh/bokeh/issues/14364 for more info
156
    if (lines[index].glyph.type == "VArea"){{
157
        lines[index].glyph.properties.fill_color.set_value(color);
158
    }}
159
    if (lines[index].glyph.type == "Line"){{
160
        lines[index].glyph.properties.line_color.set_value(color);
161
    }}
162

163
    // make the button background color the same as the line, just slightly transparent
164
    toggle.stylesheets = [`
165
        .bk-btn.bk-active {{
166
            background-color: rgb(from ${{color}} R G B / 0.3);
167
            {_toggle_styles}
168
        }}
169
    `];
170
// if turning off a variable, return the color to the pool
171
}} else {{
172
    window.colorManager.releaseColor(variable_name);
173
    toggle.stylesheets = [`
174
        .bk-btn {{
175
            {_toggle_styles}
176
        }}
177
    `];
178

179
}}
180
"""
181

182

183
def _is_process_running(pid):
2✔
184
    if sys.platform == "win32":
×
185
        # PROCESS_QUERY_LIMITED_INFORMATION is available on Windows Vista and later.
186
        PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
×
187

188
        # Attempt to open the process.
189
        handle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
×
190
        if handle:
×
191
            ctypes.windll.kernel32.CloseHandle(handle)
×
192
            return True
×
193
        else:
194
            # If OpenProcess fails, check if it's due to access being denied.
195
            ERROR_ACCESS_DENIED = 5
×
196
            if ctypes.windll.kernel32.GetLastError() == ERROR_ACCESS_DENIED:
×
197
                return True
×
198
            return False
×
199
    else:
200
        try:
×
201
            os.kill(pid, 0)
×
202
        except OSError as err:
×
203
            if err.errno == errno.ESRCH:  # No such process
×
204
                return False
×
205
            elif err.errno == errno.EPERM:  # Process exists, no permission to signal
×
206
                return True
×
207
            else:
208
                raise
×
209
        else:
210
            return True
×
211

212

213
def _realtime_opt_plot_setup_parser(parser):
2✔
214
    """
215
    Set up the realtime plot subparser for the 'openmdao opt_plot' command.
216

217
    Parameters
218
    ----------
219
    parser : argparse subparser
220
        The parser we're adding options to.
221
    """
222
    parser.add_argument(
2✔
223
        "case_recorder_filename",
224
        type=str,
225
        help="Name of openmdao case recorder filename. It should contain driver cases",
226
    )
227

228
    parser.add_argument('--pid', type=int, default=None,
2✔
229
                        help='Process ID of calling optimization script, '
230
                        'defaults to None if called by the user directly')
231
    parser.add_argument('--no-display', action='store_false', dest='show',
2✔
232
                        help="Do not launch browser showing plot. Used for CI testing")
233

234

235
def _realtime_opt_plot_cmd(options, user_args):
2✔
236
    """
237
    Run the opt_plot command.
238

239
    Parameters
240
    ----------
241
    options : argparse Namespace
242
        Command line options.
243
    user_args : list of str
244
        Args to be passed to the user script.
245
    """
246
    if bokeh_available:
×
247
        realtime_opt_plot(options.case_recorder_filename, _time_between_callbacks_in_ms,
×
248
                          options.pid, options.show)
249
    else:
250
        print("The bokeh library is not installed so the real-time optimizaton "
×
251
              "lot is not available. ")
252
        return
×
253

254

255
def _update_y_min_max(name, y, y_min, y_max):
2✔
256
    """
257
    Update the y_min and y_max dicts containing the min and max of the variables.
258

259
    Parameters
260
    ----------
261
    name : str
262
        Name of the variable.
263
    y : double
264
        Value of the variable.
265
    y_min : dict
266
        Dict of mins of each variable.
267
    y_max : dict
268
        Dict of maxs of each variable.
269

270
    Returns
271
    -------
272
    bool
273
        True if either min or max were updated.
274
    """
275
    min_max_changed = False
×
276
    if y < y_min[name]:
×
277
        y_min[name] = y
×
278
        min_max_changed = True
×
279
    if y > y_max[name]:
×
280
        y_max[name] = y
×
281
        min_max_changed = True
×
282
    return min_max_changed
×
283

284

285
def _get_value_for_plotting(value_from_recorder, var_type):
2✔
286
    """
287
    Return the double value to be used for plotting the variable.
288

289
    Handles variables that are vectors.
290

291
    Parameters
292
    ----------
293
    value_from_recorder : numpy array
294
        Value of the variable.
295
    var_type : str
296
        String indicating which type of variable it is: 'objs', 'desvars' or 'cons'.
297

298
    Returns
299
    -------
300
    double
301
        The value to be used for plotting the variable.
302
    """
303
    if value_from_recorder is None or value_from_recorder.size == 0:
×
304
        return (0.0)
×
305
    if var_type == 'cons':
×
306
        # plot the worst case value
307
        return np.linalg.norm(value_from_recorder, ord=np.inf)
×
308
    elif var_type == 'objs':
×
309
        return value_from_recorder.item()  # get as scalar
×
310
    else:  # for desvars, use L2 norm
311
        return np.linalg.norm(value_from_recorder)
×
312

313

314
def _make_header_text_for_variable_chooser(header_text):
2✔
315
    """
316
    Return a Div to be used for the label for the type of variables in the variable list.
317

318
    Parameters
319
    ----------
320
    header_text : str
321
        Label string.
322

323
    Returns
324
    -------
325
    Div
326
        The header Div for the section of variables of that type.
327
    """
328
    header_text_div = Div(
×
329
        text=f"<b>{header_text}</b>",
330
        styles={"font-size": _variable_list_header_font_size},
331
    )
332
    return header_text_div
×
333

334

335
class _CaseRecorderTracker:
2✔
336
    """
337
    A class that is used to get information from a case recorder.
338

339
    These methods are not provided by the SqliteCaseReader class.
340
    """
341

342
    def __init__(self, case_recorder_filename):
2✔
343
        self._case_recorder_filename = case_recorder_filename
×
344
        self._cr = None
×
345
        self._initial_case = None  # need the initial case to get info about the variables
×
346
        self._next_id_to_read = 1
×
347

348
    def _open_case_recorder(self):
2✔
349
        if self._cr is None:
×
350
            self._cr = SqliteCaseReader(self._case_recorder_filename)
×
351

352
    def _get_case_by_counter(self, counter):
2✔
353
        # use SQL to see if a case with this counter exists
354
        with sqlite3.connect(self._case_recorder_filename) as con:
×
355
            con.row_factory = sqlite3.Row
×
356
            cur = con.cursor()
×
357
            cur.execute("SELECT * FROM driver_iterations WHERE "
×
358
                        "counter=:counter",
359
                        {"counter": counter})
360
            row = cur.fetchone()
×
361
        con.close()
×
362

363
        # use SqliteCaseReader code to get the data from this case
364
        if row:
×
365
            # TODO would be better to not have to open up the file each time
366
            self._open_case_recorder()
×
367
            var_info = self._cr.problem_metadata['variables']
×
368
            case = Case('driver', row, self._cr._prom2abs, self._cr._abs2prom, self._cr._abs2meta,
×
369
                        self._cr._conns, var_info, self._cr._format_version)
370

371
            return case
×
372
        else:
373
            return None
×
374

375
    def _get_data_from_case(self, driver_case):
2✔
376
        objs = driver_case.get_objectives(scaled=False)
×
377
        design_vars = driver_case.get_design_vars(scaled=False)
×
378
        constraints = driver_case.get_constraints(scaled=False)
×
379

380
        new_data = {
×
381
            "counter": int(driver_case.counter),
382
        }
383

384
        # get objectives
385
        objectives = {}
×
386
        for name, value in objs.items():
×
387
            objectives[name] = value
×
388
        new_data["objs"] = objectives
×
389

390
        # get des vars
391
        desvars = {}
×
392
        for name, value in design_vars.items():
×
393
            desvars[name] = value
×
394
        new_data["desvars"] = desvars
×
395

396
        # get cons
397
        cons = {}
×
398
        for name, value in constraints.items():
×
399
            cons[name] = value
×
400
        new_data["cons"] = cons
×
401

402
        return new_data
×
403

404
    def _get_new_case(self):
2✔
405
        # get the next unread case from the recorder
406
        driver_case = self._get_case_by_counter(self._next_id_to_read)
×
407
        if driver_case is None:
×
408
            return None
×
409

410
        if self._initial_case is None:
×
411
            self._initial_case = driver_case
×
412

413
        self._next_id_to_read += 1
×
414

415
        return driver_case
×
416

417
    def _get_obj_names(self):
2✔
418
        obj_vars = self._initial_case.get_objectives()
×
419
        return obj_vars.keys()
×
420

421
    def _get_desvar_names(self):
2✔
422
        design_vars = self._initial_case.get_design_vars()
×
423
        return design_vars.keys()
×
424

425
    def _get_cons_names(self):
2✔
426
        cons = self._initial_case.get_constraints()
×
427
        return cons.keys()
×
428

429
    def _get_units(self, name):
2✔
430
        try:
×
431
            units = self._initial_case._get_units(name)
×
432
        except RuntimeError as err:
×
433
            if str(err).startswith("Can't get units for the promoted name"):
×
434
                return "Ambiguous"
×
435
            raise
×
436
        except KeyError:
×
437
            return "Unavailable"
×
438

439
        if units is None:
×
440
            units = "Unitless"
×
441
        return units
×
442

443

444
class _RealTimeOptPlot(object):
2✔
445
    """
446
    A class that handles all of the real-time plotting.
447

448
    Parameters
449
    ----------
450
    case_recorder_filename : str
451
        The path to the case recorder file.
452
    callback_period : double
453
        The time between Bokeh callback calls (in seconds).
454
    doc : bokeh.document.Document
455
        The Bokeh document which is a collection of plots, layouts, and widgets.
456
    pid_of_calling_script : int or None
457
        The process ID of the process that called the command to start the realtime plot.
458

459
        None if the plot was called for directly by the user.
460
    """
461

462
    def __init__(self, case_recorder_filename, callback_period, doc, pid_of_calling_script):
2✔
463
        """
464
        Construct and initialize _RealTimeOptPlot instance.
465
        """
466
        self._case_recorder_filename = case_recorder_filename
×
467
        self._case_tracker = _CaseRecorderTracker(case_recorder_filename)
×
468
        self._pid_of_calling_script = pid_of_calling_script
×
469

470
        self._source = None
×
471
        self._lines = []
×
472
        self._toggles = []
×
473
        self._column_items = []
×
474
        self._axes = []
×
475
        # flag to prevent updating label with units each time we get new data
476
        self._labels_updated_with_units = False
×
477
        self._source_stream_dict = None
×
478

479
        self._setup_figure()
×
480

481
        # used to keep track of the y min and max of the data so that
482
        # the axes ranges can be adjusted as data comes in
483
        self._y_min = defaultdict(lambda: float("inf"))
×
484
        self._y_max = defaultdict(lambda: float("-inf"))
×
485

486
        def _update():
×
487
            # this is the main method of the class. It gets called periodically by Bokeh
488
            # It looks for new data and if found, updates the plot with the new data
489
            new_case = self._case_tracker._get_new_case()
×
490

491
            if new_case is None:
×
492
                if self._pid_of_calling_script is None or not _is_process_running(
×
493
                    self._pid_of_calling_script
494
                ):
495
                    # no more new data in the case recorder file and the
496
                    #   optimization script stopped running, so no possible way to
497
                    #   get new data.
498
                    # But just keep sending the last data point.
499
                    # This is a hack to force the plot to re-draw.
500
                    # Otherwise if the user clicks on the variable buttons, the
501
                    #   lines will not change color because of the set_value hack done to get
502
                    #   get around the bug in setting the line color from JavaScript
503
                    self._source.stream(self._source_stream_dict)
×
504
                return
×
505

506
            new_data = self._case_tracker._get_data_from_case(new_case)
×
507

508
            # See if Bokeh source object is defined yet. If not, set it up
509
            # since now we have data from the case recorder with info about the
510
            # variables to be plotted.
511
            if self._source is None:
×
512
                self._setup_data_source()
×
513

514
                # Check to make sure we have one and only one objective before going farther
515
                obj_names = self._case_tracker._get_obj_names()
×
516
                if len(obj_names) != 1:
×
517
                    raise ValueError(
×
518
                        f"Plot requires there to be one and only one objective \
519
                            but {len(obj_names)} objectives found"
520
                    )
521

522
                # Create CustomJS callback for toggle buttons.
523
                # Pass in the data from the Python side that the JavaScript side
524
                #   needs
525
                legend_item_callback = CustomJS(
×
526
                    args=dict(
527
                        lines=self._lines,
528
                        axes=self._axes,
529
                        toggles=self._toggles,
530
                        colorPalette=_colorPalette,
531
                        plot=self.plot_figure,
532
                    ),
533
                    code=callback_code,
534
                )
535

536
                # For the variables, make lines, axes, and the buttons to turn on and
537
                #   off the variable plot.
538
                # All the lines and axes for the desvars and cons are created in
539
                #   Python but initially are not visible. They are turned on and
540
                #   off on the JavaScript side.
541

542
                # objs
543
                obj_label = _make_header_text_for_variable_chooser("OBJECTIVE")
×
544
                self._column_items.append(obj_label)
×
545

546
                for i, obj_name in enumerate(obj_names):
×
547
                    units = self._case_tracker._get_units(obj_name)
×
548
                    self.plot_figure.yaxis.axis_label = f"{obj_name} ({units})"
×
549
                    self._make_variable_button(f"{obj_name} ({units})", _obj_color,
×
550
                                               True, legend_item_callback)
551
                    self._make_line_and_hover_tool("objs", obj_name, False, _obj_color,
×
552
                                                   "solid", True)
553
                    value = new_data["objs"][obj_name]
×
554
                    float_value = _get_value_for_plotting(value, "objs")
×
555
                    # just give it some non-zero initial range since we only have one point
556
                    self.plot_figure.y_range = Range1d(float_value - 1, float_value + 1)
×
557

558
                # desvars
559
                desvars_label = _make_header_text_for_variable_chooser("DESIGN VARS")
×
560
                self._column_items.append(desvars_label)
×
561
                desvar_names = self._case_tracker._get_desvar_names()
×
562
                for i, desvar_name in enumerate(desvar_names):
×
563
                    units = self._case_tracker._get_units(desvar_name)
×
564
                    self._make_variable_button(
×
565
                        f"{desvar_name} ({units})",
566
                        _non_active_plot_color,
567
                        False,
568
                        legend_item_callback,
569
                    )
570
                    value = new_data["desvars"][desvar_name]
×
571
                    # for desvars, if value is a vector, use Bokeh Varea glyph
572
                    use_varea = value.size > 1
×
573
                    self._make_line_and_hover_tool(
×
574
                        "desvars",
575
                        desvar_name,
576
                        use_varea,
577
                        _non_active_plot_color,
578
                        "solid",
579
                        False,
580
                    )
581
                    float_value = _get_value_for_plotting(value, "desvars")
×
582
                    self._make_axis("desvars", desvar_name, float_value, units)
×
583

584
                # cons
585
                cons_label = _make_header_text_for_variable_chooser("CONSTRAINTS")
×
586
                self._column_items.append(cons_label)
×
587
                cons_names = self._case_tracker._get_cons_names()
×
588
                for i, cons_name in enumerate(cons_names):
×
589
                    units = self._case_tracker._get_units(cons_name)
×
590
                    self._make_variable_button(
×
591
                        f"{cons_name} ({units})",
592
                        _non_active_plot_color,
593
                        False,
594
                        legend_item_callback,
595
                    )
596
                    self._make_line_and_hover_tool(
×
597
                        "cons",
598
                        cons_name,
599
                        False,
600
                        _non_active_plot_color,
601
                        "dashed",
602
                        False,
603
                    )
604
                    value = new_data["cons"][cons_name]
×
605
                    float_value = _get_value_for_plotting(value, "cons")
×
606
                    self._make_axis("cons", cons_name, float_value, units)
×
607

608
                # Create a Column of the variable buttons and headers inside a scrolling window
609
                toggle_column = Column(
×
610
                    children=self._column_items,
611
                    sizing_mode="stretch_both",
612
                    height_policy="fit",
613
                    styles={
614
                        "overflow-y": "auto",
615
                        "border": "1px solid #ddd",
616
                        "padding": "8px",
617
                        "background-color": "#dddddd",
618
                        'max-height': '100vh'  # Ensures it doesn't exceed viewport
619
                    },
620
                )
621

622
                quit_button = Button(label="Quit Application", button_type="danger")
×
623

624
                # Define callback function for the quit button
625
                def quit_app():
×
626
                    raise KeyboardInterrupt("Quit button pressed")
×
627

628
                # Attach the callback to the button
629
                quit_button.on_click(quit_app)
×
630

631
                # header for the variable list
632
                label = Div(
×
633
                    text="Variables",
634
                    width=200,
635
                    styles={"font-size": "20px", "font-weight": "bold"},
636
                )
637
                label_and_toggle_column = Column(
×
638
                    quit_button,
639
                    label,
640
                    toggle_column,
641
                    sizing_mode="stretch_height",
642
                    height_policy="fit",
643
                        styles={
644
                            'max-height': '100vh'  # Ensures it doesn't exceed viewport
645
                        },
646
                )
647

648
                scroll_box = ScrollBox(
×
649
                    child=label_and_toggle_column,
650
                    sizing_mode="stretch_height",
651
                    height_policy="max",
652
                )
653

654
                graph = Row(self.plot_figure, scroll_box, sizing_mode="stretch_both")
×
655
                doc.add_root(graph)
×
656
                # end of self._source is None - plotting is setup
657

658
            # Do the actual update of the plot including updating the plot range and adding the new
659
            # data to the Bokeh plot stream
660
            counter = new_data["counter"]
×
661

662
            self._source_stream_dict = {"iteration": [counter]}
×
663

664
            iline = 0
×
665
            for obj_name, obj_value in new_data["objs"].items():
×
666
                float_obj_value = _get_value_for_plotting(obj_value, "objs")
×
667
                self._source_stream_dict[obj_name] = [float_obj_value]
×
668
                min_max_changed = _update_y_min_max(obj_name, float_obj_value,
×
669
                                                    self._y_min, self._y_max)
670
                if min_max_changed:
×
671
                    self.plot_figure.y_range.start = self._y_min[obj_name]
×
672
                    self.plot_figure.y_range.end = self._y_max[obj_name]
×
673
                iline += 1
×
674

675
            for desvar_name, desvar_value in new_data["desvars"].items():
×
676
                if not self._labels_updated_with_units and desvar_value.size > 1:
×
677
                    units = self._case_tracker._get_units(desvar_name)
×
678
                    self._toggles[iline].label = f"{desvar_name} ({units}) {desvar_value.shape}"
×
679
                min_max_changed = False
×
680
                min_max_changed = min_max_changed or _update_y_min_max(
×
681
                    desvar_name, np.min(desvar_value), self._y_min, self._y_max
682
                )
683
                min_max_changed = min_max_changed or _update_y_min_max(
×
684
                    desvar_name, np.max(desvar_value), self._y_min, self._y_max
685
                )
686
                if min_max_changed:
×
687
                    range = Range1d(
×
688
                        self._y_min[desvar_name], self._y_max[desvar_name]
689
                    )
690
                    self.plot_figure.extra_y_ranges[f"extra_y_{desvar_name}_min"] = range
×
691
                self._source_stream_dict[f"{desvar_name}_min"] = [np.min(desvar_value)]
×
692
                self._source_stream_dict[f"{desvar_name}_max"] = [np.max(desvar_value)]
×
693
                iline += 1
×
694

695
            for cons_name, cons_value in new_data["cons"].items():
×
696
                float_cons_value = _get_value_for_plotting(cons_value, "cons")
×
697
                if not self._labels_updated_with_units and cons_value.size > 1:
×
698
                    units = self._case_tracker._get_units(cons_name)
×
699
                    self._toggles[iline].label = f"{cons_name} ({units}) {cons_value.shape}"
×
700
                self._source_stream_dict[cons_name] = [float_cons_value]
×
701
                min_max_changed = _update_y_min_max(
×
702
                    cons_name, float_cons_value, self._y_min, self._y_max)
703
                if min_max_changed:
×
704
                    range = Range1d(
×
705
                        self._y_min[cons_name], self._y_max[cons_name]
706
                    )
707
                    self.plot_figure.extra_y_ranges[f"extra_y_{cons_name}"] = range
×
708
                iline += 1
×
709
            self._source.stream(self._source_stream_dict)
×
710
            self._labels_updated_with_units = True
×
711
            # end of _update method
712

713
        doc.add_periodic_callback(_update, callback_period)
×
714
        doc.title = "OpenMDAO Optimization Progress Plot"
×
715

716
    def _setup_data_source(self):
2✔
717
        self._source_dict = {"iteration": []}
×
718

719
        # Obj
720
        obj_names = self._case_tracker._get_obj_names()
×
721
        for obj_name in obj_names:
×
722
            self._source_dict[obj_name] = []
×
723

724
        # Desvars
725
        desvar_names = self._case_tracker._get_desvar_names()
×
726
        for desvar_name in desvar_names:
×
727
            self._source_dict[f"{desvar_name}_min"] = []
×
728
            self._source_dict[f"{desvar_name}_max"] = []
×
729

730
        # Cons
731
        con_names = self._case_tracker._get_cons_names()
×
732
        for con_name in con_names:
×
733
            self._source_dict[con_name] = []
×
734

735
        self._source = ColumnDataSource(self._source_dict)
×
736

737
    def _make_variable_button(self, varname, color, active, callback):
2✔
738
        toggle = Toggle(
×
739
            label=varname,
740
            active=active,
741
            margin=(0, 0, 8, 0),
742
        )
743
        toggle.js_on_change("active", callback)
×
744
        self._toggles.append(toggle)
×
745
        self._column_items.append(toggle)
×
746

747
        # Add custom CSS styles for both active and inactive states
748
        toggle.stylesheets = [
×
749
            f"""
750
                .bk-btn {{
751
                    {_toggle_styles}
752
                }}
753
                .bk-btn.bk-active {{
754
                    background-color: rgb(from #000000 R G B / 0.3);
755
                    {_toggle_styles}
756
                }}
757
            """
758
        ]
759
        return toggle
×
760

761
    def _make_line_and_hover_tool(self, var_type, varname, use_varea, color, line_dash, visible):
2✔
762
        if use_varea:
×
763
            line = self.plot_figure.varea(
×
764
                x="iteration",
765
                y1=f"{varname}_min",
766
                y2=f"{varname}_max",
767
                source=self._source,
768
                color=color,
769
                alpha=_varea_alpha,
770
                visible=visible,
771
            )
772
        else:
773
            if var_type == "desvars":
×
774
                y_name = f"{varname}_min"
×
775
            else:
776
                y_name = varname
×
777
            line = self.plot_figure.line(
×
778
                x="iteration",
779
                y=y_name,
780
                line_width=_plot_line_width,
781
                line_dash=line_dash,
782
                source=self._source,
783
                color=color,
784
                visible=visible,
785
            )
786

787
        if var_type == "desvars":
×
788
            line.y_range_name = f"extra_y_{varname}_min"
×
789
        elif var_type == "cons":
×
790
            line.y_range_name = f"extra_y_{varname}"
×
791
        self._lines.append(line)
×
792
        if not use_varea:  # hover tool does not work with Varea
×
793
            hover = HoverTool(
×
794
                renderers=[line],
795
                tooltips=[
796
                    ("Iteration", "@iteration"),
797
                    (varname, "@{%s}" % varname + "{0.00}"),
798
                ],
799
                mode="vline",
800
                visible=visible,
801
            )
802
            self.plot_figure.add_tools(hover)
×
803

804
    def _make_axis(self, var_type, varname, plot_value, units):
2✔
805
        # Make axis for this variable on the right of the plot
806
        if var_type == "desvars":
×
807
            y_range_name = f"extra_y_{varname}_min"
×
808
        else:
809
            y_range_name = f"extra_y_{varname}"
×
810
        extra_y_axis = LinearAxis(
×
811
            y_range_name=y_range_name,
812
            axis_label=f"{varname} ({units})",
813
            axis_label_text_font_size="20px",
814
            visible=False,
815
        )
816
        self._axes.append(extra_y_axis)
×
817
        self.plot_figure.add_layout(extra_y_axis, "right")
×
818
        self.plot_figure.extra_y_ranges[y_range_name] = Range1d(
×
819
            plot_value - 1, plot_value + 1
820
        )
821

822
    def _setup_figure(self):
2✔
823
        # Make the figure and all the settings for it
824
        self.plot_figure = figure(
×
825
            tools=[
826
                PanTool(),
827
                WheelZoomTool(),
828
                ZoomInTool(),
829
                ZoomOutTool(),
830
                BoxZoomTool(),
831
                ResetTool(),
832
                SaveTool(),
833
            ],
834
            width_policy="max",
835
            height_policy="max",
836
            sizing_mode="stretch_both",
837
            title=f"Optimization Progress Plot for: {self._case_recorder_filename}",
838
            active_drag=None,
839
            active_scroll="auto",
840
            active_tap=None,
841
            output_backend="webgl",
842
        )
843
        self.plot_figure.x_range.follow = "start"
×
844
        self.plot_figure.title.text_font_size = "14px"
×
845
        self.plot_figure.title.text_color = "black"
×
846
        self.plot_figure.title.text_font = "arial"
×
847
        self.plot_figure.title.align = "left"
×
848
        self.plot_figure.title.standoff = 40  # Adds 40 pixels of space below the title
×
849

850
        self.plot_figure.xaxis.axis_label = "Driver iterations"
×
851
        self.plot_figure.xaxis.minor_tick_line_color = None
×
852
        self.plot_figure.xaxis.ticker = SingleIntervalTicker(interval=1)
×
853

854
        self.plot_figure.axis.axis_label_text_font_style = "bold"
×
855
        self.plot_figure.axis.axis_label_text_font_size = "20pt"
×
856

857

858
def realtime_opt_plot(case_recorder_filename, callback_period, pid_of_calling_script, show):
2✔
859
    """
860
    Visualize the objectives, desvars, and constraints during an optimization process.
861

862
    Parameters
863
    ----------
864
    case_recorder_filename : MetaModelStructuredComp or MetaModelUnStructuredComp
865
        The metamodel component.
866
    callback_period : float
867
        The time period between when the application calls the update method.
868
    pid_of_calling_script : int
869
        The process id of the calling optimization script, if called this way.
870
    show : bool
871
        If true, launch the browser display of the plot.
872
    """
873

874
    def _make_realtime_opt_plot_doc(doc):
×
875
        _RealTimeOptPlot(
×
876
            case_recorder_filename,
877
            callback_period,
878
            doc=doc,
879
            pid_of_calling_script=pid_of_calling_script,
880
        )
881

882
    _port_number = _get_free_port()
×
883

884
    try:
×
885
        server = Server(
×
886
            {"/": Application(FunctionHandler(_make_realtime_opt_plot_doc))},
887
            port=_port_number,
888
            unused_session_lifetime_milliseconds=_unused_session_lifetime_milliseconds,
889
        )
890
        server.start()
×
891
        if show:
×
892
            server.io_loop.add_callback(server.show, "/")
×
893

894
        print(f"Real-time optimization plot server running on http://localhost:{_port_number}")
×
895
        server.io_loop.start()
×
896
    except KeyboardInterrupt as e:
×
897
        print(f"Real-time optimization plot server stopped due to keyboard interrupt: {e}")
×
898
    except Exception as e:
×
899
        print(f"Error starting real-time optimization plot server: {e}")
×
900
    finally:
901
        print("Stopping real-time optimization plot server")
×
902
        if "server" in globals():
×
903
            server.stop()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc