Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
35fe80e
added endpoint for data retrieval
prasad-sawantdesai Apr 28, 2026
05b8818
fixed formatting
prasad-sawantdesai Apr 28, 2026
66a6ba7
fixed linting and typing issues
prasad-sawantdesai Apr 29, 2026
0aec015
Apply suggestions from code review
prasad-sawantdesai May 4, 2026
8a9ee4a
use pydantic models for input and output
prasad-sawantdesai May 4, 2026
2c01fb1
resolved pull request comments from Maarten
prasad-sawantdesai May 5, 2026
4de51d9
removed _bool check
prasad-sawantdesai May 5, 2026
6783676
use namedtuple when returning function values
prasad-sawantdesai May 5, 2026
ca0b9b4
used node.has_value instead of manual checking scalar types
prasad-sawantdesai May 5, 2026
9f2a7b0
remove leftover print statement
prasad-sawantdesai May 5, 2026
3c5f45c
removed file_uuid parameter as we will always use available imas uri
prasad-sawantdesai May 21, 2026
932c141
Merge branch 'develop' into add-data-endpoint-for-simdb
prasad-sawantdesai May 21, 2026
d8a61f1
Merge branch 'develop' into add-data-endpoint-for-simdb
prasad-sawantdesai May 22, 2026
00c21e2
fix shape issue and cache_mode=none
prasad-sawantdesai May 22, 2026
e261610
Merge branch 'develop' into add-data-endpoint-for-simdb
prasad-sawantdesai May 28, 2026
8609fdb
fix import and added TypeAlias
prasad-sawantdesai May 28, 2026
98479c7
remove typealias
prasad-sawantdesai May 28, 2026
dfda0b9
support backward compatibility for metadata
prasad-sawantdesai May 28, 2026
d089813
fixed formatting
prasad-sawantdesai May 28, 2026
c661a54
added RageValue and added test for list in metadata
prasad-sawantdesai May 28, 2026
c1797bf
removed duplicate metedataValue
prasad-sawantdesai May 28, 2026
e62b61a
make json encode backward compatible- numpy arrays, reshape with shap…
prasad-sawantdesai May 28, 2026
419c0dd
fixed logic of checking numpy arrays
prasad-sawantdesai May 28, 2026
3e06696
check values when validation failed
prasad-sawantdesai May 28, 2026
dd526fe
fix validator to understand RangeValue
prasad-sawantdesai May 28, 2026
07d18c3
added cli for calling data endpoint
prasad-sawantdesai May 29, 2026
96ee78d
reverted .gitignore
prasad-sawantdesai May 29, 2026
c86a515
Make metadata non-optional
Yannicked Jun 3, 2026
8142fec
Merge branch 'bugfix/non-optional-metadata' into add-data-endpoint-fo…
prasad-sawantdesai Jun 4, 2026
378b9f2
Merge branch 'iterorganization:develop' into add-data-endpoint-for-simdb
prasad-sawantdesai Jun 8, 2026
501f670
Merge branch 'iterorganization:develop' into add-data-endpoint-for-simdb
prasad-sawantdesai Jun 19, 2026
ef7e7e0
Reverted code which is taken care in #93
prasad-sawantdesai Jun 19, 2026
35ebacc
Merge branch 'add-data-endpoint-for-simdb' of https://github.com/pras…
prasad-sawantdesai Jun 19, 2026
65db6b7
updated uv.lock
prasad-sawantdesai Jun 25, 2026
6433987
Merge branch 'test_docker_compose' into add-data-endpoint-for-simdb
prasad-sawantdesai Jun 25, 2026
8569574
Apply suggestion from @Yannicked
prasad-sawantdesai Jun 25, 2026
fece3b2
Apply suggestion from @Yannicked
prasad-sawantdesai Jun 25, 2026
a7b8d2d
fixed suggestions from yannic
prasad-sawantdesai Jun 25, 2026
029836e
fixed imports
prasad-sawantdesai Jun 25, 2026
7a3dcc2
fixes Python versions compatibility issue with f-strings
prasad-sawantdesai Jun 25, 2026
d13870a
if ids path is not available then check for renamed path
prasad-sawantdesai Jun 26, 2026
e4aad3b
added autoconvert, dd_convert and dd_target_version parameters
prasad-sawantdesai Jul 1, 2026
7522ed6
use dd_convert only when lazy=False
prasad-sawantdesai Jul 2, 2026
cf0861d
bring back renamed path logic
prasad-sawantdesai Jul 2, 2026
194fe06
Provide dd_version when field is not available in the data entry
prasad-sawantdesai Jul 2, 2026
0c6e3c8
Merge branch 'develop' into add-data-endpoint-for-simdb
prasad-sawantdesai Jul 3, 2026
b27bab3
fixed formatting issue
prasad-sawantdesai Jul 3, 2026
512e790
using SimDbUrl instead of URI class (pydantic change)
prasad-sawantdesai Jul 3, 2026
a89fc14
upadated gitignore file
prasad-sawantdesai Jul 3, 2026
6d51ee0
updated DataType pbject
prasad-sawantdesai Jul 3, 2026
04ee038
fixed linting issue and set cache_mode
prasad-sawantdesai Jul 3, 2026
780ef3a
added test for data endpoint
prasad-sawantdesai Jul 3, 2026
75f5640
plotext formatting and test
prasad-sawantdesai Jul 3, 2026
01ab437
support api
prasad-sawantdesai Jul 3, 2026
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
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ simdb-coverage-report
src/simdb/_version.py
*.egg-info
*.egg
*.whl
*.whl
.simdb-instances/
_study
.serena
myenv
7 changes: 6 additions & 1 deletion deploy/nginx.dev.conf.template
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ server {
return 200 '{"deployment":"${SIMDB_DEPLOYMENT}","git_branch":"${SIMDB_GIT_BRANCH}","git_commit":"${SIMDB_GIT_COMMIT}","deployed_at":"${SIMDB_DEPLOYED_AT}"}';
}

