Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ simulation:
time_step: 0.001
max_time: 10.0

components:
nodes:
- id: reactor1
IdealGasReactor:
temperature: 1000 # K
Expand Down
11 changes: 10 additions & 1 deletion boulder/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
import dash
import dash_bootstrap_components as dbc

from . import callbacks
# Import cantera_converter early to ensure plugins are loaded at app startup
from . import (
callbacks,
cantera_converter, # noqa: F401
)
from .config import (
get_config_from_path_with_comments,
get_initial_config,
Expand All @@ -12,6 +16,11 @@
from .layout import get_layout
from .styles import CYTOSCAPE_STYLESHEET

# Create a single, shared converter instance for the app
# This ensures that the same set of discovered plugins is used everywhere.
CONVERTER = cantera_converter.CanteraConverter()


# Initialize the Dash app with Bootstrap
app = dash.Dash(
__name__,
Expand Down
6 changes: 3 additions & 3 deletions boulder/callbacks/graph_callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ def add_reactor(trigger_data: dict, config: dict) -> Any:
"type": trigger_data["type"],
"properties": trigger_data["properties"],
}
if any(comp["id"] == new_reactor["id"] for comp in config["components"]):
if any(node["id"] == new_reactor["id"] for node in config["nodes"]):
return dash.no_update

new_components = [*config.get("components", []), new_reactor]
new_config = {**config, "components": new_components}
new_nodes = [*config.get("nodes", []), new_reactor]
new_config = {**config, "nodes": new_nodes}
return new_config

# STEP 1: Trigger MFC addition and close modal immediately
Expand Down
14 changes: 7 additions & 7 deletions boulder/callbacks/modal_callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ def update_mfc_options(config: dict) -> tuple[list[dict], list[dict]]:
"Reservoir",
]
options = [
{"label": comp["id"], "value": comp["id"]}
for comp in config.get("components", [])
if comp.get("type") in valid_types
{"label": node["id"], "value": node["id"]}
for node in config.get("nodes", [])
if node.get("type") in valid_types
]
return options, options

Expand Down Expand Up @@ -225,7 +225,7 @@ def generate_reactor_id(is_open: bool, config: dict) -> Union[str, Any]:
if not is_open:
return dash.no_update

existing_ids = [comp.get("id", "") for comp in config.get("components", [])]
existing_ids = [node.get("id", "") for node in config.get("nodes", [])]

i = 1
while f"reactor_{i}" in existing_ids:
Expand Down Expand Up @@ -273,9 +273,9 @@ def set_default_mfc_values(is_open: bool, config: dict) -> tuple:
return dash.no_update, dash.no_update, dash.no_update

reactor_ids = [
comp.get("id")
for comp in config.get("components", [])
if comp.get("type")
node.get("id")
for node in config.get("nodes", [])
if node.get("type")
in ["IdealGasReactor", "ConstVolReactor", "ConstPReactor", "Reservoir"]
]

Expand Down
20 changes: 10 additions & 10 deletions boulder/callbacks/properties_callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ def show_properties_editable(last_selected, edit_mode, config):
if last_selected and last_selected.get("type") == "node":
node_id = last_selected["data"]["id"]
# Find the latest node data from config
for comp in config["components"]:
if comp["id"] == node_id:
node_data = [comp]
for node in config["nodes"]:
if node["id"] == node_id:
node_data = [node]
break
elif last_selected and last_selected.get("type") == "edge":
edge_id = last_selected["data"]["id"]
Expand Down Expand Up @@ -267,11 +267,11 @@ def save_properties(n_clicks, node_data, edge_data, config, values, ids):
if node_data:
data = node_data[0]
comp_id = data["id"]
new_components = []
for comp in config["components"]:
if comp["id"] == comp_id:
new_nodes = []
for node in config["nodes"]:
if node["id"] == comp_id:
# Ensure properties dict exists
props = dict(comp.get("properties", {}))
props = dict(node.get("properties", {}))
for v, i in zip(values, ids):
key = i["prop"]
# Convert to float if key is temperature or pressure
Expand All @@ -282,10 +282,10 @@ def save_properties(n_clicks, node_data, edge_data, config, values, ids):
props[key] = v
else:
props[key] = v
new_components.append({**comp, "properties": props})
new_nodes.append({**node, "properties": props})
else:
new_components.append(comp)
return {**config, "components": new_components}
new_nodes.append(node)
return {**config, "nodes": new_nodes}
elif edge_data:
data = edge_data[0]
conn_id = data["id"]
Expand Down
33 changes: 27 additions & 6 deletions boulder/callbacks/simulation_callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def handle_mechanism_upload(
Output("simulation-error-display", "style"),
Output("simulation-results-card", "style"),
Output("simulation-data", "data"),
Output("simulation-running", "data", allow_duplicate=True),
],
[
Input("run-simulation", "n_clicks"),
Expand All @@ -87,8 +88,14 @@ def run_simulation(
mechanism_select: str,
custom_mechanism: str,
uploaded_filename: str,
) -> Tuple[Any, Any, Any, str, Any, Dict[str, str], Dict[str, str], Dict[str, Any]]:
from ..cantera_converter import CanteraConverter, DualCanteraConverter
) -> Tuple[
Any, Any, Any, str, Any, Dict[str, str], Dict[str, str], Dict[str, Any], bool
]:
from ..cantera_converter import (
CanteraConverter,
DualCanteraConverter,
get_plugins,
)
from ..config import USE_DUAL_CONVERTER
from ..utils import apply_theme_to_figure

Expand All @@ -102,6 +109,7 @@ def run_simulation(
{"display": "none"},
{"display": "none"},
{},
False,
)

# Determine the mechanism to use
Expand All @@ -123,13 +131,16 @@ def run_simulation(

try:
if USE_DUAL_CONVERTER:
dual_converter = DualCanteraConverter(mechanism=mechanism)
dual_converter = DualCanteraConverter(
mechanism=mechanism, plugins=get_plugins()
)
network, results, code_str = dual_converter.build_network_and_code(
config
)
else:
single_converter = CanteraConverter(mechanism=mechanism)
network, results = single_converter.build_network(config)
# Build using a fresh converter with discovered plugins
converter = CanteraConverter(mechanism=mechanism, plugins=get_plugins())
network, results = converter.build_network(config)
code_str = ""

# Build initial plots from the first available reactor (no strict need)
Expand Down Expand Up @@ -187,11 +198,14 @@ def run_simulation(
{"display": "none"},
{"display": "block"},
simulation_data,
False,
)

except Exception as e:
message = f"Error during simulation: {str(e)}"
print(f"ERROR: {message}")
# IMPORTANT: update simulation-data with a non-empty payload so the
# overlay-clearing callback (listening to simulation-data) fires.
return (
go.Figure(),
go.Figure(),
Expand All @@ -200,8 +214,15 @@ def run_simulation(
message,
{"display": "block", "color": "red"},
{"display": "none"},
{},
{"error": message},
False,
)
finally:
# Safety net: if any future refactor throws before returns,
# the overlay will still be cleared by downstream callback since we
# always return a value in both success and error paths above.
# No-op here intentionally.
...

# Overlay style now handled client-side for zero-lag responsiveness

Expand Down
Loading