crgutils 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crgutils/__init__.py +38 -0
- crgutils/_check.py +102 -0
- crgutils/_contact_point.py +167 -0
- crgutils/_convenience.py +96 -0
- crgutils/_dataset.py +192 -0
- crgutils/_eval.py +541 -0
- crgutils/_loader.py +29 -0
- crgutils/_loader_impl.py +732 -0
- crgutils/_modifiers.py +345 -0
- crgutils/_types.py +54 -0
- crgutils/py.typed +0 -0
- crgutils-0.1.0.dist-info/METADATA +173 -0
- crgutils-0.1.0.dist-info/RECORD +14 -0
- crgutils-0.1.0.dist-info/WHEEL +4 -0
crgutils/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""crgutils — Python library for reading and evaluating OpenCRG road surface files.
|
|
2
|
+
|
|
3
|
+
Quick start
|
|
4
|
+
-----------
|
|
5
|
+
>>> import crgutils
|
|
6
|
+
>>> ds = crgutils.read("road.crg") # load + validate + apply modifiers
|
|
7
|
+
>>> cp = ds.create_contact_point()
|
|
8
|
+
>>> z = cp.eval_uv_to_z(10.0, 0.0) # elevation at (u=10, v=0)
|
|
9
|
+
>>> x, y = cp.eval_uv_to_xy(10.0, 0.5) # road → world coords
|
|
10
|
+
>>> u, v = cp.eval_xy_to_uv(x, y) # world → road coords
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from ._convenience import read
|
|
14
|
+
from ._dataset import CRGDataset
|
|
15
|
+
from ._contact_point import ContactPoint
|
|
16
|
+
from ._types import BorderMode, RefLineContinue, CurvMode, GridNaNMode, EvalOptions
|
|
17
|
+
|
|
18
|
+
# Lower-level building blocks — available for advanced use but not part of the
|
|
19
|
+
# primary public API.
|
|
20
|
+
from ._loader import load # noqa: F401
|
|
21
|
+
from ._check import check # noqa: F401
|
|
22
|
+
from ._modifiers import apply_modifiers # noqa: F401
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
# High-level interface
|
|
28
|
+
"read",
|
|
29
|
+
"CRGDataset",
|
|
30
|
+
"ContactPoint",
|
|
31
|
+
# Types / enums
|
|
32
|
+
"BorderMode",
|
|
33
|
+
"RefLineContinue",
|
|
34
|
+
"CurvMode",
|
|
35
|
+
"GridNaNMode",
|
|
36
|
+
"EvalOptions",
|
|
37
|
+
"__version__",
|
|
38
|
+
]
|
crgutils/_check.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Validate a CRGDataset for consistency and accuracy.
|
|
2
|
+
|
|
3
|
+
Port of ``crgCheck()`` / ``crgCheckOpts()`` / ``crgCheckData()`` from
|
|
4
|
+
crgLoader.c / crgMgr.c.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
import warnings
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from ._dataset import CRGDataset
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def check(dataset: CRGDataset, *, raise_on_error: bool = False) -> bool:
|
|
18
|
+
"""Validate *dataset* for internal consistency.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
dataset:
|
|
23
|
+
The dataset to validate.
|
|
24
|
+
raise_on_error:
|
|
25
|
+
If True, raise a ``ValueError`` instead of returning False.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
bool
|
|
30
|
+
``True`` if all checks pass.
|
|
31
|
+
"""
|
|
32
|
+
errors: list[str] = []
|
|
33
|
+
warnings_: list[str] = []
|
|
34
|
+
|
|
35
|
+
# --- basic shape checks ---
|
|
36
|
+
if dataset.n_u < 2:
|
|
37
|
+
errors.append(f"u axis has fewer than 2 points ({dataset.n_u})")
|
|
38
|
+
if dataset.n_v < 1:
|
|
39
|
+
errors.append(f"v axis has no points ({dataset.n_v})")
|
|
40
|
+
if dataset.z.shape != (dataset.n_v, dataset.n_u):
|
|
41
|
+
errors.append(
|
|
42
|
+
f"z shape {dataset.z.shape} does not match (n_v={dataset.n_v}, n_u={dataset.n_u})"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# --- u increment consistency ---
|
|
46
|
+
if dataset.n_u >= 2:
|
|
47
|
+
computed_inc = (dataset.u_max - dataset.u_min) / (dataset.n_u - 1)
|
|
48
|
+
if abs(computed_inc - dataset.u_inc) > 1e-6 * max(1.0, abs(dataset.u_inc)):
|
|
49
|
+
warnings_.append(
|
|
50
|
+
f"u_inc mismatch: stored={dataset.u_inc:.6f}, computed={computed_inc:.6f}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# --- v monotonicity ---
|
|
54
|
+
if dataset.n_v >= 2:
|
|
55
|
+
diffs = np.diff(dataset.v.astype(float))
|
|
56
|
+
if not np.all(diffs > 0):
|
|
57
|
+
errors.append("v array is not strictly increasing")
|
|
58
|
+
|
|
59
|
+
# --- phi continuity (warn on large jumps) ---
|
|
60
|
+
if dataset.n_u >= 2:
|
|
61
|
+
phi_unwrapped = np.unwrap(dataset.phi.astype(float))
|
|
62
|
+
dphi = np.abs(np.diff(phi_unwrapped))
|
|
63
|
+
max_dphi = float(dphi.max()) if len(dphi) else 0.0
|
|
64
|
+
if max_dphi > math.pi / 2:
|
|
65
|
+
warnings_.append(
|
|
66
|
+
f"Large heading jump detected: max |Δphi| = {math.degrees(max_dphi):.1f}°"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# --- NaN fraction in z grid ---
|
|
70
|
+
nan_count = int(np.isnan(dataset.z.astype(float)).sum())
|
|
71
|
+
total = dataset.z.size
|
|
72
|
+
if nan_count > 0:
|
|
73
|
+
frac = nan_count / total
|
|
74
|
+
if frac > 0.5:
|
|
75
|
+
errors.append(f"More than 50% of z values are NaN ({nan_count}/{total})")
|
|
76
|
+
elif frac > 0.1:
|
|
77
|
+
warnings_.append(f"{nan_count}/{total} z values are NaN ({100*frac:.1f}%)")
|
|
78
|
+
|
|
79
|
+
# --- reference line x/y length vs u length ---
|
|
80
|
+
if dataset.n_u >= 2:
|
|
81
|
+
dx = np.diff(dataset.x.astype(float))
|
|
82
|
+
dy = np.diff(dataset.y.astype(float))
|
|
83
|
+
seg_lengths = np.sqrt(dx ** 2 + dy ** 2)
|
|
84
|
+
total_xy = float(seg_lengths.sum())
|
|
85
|
+
total_u = dataset.u_max - dataset.u_min
|
|
86
|
+
if total_u > 0 and abs(total_xy - total_u) / total_u > 0.05:
|
|
87
|
+
warnings_.append(
|
|
88
|
+
f"Reference line arc length {total_xy:.3f} m differs from "
|
|
89
|
+
f"u range {total_u:.3f} m by more than 5%"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
for w in warnings_:
|
|
93
|
+
warnings.warn(f"CRGDataset check: {w}", UserWarning, stacklevel=2)
|
|
94
|
+
|
|
95
|
+
if errors:
|
|
96
|
+
msg = "CRGDataset validation failed:\n" + "\n".join(f" - {e}" for e in errors)
|
|
97
|
+
if raise_on_error:
|
|
98
|
+
raise ValueError(msg)
|
|
99
|
+
warnings.warn(msg, UserWarning, stacklevel=2)
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
return True
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""ContactPoint: stateful evaluation context for a CRGDataset."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import deque
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from ._dataset import CRGDataset
|
|
11
|
+
from ._types import EvalOptions
|
|
12
|
+
from ._eval import (
|
|
13
|
+
_eval_uv_to_z,
|
|
14
|
+
_eval_uv_to_xy,
|
|
15
|
+
_xy_to_uv,
|
|
16
|
+
_eval_uv_to_pk,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ContactPoint:
|
|
21
|
+
"""Evaluation context for a :class:`CRGDataset`.
|
|
22
|
+
|
|
23
|
+
A ``ContactPoint`` binds a dataset to a set of :class:`EvalOptions` and
|
|
24
|
+
maintains a small history deque that accelerates successive nearby queries
|
|
25
|
+
(warm start for the xy→uv search).
|
|
26
|
+
|
|
27
|
+
Multiple ``ContactPoint`` instances on the same dataset are safe to use
|
|
28
|
+
independently (each has its own history and options).
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
dataset:
|
|
33
|
+
The CRG dataset to evaluate against.
|
|
34
|
+
**options:
|
|
35
|
+
Keyword arguments that override :class:`EvalOptions` defaults.
|
|
36
|
+
Keys must be valid :class:`EvalOptions` field names.
|
|
37
|
+
|
|
38
|
+
Examples
|
|
39
|
+
--------
|
|
40
|
+
>>> import crgutils
|
|
41
|
+
>>> ds = crgutils.load("road.crg")
|
|
42
|
+
>>> cp = crgutils.ContactPoint(ds)
|
|
43
|
+
>>> z = cp.eval_uv_to_z(5.0, 0.0)
|
|
44
|
+
>>> x, y = cp.eval_uv_to_xy(5.0, 0.5)
|
|
45
|
+
>>> u, v = cp.eval_xy_to_uv(x, y)
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, dataset: CRGDataset, **options: Any) -> None:
|
|
49
|
+
self._ds = dataset
|
|
50
|
+
# Merge dataset-level default options with caller overrides
|
|
51
|
+
merged = {**dataset.default_options, **options}
|
|
52
|
+
self._options = EvalOptions(**merged)
|
|
53
|
+
self._history: deque[tuple[float, float, int]] = deque(
|
|
54
|
+
maxlen=self._options.history_size
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Pre-seed history if ref_line_search_u is set
|
|
58
|
+
if self._options.ref_line_search_u is not None:
|
|
59
|
+
su = self._options.ref_line_search_u
|
|
60
|
+
idx = int((su - dataset.u_min) / dataset.u_inc)
|
|
61
|
+
idx = max(1, min(idx, dataset.n_u - 1))
|
|
62
|
+
x_seed, y_seed = _eval_uv_to_xy(dataset, self._options, su, 0.0)
|
|
63
|
+
self._history.appendleft((x_seed, y_seed, idx))
|
|
64
|
+
|
|
65
|
+
# ------------------------------------------------------------------
|
|
66
|
+
# Factory
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
def with_options(self, **overrides: Any) -> "ContactPoint":
|
|
70
|
+
"""Return a new ``ContactPoint`` with option overrides applied.
|
|
71
|
+
|
|
72
|
+
The new instance shares the same dataset but gets a fresh history.
|
|
73
|
+
"""
|
|
74
|
+
from dataclasses import asdict
|
|
75
|
+
current = asdict(self._options)
|
|
76
|
+
current.update(overrides)
|
|
77
|
+
return ContactPoint(self._ds, **current)
|
|
78
|
+
|
|
79
|
+
# ------------------------------------------------------------------
|
|
80
|
+
# Properties
|
|
81
|
+
# ------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def options(self) -> EvalOptions:
|
|
85
|
+
return self._options
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def dataset(self) -> CRGDataset:
|
|
89
|
+
return self._ds
|
|
90
|
+
|
|
91
|
+
# ------------------------------------------------------------------
|
|
92
|
+
# Primary evaluation methods
|
|
93
|
+
# ------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
def eval_uv_to_z(self, u: float, v: float) -> float:
|
|
96
|
+
"""Bilinear elevation at road coordinate (u, v) [m]."""
|
|
97
|
+
return _eval_uv_to_z(self._ds, self._options, u, v)
|
|
98
|
+
|
|
99
|
+
def eval_xy_to_z(self, x: float, y: float) -> float:
|
|
100
|
+
"""Elevation at Cartesian position (x, y) [m]."""
|
|
101
|
+
u, v = _xy_to_uv(self._ds, self._options, x, y, self._history)
|
|
102
|
+
return _eval_uv_to_z(self._ds, self._options, u, v)
|
|
103
|
+
|
|
104
|
+
def eval_uv_to_xy(self, u: float, v: float) -> tuple[float, float]:
|
|
105
|
+
"""Convert road (u, v) to Cartesian (x, y) [m]."""
|
|
106
|
+
return _eval_uv_to_xy(self._ds, self._options, u, v)
|
|
107
|
+
|
|
108
|
+
def eval_xy_to_uv(self, x: float, y: float) -> tuple[float, float]:
|
|
109
|
+
"""Convert Cartesian (x, y) to road (u, v) [m]."""
|
|
110
|
+
return _xy_to_uv(self._ds, self._options, x, y, self._history)
|
|
111
|
+
|
|
112
|
+
def eval_uv_to_pk(self, u: float, v: float) -> tuple[float, float]:
|
|
113
|
+
"""Heading [rad] and curvature [1/m] at road (u, v)."""
|
|
114
|
+
return _eval_uv_to_pk(self._ds, self._options, u, v)
|
|
115
|
+
|
|
116
|
+
def eval_xy_to_pk(self, x: float, y: float) -> tuple[float, float]:
|
|
117
|
+
"""Heading [rad] and curvature [1/m] at Cartesian (x, y)."""
|
|
118
|
+
u, v = _xy_to_uv(self._ds, self._options, x, y, self._history)
|
|
119
|
+
return _eval_uv_to_pk(self._ds, self._options, u, v)
|
|
120
|
+
|
|
121
|
+
# ------------------------------------------------------------------
|
|
122
|
+
# Vectorised convenience methods
|
|
123
|
+
# ------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
def eval_uv_to_z_grid(
|
|
126
|
+
self,
|
|
127
|
+
u: "np.ndarray",
|
|
128
|
+
v: "np.ndarray",
|
|
129
|
+
) -> "np.ndarray":
|
|
130
|
+
"""Vectorised elevation evaluation over arrays of (u, v) pairs.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
u, v:
|
|
135
|
+
1-D arrays of matching length or broadcastable shapes.
|
|
136
|
+
|
|
137
|
+
Returns
|
|
138
|
+
-------
|
|
139
|
+
np.ndarray
|
|
140
|
+
Elevation values, same shape as broadcast(u, v).
|
|
141
|
+
"""
|
|
142
|
+
u_arr = np.asarray(u, dtype=float).ravel()
|
|
143
|
+
v_arr = np.asarray(v, dtype=float).ravel()
|
|
144
|
+
z_arr = np.empty(len(u_arr), dtype=float)
|
|
145
|
+
for i in range(len(u_arr)):
|
|
146
|
+
z_arr[i] = _eval_uv_to_z(self._ds, self._options, float(u_arr[i]), float(v_arr[i]))
|
|
147
|
+
return z_arr
|
|
148
|
+
|
|
149
|
+
def eval_xy_to_z_grid(
|
|
150
|
+
self,
|
|
151
|
+
x: "np.ndarray",
|
|
152
|
+
y: "np.ndarray",
|
|
153
|
+
) -> "np.ndarray":
|
|
154
|
+
"""Vectorised elevation evaluation over arrays of (x, y) pairs."""
|
|
155
|
+
x_arr = np.asarray(x, dtype=float).ravel()
|
|
156
|
+
y_arr = np.asarray(y, dtype=float).ravel()
|
|
157
|
+
z_arr = np.empty(len(x_arr), dtype=float)
|
|
158
|
+
for i in range(len(x_arr)):
|
|
159
|
+
z_arr[i] = self.eval_xy_to_z(float(x_arr[i]), float(y_arr[i]))
|
|
160
|
+
return z_arr
|
|
161
|
+
|
|
162
|
+
def __repr__(self) -> str:
|
|
163
|
+
return (
|
|
164
|
+
f"ContactPoint(dataset={self._ds.source_file!r}, "
|
|
165
|
+
f"border_mode_u={self._options.border_mode_u.name}, "
|
|
166
|
+
f"border_mode_v={self._options.border_mode_v.name})"
|
|
167
|
+
)
|
crgutils/_convenience.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Convenience functions that combine multiple crgutils steps."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ._dataset import CRGDataset
|
|
8
|
+
from ._loader import load
|
|
9
|
+
from ._check import check
|
|
10
|
+
from ._modifiers import apply_modifiers
|
|
11
|
+
from ._types import GridNaNMode
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def read(
|
|
15
|
+
path: str | Path,
|
|
16
|
+
*,
|
|
17
|
+
raise_on_error: bool = True,
|
|
18
|
+
# ---- scale modifiers ----
|
|
19
|
+
scale_z: float | None = None,
|
|
20
|
+
scale_slope: float | None = None,
|
|
21
|
+
scale_bank: float | None = None,
|
|
22
|
+
scale_length: float | None = None,
|
|
23
|
+
scale_width: float | None = None,
|
|
24
|
+
scale_curvature: float | None = None,
|
|
25
|
+
# ---- NaN handling ----
|
|
26
|
+
grid_nan_mode: int | str | GridNaNMode | None = None,
|
|
27
|
+
grid_nan_offset: float | None = None,
|
|
28
|
+
# ---- repositioning by reference point ----
|
|
29
|
+
ref_point_u: float | None = None,
|
|
30
|
+
ref_point_u_frac: float | None = None,
|
|
31
|
+
ref_point_u_offset: float | None = None,
|
|
32
|
+
ref_point_v: float | None = None,
|
|
33
|
+
ref_point_v_frac: float | None = None,
|
|
34
|
+
ref_point_v_offset: float | None = None,
|
|
35
|
+
ref_point_x: float | None = None,
|
|
36
|
+
ref_point_y: float | None = None,
|
|
37
|
+
ref_point_z: float | None = None,
|
|
38
|
+
ref_point_phi: float | None = None,
|
|
39
|
+
# ---- reference line offsets / rotation ----
|
|
40
|
+
ref_line_offset_x: float | None = None,
|
|
41
|
+
ref_line_offset_y: float | None = None,
|
|
42
|
+
ref_line_offset_z: float | None = None,
|
|
43
|
+
ref_line_offset_phi: float | None = None,
|
|
44
|
+
ref_line_rot_center_x: float | None = None,
|
|
45
|
+
ref_line_rot_center_y: float | None = None,
|
|
46
|
+
) -> CRGDataset | None:
|
|
47
|
+
"""Load a .crg file, validate it, and apply modifiers in one call.
|
|
48
|
+
|
|
49
|
+
Equivalent to::
|
|
50
|
+
|
|
51
|
+
ds = crgutils.load(path)
|
|
52
|
+
crgutils.check(ds, raise_on_error=True)
|
|
53
|
+
crgutils.apply_modifiers(ds, **ds.pending_modifiers)
|
|
54
|
+
ds.pending_modifiers.clear()
|
|
55
|
+
|
|
56
|
+
File-embedded ``pending_modifiers`` are always applied first. Any modifier
|
|
57
|
+
kwargs passed here override same-named keys from the file's embedded
|
|
58
|
+
modifiers.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
path:
|
|
63
|
+
Path to a ``.crg`` file.
|
|
64
|
+
raise_on_error:
|
|
65
|
+
If ``True`` (default), raise :exc:`ValueError` on validation failure.
|
|
66
|
+
If ``False``, return ``None`` instead.
|
|
67
|
+
scale_z, scale_slope, ... :
|
|
68
|
+
Modifier overrides — see :func:`~crgutils.apply_modifiers`.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
CRGDataset | None
|
|
73
|
+
The loaded, validated, modified dataset; or ``None`` if
|
|
74
|
+
*raise_on_error* is ``False`` and validation fails.
|
|
75
|
+
"""
|
|
76
|
+
ds = load(path)
|
|
77
|
+
if not check(ds, raise_on_error=raise_on_error):
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
# Build dict of explicitly-supplied caller overrides (skip None sentinels)
|
|
81
|
+
_locs = locals()
|
|
82
|
+
_modifier_keys = [
|
|
83
|
+
"scale_z", "scale_slope", "scale_bank", "scale_length",
|
|
84
|
+
"scale_width", "scale_curvature", "grid_nan_mode", "grid_nan_offset",
|
|
85
|
+
"ref_point_u", "ref_point_u_frac", "ref_point_u_offset",
|
|
86
|
+
"ref_point_v", "ref_point_v_frac", "ref_point_v_offset",
|
|
87
|
+
"ref_point_x", "ref_point_y", "ref_point_z", "ref_point_phi",
|
|
88
|
+
"ref_line_offset_x", "ref_line_offset_y", "ref_line_offset_z",
|
|
89
|
+
"ref_line_offset_phi", "ref_line_rot_center_x", "ref_line_rot_center_y",
|
|
90
|
+
]
|
|
91
|
+
caller_overrides = {k: _locs[k] for k in _modifier_keys if _locs[k] is not None}
|
|
92
|
+
|
|
93
|
+
merged = {**ds.pending_modifiers, **caller_overrides}
|
|
94
|
+
apply_modifiers(ds, **merged)
|
|
95
|
+
ds.pending_modifiers.clear()
|
|
96
|
+
return ds
|
crgutils/_dataset.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""CRGDataset: the core data container for a loaded .crg file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ._contact_point import ContactPoint
|
|
10
|
+
from ._types import BorderMode, CurvMode, RefLineContinue
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class CRGDataset:
|
|
17
|
+
"""Holds all arrays and metadata parsed from a single .crg file.
|
|
18
|
+
|
|
19
|
+
Coordinate system
|
|
20
|
+
-----------------
|
|
21
|
+
u — arc-length along the reference line [m]
|
|
22
|
+
v — lateral offset from the reference line [m]
|
|
23
|
+
x, y — Cartesian world coordinates of the reference line [m]
|
|
24
|
+
phi — heading angle of the reference line [rad]
|
|
25
|
+
z — elevation grid, shape (n_v, n_u) [m], float32
|
|
26
|
+
|
|
27
|
+
All arrays are stored in increasing u / v order.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# --- Reference line (all float64, shape (n_u,)) ---
|
|
31
|
+
u: np.ndarray # u coordinates
|
|
32
|
+
x: np.ndarray # Cartesian x of ref line (derived from phi if absent)
|
|
33
|
+
y: np.ndarray # Cartesian y of ref line
|
|
34
|
+
phi: np.ndarray # heading angles [rad]
|
|
35
|
+
|
|
36
|
+
# --- Cross-section grid ---
|
|
37
|
+
v: np.ndarray # v positions, shape (n_v,), float64
|
|
38
|
+
z: np.ndarray # elevation grid, shape (n_v, n_u), float32
|
|
39
|
+
z_mean: np.ndarray # per-v-channel mean subtracted during normalisation, (n_v,)
|
|
40
|
+
|
|
41
|
+
# --- Optional reference line channels (shape (n_u,) or scalar) ---
|
|
42
|
+
ref_z: np.ndarray | None # ref line elevation [m]
|
|
43
|
+
bank: np.ndarray | None # banking [1/m] (tan of bank angle)
|
|
44
|
+
slope: np.ndarray | None # slope [1]
|
|
45
|
+
|
|
46
|
+
# --- Grid spacing / topology ---
|
|
47
|
+
u_inc: float
|
|
48
|
+
v_inc: float = 0.0 # 0 if v is non-equally spaced
|
|
49
|
+
u_is_closed: bool = False
|
|
50
|
+
u_close_min: float = 0.0
|
|
51
|
+
u_close_max: float = 0.0
|
|
52
|
+
v_equally_spaced: bool = True
|
|
53
|
+
has_bank: bool = False
|
|
54
|
+
|
|
55
|
+
# --- Precomputed trigonometry for extrapolation ---
|
|
56
|
+
phi_first_sin: float = 0.0
|
|
57
|
+
phi_first_cos: float = 1.0
|
|
58
|
+
phi_last_sin: float = 0.0
|
|
59
|
+
phi_last_cos: float = 1.0
|
|
60
|
+
|
|
61
|
+
# --- Options / modifiers embedded in the file header ---
|
|
62
|
+
default_options: dict[str, Any] = field(default_factory=dict)
|
|
63
|
+
pending_modifiers: dict[str, Any] = field(default_factory=dict)
|
|
64
|
+
|
|
65
|
+
# --- Source info (informational only) ---
|
|
66
|
+
source_file: str = ""
|
|
67
|
+
comment: str = ""
|
|
68
|
+
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
# Properties
|
|
71
|
+
# ------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def u_min(self) -> float:
|
|
75
|
+
return float(self.u[0])
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def u_max(self) -> float:
|
|
79
|
+
return float(self.u[-1])
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def v_min(self) -> float:
|
|
83
|
+
return float(self.v[0])
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def v_max(self) -> float:
|
|
87
|
+
return float(self.v[-1])
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def u_range(self) -> tuple[float, float]:
|
|
91
|
+
return (self.u_min, self.u_max)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def v_range(self) -> tuple[float, float]:
|
|
95
|
+
return (self.v_min, self.v_max)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def shape(self) -> tuple[int, int]:
|
|
99
|
+
"""(n_v, n_u)"""
|
|
100
|
+
return self.z.shape
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def n_u(self) -> int:
|
|
104
|
+
return len(self.u)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def n_v(self) -> int:
|
|
108
|
+
return len(self.v)
|
|
109
|
+
|
|
110
|
+
# ------------------------------------------------------------------
|
|
111
|
+
# Human-readable summaries
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
def info(self) -> str:
|
|
115
|
+
lines = [
|
|
116
|
+
f"CRGDataset: {self.source_file}",
|
|
117
|
+
f" u range : [{self.u_min:.4f}, {self.u_max:.4f}] m (n={self.n_u}, inc={self.u_inc:.4f})",
|
|
118
|
+
f" v range : [{self.v_min:.4f}, {self.v_max:.4f}] m (n={self.n_v}"
|
|
119
|
+
+ (f", inc={self.v_inc:.4f})" if self.v_equally_spaced else ", irregular)"),
|
|
120
|
+
f" z shape : {self.z.shape} (float32)",
|
|
121
|
+
f" has_bank: {self.has_bank}",
|
|
122
|
+
f" closed : {self.u_is_closed}",
|
|
123
|
+
]
|
|
124
|
+
return "\n".join(lines)
|
|
125
|
+
|
|
126
|
+
def channel_info(self) -> str:
|
|
127
|
+
n_channels = 1 + self.n_v # U virtual + v channels
|
|
128
|
+
if self.phi is not None and len(self.phi):
|
|
129
|
+
n_channels += 1
|
|
130
|
+
lines = [f"Channel count: {n_channels}"]
|
|
131
|
+
lines.append(f" U : u coordinate, virtual, [{self.u_min:.4f}, {self.u_max:.4f}] m")
|
|
132
|
+
if self.phi is not None and len(self.phi):
|
|
133
|
+
lines.append(f" phi: heading angle, [{float(self.phi.min()):.4f}, {float(self.phi.max()):.4f}] rad")
|
|
134
|
+
for i, vpos in enumerate(self.v):
|
|
135
|
+
zch = self.z[i]
|
|
136
|
+
lines.append(
|
|
137
|
+
f" D{i+1} : v={vpos:+.4f} m, z in [{float(np.nanmin(zch)+self.z_mean[i]):.4f}, "
|
|
138
|
+
f"{float(np.nanmax(zch)+self.z_mean[i]):.4f}] m"
|
|
139
|
+
)
|
|
140
|
+
return "\n".join(lines)
|
|
141
|
+
|
|
142
|
+
def road_info(self) -> str:
|
|
143
|
+
lines = [
|
|
144
|
+
"Road information:",
|
|
145
|
+
f" reference line : u=[{self.u_min:.4f}, {self.u_max:.4f}] m",
|
|
146
|
+
f" x range : [{float(self.x.min()):.4f}, {float(self.x.max()):.4f}] m",
|
|
147
|
+
f" y range : [{float(self.y.min()):.4f}, {float(self.y.max()):.4f}] m",
|
|
148
|
+
f" phi range : [{float(self.phi.min()):.4f}, {float(self.phi.max()):.4f}] rad",
|
|
149
|
+
f" v range : [{self.v_min:.4f}, {self.v_max:.4f}] m",
|
|
150
|
+
]
|
|
151
|
+
z_all = self.z.astype(np.float64) + self.z_mean[:, np.newaxis]
|
|
152
|
+
lines.append(
|
|
153
|
+
f" z range : [{float(np.nanmin(z_all)):.4f}, {float(np.nanmax(z_all)):.4f}] m"
|
|
154
|
+
)
|
|
155
|
+
return "\n".join(lines)
|
|
156
|
+
|
|
157
|
+
def create_contact_point(
|
|
158
|
+
self,
|
|
159
|
+
*,
|
|
160
|
+
border_mode_u: BorderMode | None = None,
|
|
161
|
+
border_mode_v: BorderMode | None = None,
|
|
162
|
+
border_offset_u: float | None = None,
|
|
163
|
+
border_offset_v: float | None = None,
|
|
164
|
+
smooth_u_begin: float | None = None,
|
|
165
|
+
smooth_u_end: float | None = None,
|
|
166
|
+
ref_line_continue: RefLineContinue | None = None,
|
|
167
|
+
curv_mode: CurvMode | None = None,
|
|
168
|
+
ref_line_search_u: float | None = None,
|
|
169
|
+
ref_line_close: float | None = None,
|
|
170
|
+
ref_line_far: float | None = None,
|
|
171
|
+
history_size: int | None = None,
|
|
172
|
+
) -> ContactPoint:
|
|
173
|
+
"""Create a :class:`ContactPoint` evaluation context for this dataset.
|
|
174
|
+
|
|
175
|
+
All parameters correspond to fields of :class:`~crgutils.EvalOptions`.
|
|
176
|
+
Omitted parameters use the dataset's ``default_options`` or the
|
|
177
|
+
:class:`~crgutils.EvalOptions` defaults.
|
|
178
|
+
|
|
179
|
+
Returns
|
|
180
|
+
-------
|
|
181
|
+
ContactPoint
|
|
182
|
+
A new evaluation context bound to this dataset.
|
|
183
|
+
"""
|
|
184
|
+
from ._contact_point import ContactPoint # lazy import — avoids circular dep
|
|
185
|
+
_locs = locals()
|
|
186
|
+
_option_keys = [
|
|
187
|
+
"border_mode_u", "border_mode_v", "border_offset_u", "border_offset_v",
|
|
188
|
+
"smooth_u_begin", "smooth_u_end", "ref_line_continue", "curv_mode",
|
|
189
|
+
"ref_line_search_u", "ref_line_close", "ref_line_far", "history_size",
|
|
190
|
+
]
|
|
191
|
+
options = {k: _locs[k] for k in _option_keys if _locs[k] is not None}
|
|
192
|
+
return ContactPoint(self, **options)
|