location ~ ^/(v1|v1\.1|v1\.2)/$ {
default_type application/json;
return 200 '{"api":"simdb","api_version":"$1","server_version":"0.0.0","endpoints":["$scheme://$http_host/$1/simulations","$scheme://$http_host/$1/files","$scheme://$http_host/$1/validation_schema","$scheme://$http_host/$1/metadata","$scheme://$http_host/$1/upload_options"],"documentation":"$scheme://$http_host/$1/docs"}';
}

location / {
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ dependencies = [
"numpy>=1.14",
"pydantic>=2.10.6",
"python-dateutil>=2.6",
"plotext==5.3.2",
"pyyaml>=3.13",
"requests>=2.27.0",
"semantic-version>=2.8",
Expand Down
70 changes: 69 additions & 1 deletion src/simdb/cli/commands/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@
from simdb.validation import ValidationError, Validator

from . import check_meta_args, pass_config
from .utils import print_simulations
from .utils import (
is_numeric_1d,
print_quantity,
print_simulations,
show_quantity_textual_plot,
)
from .validators import validate_non_negative


Expand Down Expand Up @@ -375,6 +380,69 @@ def simulation_query(
)


@simulation.command("data", cls=n_required_args_adaptor(2))
@pass_config
@click.argument("remote", required=False)
@click.argument("sim_id")
@click.argument("ids_path")
@click.option("--username", help="Username used to authenticate with the remote.")
@click.option("--password", help="Password used to authenticate with the remote.")
def simulation_data(
config: Config,
remote: Optional[str],
sim_id: str,
ids_path: str,
username: Optional[str],
password: Optional[str],
):
"""Fetch IDS field data for simulation SIM_ID (UUID or alias) from REMOTE.

\b
IDS_PATH format:
ids_name[:<occurrence>]/path/to/field

\b
Examples:
simdb sim data iter 4dd781b... profiles_1d[0]/grid/rho_tor_norm
simdb sim data 4dd781b... equilibrium:0/time_slice[0]/profiles_1d/psi
"""
api = RemoteAPI(remote, username, password, config)

try:
result = api.get_simulation_data(sim_id, ids_path)
except Exception as err:
raise click.ClickException(str(err)) from err

click.echo(f"simulation : {result['simulation']}")
click.echo(f"path : {result['path']} (occurrence {result['occurrence']})")

coordinates = result.get("coordinates") or []
plot_coordinate = next(
(
coord
for coord in coordinates
if isinstance(coord.get("data"), list)
and isinstance(result["field"].get("data"), list)
and len(coord["data"]) == len(result["field"]["data"])
),
None,
)
field_is_1d = is_numeric_1d(result["field"].get("data"))
if field_is_1d:
show_quantity_textual_plot(
result["field"], label="field", x_quantity=plot_coordinate
)
else:
print_quantity(result["field"], label="field")

if config.verbose and coordinates:
for coord in coordinates:
if field_is_1d and is_numeric_1d(coord.get("data")):
continue
if isinstance(coord.get("data"), list):
print_quantity(coord, label=f"coord {coord['name']}", show_stats=False)


@simulation.command("validate", cls=n_required_args_adaptor(1))
@pass_config
@click.argument("remote", required=False)
Expand Down
231 changes: 230 additions & 1 deletion src/simdb/cli/commands/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from collections import OrderedDict
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, TypeVar
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple, TypeVar

import click
import plotext
from rich.console import Console, Group
from rich.panel import Panel
from rich.table import Table
from rich.text import Text

if TYPE_CHECKING:
# Only importing these for type checking and documentation generation in order to
Expand All @@ -10,6 +15,230 @@
else:
Config = TypeVar("Config")

_RICH_CONSOLE = Console()


def _get_shape(data: Any) -> Tuple[int, ...]:
"""Recursively compute shape of a nested list"""
if not isinstance(data, list):
return ()
if not data:
return (0,)
return (len(data), *_get_shape(data[0]))


def _fmt_val(v: Any) -> str:
if isinstance(v, float):
return f"{v:.6g}"
return str(v)


def _fmt_row(row: list) -> str:
"""Format a 1-D list with numpy-style head/tail truncation."""
if len(row) <= 8:
return " ".join(_fmt_val(v) for v in row)
head = " ".join(_fmt_val(v) for v in row[:3])
tail = " ".join(_fmt_val(v) for v in row[-3:])
return f"{head} ... {tail}"


def _is_numeric(v: Any) -> bool:
return isinstance(v, (int, float)) and not isinstance(v, bool)


def is_numeric_1d(data: Any) -> bool:
return isinstance(data, list) and bool(data) and all(_is_numeric(v) for v in data)


def _quantity_axis_label(q: dict, fallback: str = "") -> str:
name = q.get("name") or fallback
units = q.get("units") or "-"
label = str(name).rsplit("/", 1)[-1] or str(name)
return f"{label} [{units}]"


def _build_array_body(data: list, shape: Tuple[int, ...]) -> str:
"""Build string for 1-D or 2-D arrays."""
if len(shape) == 1:
return f"[{_fmt_row(data)}]"

if len(shape) == 2:
if len(data) <= 8:
rows = data
lines = [f" [{_fmt_row(row)}]" for row in rows]
else:
lines = [f" [{_fmt_row(row)}]" for row in data[:3]]
lines.append(" ...")
lines += [f" [{_fmt_row(row)}]" for row in data[-3:]]
formatted_lines = "\n".join(lines)
return f"[\n{formatted_lines}\n]"

return f"<{len(shape)}-D array, shape {shape}>"


def _iter_numeric(data: Any) -> Iterable[float]:
"""Yield all numeric leaf values from a nested list, skipping None."""
if isinstance(data, list):
for item in data:
yield from _iter_numeric(item)
elif _is_numeric(data):
yield float(data)


def _compute_stats(data: Any) -> Optional[Dict[str, float]]:
"""Return basic statistics for numeric data, or None if not applicable."""
values = list(_iter_numeric(data))
if len(values) < 2:
return None
n = len(values)
vmin = min(values)
vmax = max(values)
mean = sum(values) / n
std = (sum((x - mean) ** 2 for x in values) / n) ** 0.5
sorted_v = sorted(values)
mid = n // 2
median = sorted_v[mid] if n % 2 else (sorted_v[mid - 1] + sorted_v[mid]) / 2
return {
"n": n,
"min": vmin,
"max": vmax,
"mean": mean,
"std": std,
"median": median,
}


def _stats_table(stats: Dict[str, float]) -> Table:
table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2))
for key in ("n", "min", "max", "mean", "std", "median"):
table.add_column(key, justify="right")
table.add_row(
str(int(stats["n"])),
_fmt_val(stats["min"]),
_fmt_val(stats["max"]),
_fmt_val(stats["mean"]),
_fmt_val(stats["std"]),
_fmt_val(stats["median"]),
)
return table


