Skip to content

Commit fded6dd

Browse files
committed
add interactive plot of solution
1 parent 2b50afd commit fded6dd

File tree

1 file changed

+155
-0
lines changed

1 file changed

+155
-0
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
Plot temperature and species fractions versus density from state_over_time.txt using Plotly.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import argparse
10+
from pathlib import Path
11+
from typing import Sequence
12+
13+
import pandas as pd
14+
import plotly.graph_objects as go
15+
import plotly.io as pio
16+
from plotly.subplots import make_subplots
17+
18+
19+
def parse_header(path: Path) -> Sequence[str]:
20+
"""Extract column names from the leading comment line."""
21+
with path.open("r", encoding="ascii") as handle:
22+
for line in handle:
23+
stripped = line.strip()
24+
if not stripped:
25+
continue
26+
if stripped.startswith("#"):
27+
return stripped.lstrip("#").split()
28+
# If no comment header is present, fall back to whitespace split.
29+
return stripped.split()
30+
raise ValueError(f"Could not find a header row in {path}")
31+
32+
33+
def load_state_data(path: Path) -> pd.DataFrame:
34+
columns = parse_header(path)
35+
# Skip the header we just parsed and load the remainder.
36+
return pd.read_csv(
37+
path,
38+
delim_whitespace=True,
39+
skiprows=1,
40+
names=columns,
41+
comment="#",
42+
)
43+
44+
45+
def plot_state(path: Path, output: Path | None) -> None:
46+
data = load_state_data(path)
47+
48+
required_columns = {"Density", "Temperature"}
49+
missing = required_columns - set(data.columns)
50+
if missing:
51+
missing_cols = ", ".join(sorted(missing))
52+
raise KeyError(f"Missing required column(s): {missing_cols}")
53+
54+
density = data["Density"]
55+
temperature = data["Temperature"]
56+
species_cols = [
57+
col for col in data.columns if col not in {"Time", "Density", "Temperature"}
58+
]
59+
60+
if not species_cols:
61+
raise ValueError("No species fraction columns found in input data.")
62+
63+
if (density <= 0).any():
64+
raise ValueError("Density contains non-positive values; cannot use log scale.")
65+
if (temperature <= 0).any():
66+
raise ValueError("Temperature contains non-positive values; cannot use log scale.")
67+
68+
fig = make_subplots(
69+
rows=2,
70+
cols=1,
71+
shared_xaxes=True,
72+
vertical_spacing=0.08,
73+
subplot_titles=(
74+
"Temperature vs. Density",
75+
"Species Fractions vs. Density",
76+
),
77+
)
78+
79+
fig.add_trace(
80+
go.Scatter(
81+
x=density,
82+
y=temperature,
83+
mode="lines",
84+
name="Temperature",
85+
hovertemplate=(
86+
"Density: %{x:.3e}<br>Temperature: %{y:.3e}<extra>Temperature</extra>"
87+
),
88+
),
89+
row=1,
90+
col=1,
91+
)
92+
93+
for column in species_cols:
94+
series = data[column]
95+
positive_mask = series > 0
96+
if positive_mask.any():
97+
fig.add_trace(
98+
go.Scatter(
99+
x=density[positive_mask],
100+
y=series[positive_mask],
101+
mode="lines",
102+
name=column,
103+
hovertemplate=(
104+
"Species: "
105+
+ column
106+
+ "<br>Density: %{x:.3e}"
107+
+ "<br>Fraction: %{y:.3e}<extra></extra>"
108+
),
109+
),
110+
row=2,
111+
col=1,
112+
)
113+
114+
fig.update_xaxes(type="log", row=1, col=1)
115+
fig.update_xaxes(type="log", title_text="Density", row=2, col=1)
116+
fig.update_yaxes(type="log", title_text="Temperature", row=1, col=1)
117+
fig.update_yaxes(type="log", title_text="Species Fraction", row=2, col=1)
118+
119+
fig.update_layout(
120+
height=800,
121+
legend_title_text="Species",
122+
hovermode="x unified",
123+
margin=dict(l=70, r=200, t=80, b=60),
124+
)
125+
126+
if output:
127+
output.parent.mkdir(parents=True, exist_ok=True)
128+
pio.write_html(fig, file=str(output), include_plotlyjs="cdn", full_html=True)
129+
else:
130+
fig.show()
131+
132+
133+
def main() -> None:
134+
parser = argparse.ArgumentParser(
135+
description="Plot temperature and species fractions versus density."
136+
)
137+
parser.add_argument(
138+
"--input",
139+
type=Path,
140+
default=Path("state_over_time.txt"),
141+
help="Path to the state_over_time.txt file (default: state_over_time.txt).",
142+
)
143+
parser.add_argument(
144+
"--output",
145+
type=Path,
146+
default=None,
147+
help="Optional output path for saving the Plotly figure as HTML.",
148+
)
149+
args = parser.parse_args()
150+
151+
plot_state(args.input, args.output)
152+
153+
154+
if __name__ == "__main__":
155+
main()

0 commit comments

Comments
 (0)