Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 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
287d0e1
Update docker files
Yannicked Jun 4, 2026
05cf791
Temporarily comment celery worker and beat
Yannicked Jun 8, 2026
fc7c3ef
Add uv lockfile
Yannicked Jun 8, 2026
378b9f2
Merge branch 'iterorganization:develop' into add-data-endpoint-for-simdb
prasad-sawantdesai Jun 8, 2026
4fdab64
add docker-compose-pyver.yml, argument PYVER, with_workers profile
Louwrensth Jun 8, 2026
fe146c1
server.port as env/config option
Louwrensth Jun 8, 2026
7854b4d
Dockerfile: explicit uv lock
Louwrensth Jun 8, 2026
eacea55
Revert "Dockerfile: explicit uv lock"
Louwrensth Jun 12, 2026
38dabab
newline at end of file
Louwrensth Jun 12, 2026
4451edd
add ./validation path for compose server
Louwrensth Jun 12, 2026
b2a10df
bug: missing server.imas_remote_host
Louwrensth Jun 12, 2026
21dd249
Merge branch 'feature/docker-compose' into test_docker_compose
Jun 16, 2026
fb2e2e2
updated uv lock
prasad-sawantdesai Jun 17, 2026
00c45dc
fixes when pulled simulation which has metadata with numy arrays(old …
prasad-sawantdesai Jun 17, 2026
a4e4c16
changed validation/validation-schema.yaml as file which containts red…
prasad-sawantdesai Jun 18, 2026
581f245
Handle min max values coming from database for validation
prasad-sawantdesai Jun 18, 2026
a0638eb
Rangevalue Pydantic model
prasad-sawantdesai Jun 18, 2026
7a5f006
ruff formatting
prasad-sawantdesai Jun 18, 2026
cf98bb5
merge with develop
prasad-sawantdesai Jun 18, 2026
bf55df7
remove hardcoded path for validaction-schema.yaml and get path from c…
prasad-sawantdesai Jun 19, 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
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
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,9 @@ simdb-coverage-report
src/simdb/_version.py
*.egg-info
*.egg
*.whl
*.whl
.simdb-instances/
_study
.serena
myenv
testpulse
2 changes: 1 addition & 1 deletion config/simdb.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ password = simdb
database = simdb

[validation]
path = ./validation
path = ./validation/iter_scenarios_validation.yaml
auto_validate = True
error_on_fail = True

Expand Down
13 changes: 7 additions & 6 deletions docs/maintenance_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,15 +210,16 @@ SSL_ENABLED = True

...
```
Now create a validation schema in the application configuration directory, which can be located by using:
Now create a validation schema and configure its path. Set `validation.path` in the server config to point directly to your YAML schema file:

```ini
[validation]
path = /path/to/iter_scenarios_validation.yaml
```
dirname "$(simdb config path)"
```
In this directory, you should create a file ‘validation-schema.yaml’ specifying the validation schema.
Example of validation-schema.yaml:

```
Example schema file:

```yaml
description:
required: true
type: string
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,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 @@ -377,6 +382,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
227 changes: 226 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,226 @@
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))

plotext.clear_figure()
plotext.theme("clear")
plotext.plotsize(plot_width, 18)
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 @@ -663,6 +663,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
5 changes: 5 additions & 0 deletions src/simdb/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,9 @@ def default(self, o: Any) -> Any:
return {"_type": "uuid.UUID", "hex": o.hex}
elif isinstance(o, enum.Enum):
return o.value
elif isinstance(o, np.ndarray):
if np.issubdtype(o.dtype, np.number):
valid = o[~np.isnan(o)] if np.issubdtype(o.dtype, np.floating) else o
return {"min": float(valid.min()), "max": float(valid.max())}
return o.tolist()
return super().default(o)
Loading
Loading