def _plot_stats_table(stats: Dict[str, float], shape: Tuple[int, ...]) -> Table:
table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2))
for key in ("n", "min", "max", "mean", "std", "median"):
table.add_column(key, justify="right")
table.add_row(
str(int(stats["n"])),
_fmt_val(stats["min"]),
_fmt_val(stats["max"]),
_fmt_val(stats["mean"]),
_fmt_val(stats["std"]),
_fmt_val(stats["median"]),
)
return table


def _plot_panel(
*,
plot: Text,
title: str,
units: str,
stats: Optional[Dict[str, float]],
shape: Tuple[int, ...],
) -> None:
content = plot
if stats:
content = Group(plot, _plot_stats_table(stats, shape))

_RICH_CONSOLE.print(
Panel(
content,
title=f"[bold]{title}[/bold] [dim]\\[{units}][/dim]",
subtitle=f"shape {shape}",
)
)


def show_quantity_textual_plot(
q: dict,
label: str = "",
x_quantity: Optional[dict] = None,
) -> None:
"""Print line plot for a 1-D numeric QuantityData dict."""
name = q["name"]
units = q["units"] or "-"
data = q["data"]
if not is_numeric_1d(data):
print_quantity(q, label=label)
return

y_values = [float(value) for value in data]
shape = _get_shape(data)
x_values = None
xlabel = "index [-]"
if (
x_quantity
and is_numeric_1d(x_quantity.get("data"))
and len(x_quantity["data"]) == len(y_values)
):
x_values = [float(value) for value in x_quantity["data"]]
xlabel = _quantity_axis_label(x_quantity, fallback="x")

