Skip to content

Commit

Permalink
Saving model onchange (#851)
Browse files Browse the repository at this point in the history
* Enhancing the way we save the model. The model now saves when the graph changes

* Changing the way check if the graph has changed and save the model accordingly

* Adding a settings block to the backend server for general setting values to be set by the user. Only one setting value has been added to the settings block which is the save_time_interval

* cover tests for the settings block feature

Co-authored-by: Keith Beattie <[email protected]>
  • Loading branch information
elbashandy and ksbeattie authored May 20, 2022
1 parent f2b81bd commit dbf0724
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 18 deletions.
4 changes: 4 additions & 0 deletions idaes/core/ui/fsvis/fsvis.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def visualize(
save: Optional[Union[Path, str, bool]] = None,
load_from_saved: bool = True,
save_dir: Optional[Path] = None,
save_time_interval = 5000, # 5 seconds
overwrite: bool = False,
browser: bool = True,
port: Optional[int] = None,
Expand All @@ -78,6 +79,8 @@ def visualize(
save_dir: If this argument is given, and ``save`` is not given or a relative path, then it will
be used as the directory to save the default or given file. The current working directory is
the default. If ``save`` is given and an absolute path, this argument is ignored.
save_time_interval: The time interval that the UI application checks if any changes has occurred
in the graph for it to save the model. Default is 5 seconds
overwrite: If True, and the file given by ``save`` exists, overwrite instead of creating a new
numbered file.
browser: If true, open a browser
Expand All @@ -104,6 +107,7 @@ def visualize(
# Start the web server
if web_server is None:
web_server = FlowsheetServer(port=port)
web_server.add_setting('save_time_interval', save_time_interval)
web_server.start()
if not quiet:
print("Started visualization server")
Expand Down
65 changes: 60 additions & 5 deletions idaes/core/ui/fsvis/model_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def __init__(self, port=None):
self._dsm = persist.DataStoreManager()
self._flowsheets = {}
self._thr = None
self._settings_block = {}

@property
def port(self):
Expand All @@ -70,6 +71,36 @@ def start(self):
self._thr.setDaemon(True)
self._thr.start()

def add_setting(self, key: str, value):
"""Add a setting to the flowsheet's settings block. Settings block is
a dict that has general setting values related to the UI server. Such
values could be retrieved to set some settings in the UI.
An example setting value is the `save_model_time_interval` which sets
the time interval at which the model checks if the graph has changed
or not for the model to be saved.
Args:
key: Setting name
value: Setting value
"""
self._settings_block[key] = value

def get_setting(self, key: str):
"""Get a setting value from the flowsheet's settings block.
Args:
key: Setting name
Returns:
Setting value. None if Setting name (key) doesn't exist
"""
if key not in self._settings_block:
_log.warning(f"key '{key}' is not set in the flowsheet settings block")
return None
return self._settings_block[key]


def add_flowsheet(self, id_, flowsheet, store: persist.DataStore) -> str:
"""Add a flowsheet, and also the method of saving it.
Expand Down Expand Up @@ -228,19 +259,31 @@ def do_GET(self):
Routes:
* `/app`: Return the web page
* `/fs`: Retrieve an updated flowsheet.
* `/setting`: Retrieve a setting value.
* `/path/to/file`: Retrieve file stored static directory
"""
u, id_ = self._parse_flowsheet_url(self.path)
u, queries = self._parse_flowsheet_url(self.path)
id_ = queries.get("id", None) if queries else None

_log.debug(f"do_GET: path={self.path} id=={id_}")
if u.path in ("/app", "/fs") and id_ is None:
self.send_error(
400, message=f"Query parameter 'id' is required for '{u.path}'"
)
return

if u.path == "/app":
self._get_app(id_)
elif u.path == "/fs":
self._get_fs(id_)
elif u.path == "/setting":
setting_key_ = queries.get("setting_key", None) if queries else None
if setting_key_ is None:
self.send_error(
400, message=f"Query parameter 'setting_key' is required for '{u.path}'"
)
return
self._get_setting(setting_key_)
else:
# Try to serve a file
self.directory = _static_dir # keep here: overwritten if set earlier
Expand Down Expand Up @@ -277,12 +320,25 @@ def _get_fs(self, id_: str):
# Return merged flowsheet
self._write_json(200, merged)

def _get_setting(self, setting_key_: str):
"""Get setting value.
Args:
id_: Flowsheet identifier
setting_key_: Setting name (key)
Returns:
Setting value
"""
self._write_json(200, {'setting_value': self.server.get_setting(setting_key_)})

# === PUT ===

def do_PUT(self):
"""Process a request to store data.
"""
u, id_ = self._parse_flowsheet_url(self.path)
u, queries = self._parse_flowsheet_url(self.path)
id_ = queries.get("id", None) if queries else None
_log.info(f"do_PUT: route={u} id={id_}")
if u.path in ("/fs",) and id_ is None:
self.send_error(
Expand Down Expand Up @@ -328,11 +384,10 @@ def _write_html(self, code, page):
self.wfile.write(value)

def _parse_flowsheet_url(self, path):
u, id_ = urlparse(self.path), None
u, queries = urlparse(path), None
if u.query:
queries = dict([q.split("=") for q in u.query.split("&")])
id_ = queries.get("id", None)
return u, id_
return u, queries

# === Logging ===

Expand Down
74 changes: 70 additions & 4 deletions idaes/core/ui/fsvis/static/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ export class App {
constructor (flowsheetId) {
this.paper = new Paper(this);
const url = `/fs?id=${ flowsheetId }`;

// Adding a special flag to mark that the graph changed
this._is_graph_changed = false;
// Setting name (key) that defines the save model time interval
this._save_time_interval_key = 'save_time_interval';
this._default_save_time_interval = 5000; // Default time interval
this._save_time_interval = this.getSaveTimeInterval();

this.setupGraphChangeChecker(this._save_time_interval);

$.ajax({url: url, datatype: 'json'})
.done((model) => {
this.renderModel(model);
Expand Down Expand Up @@ -92,12 +102,10 @@ export class App {
*/
refreshModel(url, paper) {
// Inform user of progress (1)
// console.debug("paper.model=", paper.model);
this.informUser(0, "Refresh: save current values from model");
// First save our version of the model
let clientModel = paper.graph;
let clientData = JSON.stringify(clientModel.toJSON());
// console.debug(`Sending to ${url}: ` + clientData);
$.ajax({url: url, type: 'PUT', contentType: "application/json", data: clientData})
// On failure inform user and stop
.fail(error => this.informUser(
Expand Down Expand Up @@ -135,7 +143,66 @@ export class App {
}

/**
* Save the model value.
* Get the save time interval value from the application's setting block.
*/
getSaveTimeInterval() {
let settings_url = "/setting?setting_key=".concat(this._save_time_interval_key);

let save_time_interval = this._default_save_time_interval;

$.ajax({url: settings_url, type: 'GET', contentType: "application/json"})
// On failure inform user and stop
.fail(error => this.informUser(
2, "Fatal error: cannot get setting value: " + error))
.done((response) => {
if (response.value != 'None') {
save_time_interval = response.value;
}
else {
this.informUser(1, "Warning: save_time_interval was not set correctly. " +
"Default time value of " + this._default_save_time_interval.toString() + "will be set.");
}
});
return save_time_interval;
}

/**
* Set `_is_graph_changed` flag to true.
*
* An example application for this flag is to save the model whenever the
* graph is changed.
*/
graphChanged() {
this._is_graph_changed = true;
}

/**
* Setup an JS interval that check if the graph has changed and saveModel
* if it does change.
*
* @param wait waiting time before actually saving the model
*/
setupGraphChangeChecker(wait) {
let model_id = $("#idaes-fs-name").data("flowsheetId");
let flowsheet_url = "/fs?id=".concat(model_id);

var graphChangedChecker = setInterval(() => {
if (this._is_graph_changed) {
this.saveModel(flowsheet_url, this.paper.graph);
// reset flag
this._is_graph_changed = false;
}
}, wait);
return graphChangedChecker;
}

/**
* Save the model value. Waiting time could be specified to
* disable multiple redundant saves caused by a stream of events
*
* Changing cell positions & link vertices fire multiple events
* subsequently. That's why we add waiting time before actually
* saving the model.
*
* This sends a PUT to the server to save the current model value.
*
Expand All @@ -144,7 +211,6 @@ export class App {
*/
saveModel(url, model) {
let clientData = JSON.stringify(model.toJSON());
// console.debug(`Sending to ${url}: ` + clientData);
this.informUser(0, "Save current values from model");
$.ajax({url: url, type: 'PUT', contentType: "application/json", data: clientData})
// On failure inform user and stop
Expand Down
13 changes: 5 additions & 8 deletions idaes/core/ui/fsvis/static/js/paper.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,11 @@ export class Paper {
* Register Events before the graph model is loaded
*/
preSetupRegisterEvents() {
let model_id = $("#idaes-fs-name").data("flowsheetId");
let url = "/fs?id=".concat(model_id);

// Save model every time the graph changes
this._graph.on('change:position change:angle change:vertices', () => {
this._app.graphChanged();
});

// Getting the main elements for the idaes canvas and the stream table
// to be able to dispatch highlighting events to the streams existing
Expand Down Expand Up @@ -182,12 +185,6 @@ export class Paper {
idaesCanvas.dispatchEvent(removeHighlightStreamEvent);
});

// Send a post request to the server with the new this._graph
// This is essentially the saving mechanism (for a server instance) for
// right now
// See the comments above the save button for more saving TODOs
self._paper.on('paper:mouseleave', () => {this._app.saveModel(url, self._graph)});

// Link labels will appear and disappear on right click. Replaces browser context menu
self._paper.on("link:contextmenu", function(linkView, evt) {
if (linkView.model.label(0)["attrs"]["text"]["display"] === 'none') {
Expand Down
3 changes: 2 additions & 1 deletion idaes/core/ui/fsvis/static/js/toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,13 @@ export class Toolbar {

// Save event listener
document.querySelector("#save-btn").addEventListener("click", () => {
this._app.saveModel(url, this._paper.graph)
this._app.saveModel(url, this._paper.graph);
});

// Refresh event listener
document.querySelector("#refresh-btn").addEventListener("click", () => {
this._app.refreshModel(url, this._paper)
this._app.saveModel(url, this._paper.graph)
});

// Flowsheet to SVG export event listener
Expand Down
12 changes: 12 additions & 0 deletions idaes/core/ui/fsvis/tests/test_model_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,15 @@ def test_flowsheet_server_run(flash_model):
print("Bogus PUT")
resp = requests.put(f"http://localhost:{srv.port}/fs")
assert not resp.ok
# test getting setting values
resp = requests.get(f"http://localhost:{srv.port}/setting")
assert not resp.ok
resp = requests.get(f"http://localhost:{srv.port}/setting?bogus_key=1234")
assert not resp.ok
resp = requests.get(f"http://localhost:{srv.port}/setting?setting_key=save_time_interval")
assert resp.ok
assert resp.json()["setting_value"] == None
srv.add_setting('dummy_setting', 5000)
resp = requests.get(f"http://localhost:{srv.port}/setting?setting_key=dummy_setting")
assert resp.ok
assert resp.json()["setting_value"] == 5000

0 comments on commit dbf0724

Please sign in to comment.