title = label or name
if x_values is None:
x_values = [float(index) for index in range(len(y_values))]

console_width = _RICH_CONSOLE.size.width
plot_width = max(48, min(70, console_width - 12))
_, terminal_height = plotext.terminal_size()
plot_height = max(12, min(24, terminal_height - 8))

plotext.clear_figure()
plotext.canvas_color("default")
plotext.axes_color("default")
plotext.ticks_color("default")
plotext.plot_size(plot_width, plot_height)
plotext.xlabel(xlabel)
plotext.ylabel(_quantity_axis_label(q, fallback=label or "field"))
plotext.plot(x_values, y_values, marker="braille", color="cyan")
plot = Text.from_ansi(plotext.build())
stats = _compute_stats(y_values)
_plot_panel(
plot=plot,
title=title,
units=units,
stats=stats,
shape=shape,
)
print_quantity(q, label=label)


def print_quantity(q: dict, label: str = "", show_stats: bool = True) -> None:
"""Print a QuantityData dict with array display and stats."""
name = q["name"]
units = q["units"] or "-"
data = q["data"]
title = f"[bold]{label or name}[/bold] [dim]\\[{units}][/dim]"

if not isinstance(data, list):
_RICH_CONSOLE.print(Panel(f"{_fmt_val(data)}", title=title, subtitle="scalar"))
return

shape = _get_shape(data)
stats = _compute_stats(data)
array_body = _build_array_body(data, shape)
subtitle = f"shape ({shape[0]},)" if len(shape) == 1 else f"shape {shape}"
if show_stats and stats:
_RICH_CONSOLE.print(
Panel(
Group(array_body, _stats_table(stats)),
title=title,
subtitle=subtitle,
)
)
else:
_RICH_CONSOLE.print(Panel(array_body, title=title, subtitle=subtitle))


def _flatten_dict(values: Dict) -> List[Tuple[str, str]]:
items = []
Expand Down
5 changes: 5 additions & 0 deletions src/simdb/cli/remote_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,11 @@ def delete_metadata(self, sim_id: str, key: str) -> List[str]:
res = self.delete("simulation/metadata/" + sim_id, {"key": key})
return [data["value"] for data in res.json()]

@try_request
def get_simulation_data(self, sim_id: str, path: str) -> Dict[str, Any]:
res = self.get(f"simulation/{sim_id}/data", params={"path": path})
return res.json()

@try_request
def get_directory(self) -> str:
res = self.get("staging_dir")
Expand Down
3 changes: 2 additions & 1 deletion src/simdb/remote/apis/v1_2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from simdb.remote.core.typing import current_app
from simdb.remote.models import StagingDirectoryResponse

from .simulation_data import api as data_ns
from .simulations import api as sim_ns

api = Api(
Expand All @@ -31,7 +32,7 @@
)

api.add_namespace(sim_ns)
namespaces = [metadata_ns, watcher_ns, file_ns, sim_ns]
namespaces = [metadata_ns, watcher_ns, file_ns, sim_ns, data_ns]


@api.route("/staging_dir", defaults={"sim_hex": None})
Expand Down
Loading
Loading