openodd-py 0.2.2__tar.gz → 0.3.0__tar.gz
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.
- {openodd_py-0.2.2 → openodd_py-0.3.0}/Cargo.lock +1 -1
- {openodd_py-0.2.2 → openodd_py-0.3.0}/Cargo.toml +1 -1
- {openodd_py-0.2.2 → openodd_py-0.3.0}/PKG-INFO +21 -1
- {openodd_py-0.2.2 → openodd_py-0.3.0}/README.md +20 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/python/openodd/__init__.py +11 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/python/openodd/_core.pyi +11 -0
- openodd_py-0.3.0/python/openodd/cells.py +76 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/python/openodd/items.py +12 -0
- openodd_py-0.3.0/src/cells.rs +209 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/src/items.rs +23 -2
- {openodd_py-0.2.2 → openodd_py-0.3.0}/src/lib.rs +1 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/src/model.rs +1 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/src/python.rs +57 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/tests/differential/run.py +6 -1
- openodd_py-0.3.0/tests/test_cells.py +83 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/.gitattributes +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/.github/workflows/ci.yml +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/.github/workflows/release-label.yml +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/.github/workflows/release.yml +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/.gitignore +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/LICENSE +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/NOTICE +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/docs/versioning.md +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/pyproject.toml +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/python/openodd/backend.py +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/python/openodd/loader.py +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/python/openodd/model.py +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/python/openodd/odds/default/default.binding.yaml +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/python/openodd/odds/default/default.yaml +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/python/openodd/py.typed +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/python/openodd/report.py +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/python/openodd/sampler.py +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/python/openodd/sources.py +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/src/error.rs +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/src/loader.rs +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/src/report.rs +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/src/sampler.rs +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/src/sources.rs +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/src/transmission.rs +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/src/units.rs +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/src/value.rs +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/src/yaml.rs +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/tests/differential/cases.py +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/tests/differential/compare.py +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/tests/differential/difftest_probes.py +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/tests/differential/report_run.py +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/tests/test_api.py +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/tests/test_differential.py +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/tests/test_sampling.py +0 -0
- {openodd_py-0.2.2 → openodd_py-0.3.0}/tools/bump_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openodd-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: 3
|
|
6
6
|
Classifier: Topic :: Scientific/Engineering
|
|
@@ -44,6 +44,26 @@ odd.revision # sha256 of the files it was read from
|
|
|
44
44
|
odd.archive() # those files as a deterministic .tar.gz
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
### Coverage
|
|
48
|
+
|
|
49
|
+
A coverage document records, besides each item's buckets, how a run spent its
|
|
50
|
+
time across the ODD's buckets: `OddCells` adds up steps, seconds and metres per
|
|
51
|
+
*cell* -- one bucket of each attribute, as its cover item buckets it -- so a
|
|
52
|
+
consumer can slice runs by any attributes without the per-step values:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
cells = openodd.OddCells(odd)
|
|
56
|
+
cells.add(values, seconds=dt, meters=ds) # every step: each attribute's value
|
|
57
|
+
cells.to_dict()
|
|
58
|
+
# {"attributes": ["speed", "rain"], "rows": [[["[30, 60]", "heavy"], 812, 40.6, 588.2], ...]}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
A missing value is `<missing>`, a number outside the buckets `<below>` / `<above>`.
|
|
62
|
+
Each item's definition (`describe()`) names the measure its coverage is read from
|
|
63
|
+
(`amount`: `hits`, `counted.seconds`, ...) and, for an ODD attribute's item, the
|
|
64
|
+
attribute (`attribute`). `openodd.__version__` is the installed version, for
|
|
65
|
+
coverage files to say what wrote them.
|
|
66
|
+
|
|
47
67
|
## Development
|
|
48
68
|
|
|
49
69
|
```bash
|
|
@@ -29,6 +29,26 @@ odd.revision # sha256 of the files it was read from
|
|
|
29
29
|
odd.archive() # those files as a deterministic .tar.gz
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
### Coverage
|
|
33
|
+
|
|
34
|
+
A coverage document records, besides each item's buckets, how a run spent its
|
|
35
|
+
time across the ODD's buckets: `OddCells` adds up steps, seconds and metres per
|
|
36
|
+
*cell* -- one bucket of each attribute, as its cover item buckets it -- so a
|
|
37
|
+
consumer can slice runs by any attributes without the per-step values:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
cells = openodd.OddCells(odd)
|
|
41
|
+
cells.add(values, seconds=dt, meters=ds) # every step: each attribute's value
|
|
42
|
+
cells.to_dict()
|
|
43
|
+
# {"attributes": ["speed", "rain"], "rows": [[["[30, 60]", "heavy"], 812, 40.6, 588.2], ...]}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A missing value is `<missing>`, a number outside the buckets `<below>` / `<above>`.
|
|
47
|
+
Each item's definition (`describe()`) names the measure its coverage is read from
|
|
48
|
+
(`amount`: `hits`, `counted.seconds`, ...) and, for an ODD attribute's item, the
|
|
49
|
+
attribute (`attribute`). `openodd.__version__` is the installed version, for
|
|
50
|
+
coverage files to say what wrote them.
|
|
51
|
+
|
|
32
52
|
## Development
|
|
33
53
|
|
|
34
54
|
```bash
|
|
@@ -16,7 +16,10 @@ What measures the attributes -- a simulator, a recorded log -- is the
|
|
|
16
16
|
application's :class:`Backend`.
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
+
from importlib import metadata as _metadata
|
|
20
|
+
|
|
19
21
|
from ._core import GitSourceError, OpenOddError, UnitError, Units, convert, normalize_unit
|
|
22
|
+
from .cells import MISSING, OddCells
|
|
20
23
|
from .backend import Backend, Probe, default_backend, import_callable, set_default_backend
|
|
21
24
|
from .items import (
|
|
22
25
|
ABOVE_RANGE,
|
|
@@ -65,7 +68,15 @@ from .model import (
|
|
|
65
68
|
read_probe,
|
|
66
69
|
)
|
|
67
70
|
|
|
71
|
+
try:
|
|
72
|
+
__version__: str = _metadata.version("openodd-py")
|
|
73
|
+
except _metadata.PackageNotFoundError: # a source tree, not installed
|
|
74
|
+
__version__ = "0+unknown"
|
|
75
|
+
|
|
68
76
|
__all__ = [
|
|
77
|
+
"MISSING",
|
|
78
|
+
"OddCells",
|
|
79
|
+
"__version__",
|
|
69
80
|
"COVERAGE_SCHEMA",
|
|
70
81
|
"CoverageReport",
|
|
71
82
|
"MergedEntry",
|
|
@@ -47,8 +47,19 @@ class Units:
|
|
|
47
47
|
def convert(self, value: float, from_unit: str, to_unit: str) -> float: ...
|
|
48
48
|
def check_type(self, unit: str, unit_type: str) -> None: ...
|
|
49
49
|
|
|
50
|
+
class Cells:
|
|
51
|
+
def __init__(self, odd: Any) -> None: ...
|
|
52
|
+
@property
|
|
53
|
+
def attributes(self) -> list[str]: ...
|
|
54
|
+
@property
|
|
55
|
+
def ticks(self) -> int: ...
|
|
56
|
+
def cell_of(self, values: list[Any]) -> list[str]: ...
|
|
57
|
+
def add(self, values: list[Any], seconds: float = 0.0, meters: float = 0.0) -> None: ...
|
|
58
|
+
def to_json(self) -> str: ...
|
|
59
|
+
|
|
50
60
|
UNDECIDED: Any
|
|
51
61
|
CACHE_ENV: str
|
|
62
|
+
MISSING: str
|
|
52
63
|
|
|
53
64
|
def convert(value: float, from_unit: str, to_unit: str) -> float: ...
|
|
54
65
|
def normalize_unit(unit: str) -> str: ...
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Cells: how long a run spent in each combination of the ODD's buckets.
|
|
2
|
+
|
|
3
|
+
Every step, each attribute with buckets puts its value in one (its cover
|
|
4
|
+
item's ``bucket_of``); the labels together are the step's *cell*.
|
|
5
|
+
:class:`OddCells` adds up steps, seconds and metres per cell, so a consumer can
|
|
6
|
+
slice a run by any attributes afterwards -- a matrix of two of them inside a
|
|
7
|
+
third's bucket -- without keeping every step's values, and without bucketing
|
|
8
|
+
them again by rules of its own::
|
|
9
|
+
|
|
10
|
+
cells = OddCells(odd)
|
|
11
|
+
for step in run: # every attribute's value, in order
|
|
12
|
+
cells.add(step.values, seconds=step.dt, meters=step.ds)
|
|
13
|
+
cells.to_dict()
|
|
14
|
+
# {"attributes": ["scenery.speed_limit", "environment.rain"],
|
|
15
|
+
# "rows": [[["[50, 60)", "none"], 812, 40.6, 588.2], ...]}
|
|
16
|
+
|
|
17
|
+
A missing value is :data:`MISSING`; a number outside the buckets
|
|
18
|
+
``<below>``/``<above>``; a category none of the values names keeps its own
|
|
19
|
+
label. Attributes without buckets are left out of the cell.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
from collections.abc import Mapping, Sequence
|
|
26
|
+
from typing import TYPE_CHECKING, Any, Union
|
|
27
|
+
|
|
28
|
+
from . import _core
|
|
29
|
+
from .model import to_core
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from .model import OddDefinition
|
|
33
|
+
|
|
34
|
+
__all__ = ["MISSING", "OddCells"]
|
|
35
|
+
|
|
36
|
+
#: The label of a step whose value was missing.
|
|
37
|
+
MISSING: str = _core.MISSING
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OddCells:
|
|
41
|
+
"""Steps, seconds and metres per cell of *odd*'s attributes."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, odd: "OddDefinition") -> None:
|
|
44
|
+
self._names = [a.name for a in odd.attributes]
|
|
45
|
+
self._core = _core.Cells(odd._core)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def attributes(self) -> list[str]:
|
|
49
|
+
"""The attributes with buckets: what a cell's labels are of, in order."""
|
|
50
|
+
return list(self._core.attributes)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def ticks(self) -> int:
|
|
54
|
+
"""Steps counted so far."""
|
|
55
|
+
return int(self._core.ticks)
|
|
56
|
+
|
|
57
|
+
def _values(self, values: Union[Sequence[Any], Mapping[str, Any]]) -> list[Any]:
|
|
58
|
+
if isinstance(values, Mapping):
|
|
59
|
+
return [to_core(values.get(name)) for name in self._names]
|
|
60
|
+
return [to_core(v) for v in values]
|
|
61
|
+
|
|
62
|
+
def cell_of(self, values: Union[Sequence[Any], Mapping[str, Any]]) -> list[str]:
|
|
63
|
+
"""The cell of one step: *values* by attribute name, or one per
|
|
64
|
+
attribute of the ODD in its order."""
|
|
65
|
+
return list(self._core.cell_of(self._values(values)))
|
|
66
|
+
|
|
67
|
+
def add(self, values: Union[Sequence[Any], Mapping[str, Any]], seconds: float = 0.0,
|
|
68
|
+
meters: float = 0.0) -> None:
|
|
69
|
+
"""Count one step that took *seconds* and covered *meters*."""
|
|
70
|
+
self._core.add(self._values(values), float(seconds), float(meters))
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> dict[str, Any]:
|
|
73
|
+
"""``{"attributes": [...], "rows": [[[label, ...], ticks, seconds, meters], ...]}``,
|
|
74
|
+
most steps first (ties by labels); seconds and metres to 3 decimals."""
|
|
75
|
+
out: dict[str, Any] = json.loads(self._core.to_json())
|
|
76
|
+
return out
|
|
@@ -119,6 +119,8 @@ class CoverItem:
|
|
|
119
119
|
group: CoverGroup = CoverGroup.SCENARIO
|
|
120
120
|
cover_by: str = "hits"
|
|
121
121
|
min_stay: Optional[float] = None
|
|
122
|
+
#: The ODD attribute the item buckets, when it is one's.
|
|
123
|
+
attribute: Optional[str] = None
|
|
122
124
|
|
|
123
125
|
#: Bucket edges of a numeric item, ascending; empty for a categorical one.
|
|
124
126
|
edges: list[float] = field(init=False, default_factory=list)
|
|
@@ -173,6 +175,7 @@ class CoverItem:
|
|
|
173
175
|
self.group = group
|
|
174
176
|
self.cover_by = bucketing.cover_by
|
|
175
177
|
self.min_stay = bucketing.min_stay
|
|
178
|
+
self.attribute = bucketing.attribute
|
|
176
179
|
self._bucketing = bucketing
|
|
177
180
|
return self
|
|
178
181
|
|
|
@@ -181,6 +184,12 @@ class CoverItem:
|
|
|
181
184
|
"""Whether the buckets are ranges rather than values."""
|
|
182
185
|
return bool(self.edges)
|
|
183
186
|
|
|
187
|
+
@property
|
|
188
|
+
def amount(self) -> str:
|
|
189
|
+
"""The measure a bucket's coverage is read from: ``cover_by``, or with
|
|
190
|
+
a ``min_stay`` the stays long enough to count (``counted.<cover_by>``)."""
|
|
191
|
+
return f"counted.{self.cover_by}" if self.min_stay is not None else self.cover_by
|
|
192
|
+
|
|
184
193
|
def bucket_of(self, value: Any) -> str:
|
|
185
194
|
"""The label of the bucket *value* falls in.
|
|
186
195
|
|
|
@@ -208,6 +217,8 @@ class CoverItem:
|
|
|
208
217
|
"target": self.target,
|
|
209
218
|
"cover_by": self.cover_by,
|
|
210
219
|
"min_stay": self.min_stay,
|
|
220
|
+
"amount": self.amount,
|
|
221
|
+
**({"attribute": self.attribute} if self.attribute is not None else {}),
|
|
211
222
|
}
|
|
212
223
|
|
|
213
224
|
|
|
@@ -267,4 +278,5 @@ class CrossItem:
|
|
|
267
278
|
"target": self.target,
|
|
268
279
|
"cover_by": self.cover_by,
|
|
269
280
|
"min_stay": self.min_stay,
|
|
281
|
+
"amount": f"counted.{self.cover_by}" if self.min_stay is not None else self.cover_by,
|
|
270
282
|
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
//! Cells: how long a run spent in each combination of the ODD's buckets.
|
|
2
|
+
//!
|
|
3
|
+
//! Every step, each attribute with buckets (a cover item) puts its value in a
|
|
4
|
+
//! bucket; the tuple of those labels is the step's *cell*. A [`CellCounter`]
|
|
5
|
+
//! adds up the steps, seconds and metres per cell, so a consumer can slice the
|
|
6
|
+
//! run by any attributes afterwards without the per-step values -- and buckets
|
|
7
|
+
//! them exactly as the cover items do, with no second copy of the rules.
|
|
8
|
+
//!
|
|
9
|
+
//! A missing value is [`MISSING`]; a numeric value below or above every bucket
|
|
10
|
+
//! is [`BELOW_RANGE`] / [`ABOVE_RANGE`]; a categorical value that is none of
|
|
11
|
+
//! the values keeps its own label (as [`CoverItem::bucket_of`] does).
|
|
12
|
+
|
|
13
|
+
use indexmap::IndexMap;
|
|
14
|
+
use serde_json::{json, Value as Json};
|
|
15
|
+
|
|
16
|
+
use crate::error::{Error, Result};
|
|
17
|
+
use crate::items::CoverItem;
|
|
18
|
+
use crate::model::Odd;
|
|
19
|
+
use crate::value::Value;
|
|
20
|
+
|
|
21
|
+
/// The label of a step whose value was missing (or could not be bucketed).
|
|
22
|
+
pub const MISSING: &str = "<missing>";
|
|
23
|
+
|
|
24
|
+
/// Steps, seconds and metres per cell of an ODD's attributes.
|
|
25
|
+
#[derive(Debug, Clone)]
|
|
26
|
+
pub struct CellCounter {
|
|
27
|
+
/// The attributes with buckets, in the ODD's order: the cell's labels.
|
|
28
|
+
pub attributes: Vec<String>,
|
|
29
|
+
/// For each of the ODD's attributes (all of them, in order), its item.
|
|
30
|
+
items: Vec<Option<CoverItem>>,
|
|
31
|
+
cells: IndexMap<Vec<String>, (u64, f64, f64)>,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fn round3(x: f64) -> f64 {
|
|
35
|
+
(x * 1000.0).round() / 1000.0
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
impl CellCounter {
|
|
39
|
+
pub fn new(odd: &Odd) -> Self {
|
|
40
|
+
let items: Vec<Option<CoverItem>> = odd.attributes.iter().map(|a| a.item.clone()).collect();
|
|
41
|
+
let attributes = odd
|
|
42
|
+
.attributes
|
|
43
|
+
.iter()
|
|
44
|
+
.filter(|a| a.item.is_some())
|
|
45
|
+
.map(|a| a.name.clone())
|
|
46
|
+
.collect();
|
|
47
|
+
CellCounter {
|
|
48
|
+
attributes,
|
|
49
|
+
items,
|
|
50
|
+
cells: IndexMap::new(),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// The cell of one step: `values` has one value per attribute of the ODD
|
|
55
|
+
/// (every attribute, in the ODD's order, as samples are recorded).
|
|
56
|
+
pub fn cell_of(&self, values: &[Value]) -> Result<Vec<String>> {
|
|
57
|
+
if values.len() != self.items.len() {
|
|
58
|
+
return Err(Error::value(format!(
|
|
59
|
+
"cells: {} values for {} attributes",
|
|
60
|
+
values.len(),
|
|
61
|
+
self.items.len()
|
|
62
|
+
)));
|
|
63
|
+
}
|
|
64
|
+
Ok(self
|
|
65
|
+
.items
|
|
66
|
+
.iter()
|
|
67
|
+
.zip(values)
|
|
68
|
+
.filter_map(|(item, value)| item.as_ref().map(|item| (item, value)))
|
|
69
|
+
.map(|(item, value)| match value {
|
|
70
|
+
Value::Missing | Value::Open => MISSING.to_string(),
|
|
71
|
+
v => item.bucket_of(v).unwrap_or_else(|_| MISSING.to_string()),
|
|
72
|
+
})
|
|
73
|
+
.collect())
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Count one step that took `seconds` and covered `meters`.
|
|
77
|
+
pub fn add(&mut self, values: &[Value], seconds: f64, meters: f64) -> Result<()> {
|
|
78
|
+
let cell = self.cell_of(values)?;
|
|
79
|
+
let entry = self.cells.entry(cell).or_insert((0, 0.0, 0.0));
|
|
80
|
+
entry.0 += 1;
|
|
81
|
+
entry.1 += seconds.max(0.0);
|
|
82
|
+
entry.2 += meters.max(0.0);
|
|
83
|
+
Ok(())
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Steps counted so far.
|
|
87
|
+
pub fn ticks(&self) -> u64 {
|
|
88
|
+
self.cells.values().map(|c| c.0).sum()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// The cells: labels, steps, seconds, metres; most steps first, then by labels.
|
|
92
|
+
pub fn rows(&self) -> Vec<(Vec<String>, u64, f64, f64)> {
|
|
93
|
+
let mut rows: Vec<_> = self
|
|
94
|
+
.cells
|
|
95
|
+
.iter()
|
|
96
|
+
.map(|(k, (t, s, m))| (k.clone(), *t, round3(*s), round3(*m)))
|
|
97
|
+
.collect();
|
|
98
|
+
rows.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
|
|
99
|
+
rows
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// `{"attributes": [...], "rows": [[[label, ...], ticks, seconds, meters], ...]}`.
|
|
103
|
+
pub fn to_json(&self) -> Json {
|
|
104
|
+
json!({
|
|
105
|
+
"attributes": self.attributes,
|
|
106
|
+
"rows": self.rows().into_iter()
|
|
107
|
+
.map(|(labels, t, s, m)| json!([labels, t, s, m]))
|
|
108
|
+
.collect::<Vec<_>>(),
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#[cfg(test)]
|
|
114
|
+
mod tests {
|
|
115
|
+
use super::*;
|
|
116
|
+
use crate::items::{Buckets, Criteria};
|
|
117
|
+
use crate::model::{Attribute, Probe};
|
|
118
|
+
|
|
119
|
+
fn odd() -> Odd {
|
|
120
|
+
let mut speed =
|
|
121
|
+
Attribute::new("speed", Probe::Missing, "km/h", "", Criteria::default()).unwrap();
|
|
122
|
+
speed
|
|
123
|
+
.set_buckets(Buckets::Edges(vec![0.0, 30.0, 60.0]))
|
|
124
|
+
.unwrap();
|
|
125
|
+
let mut rain = Attribute::new("rain", Probe::Missing, "", "", Criteria::default()).unwrap();
|
|
126
|
+
rain.set_buckets(Buckets::Values(vec![
|
|
127
|
+
Value::Str("none".into()),
|
|
128
|
+
Value::Str("heavy".into()),
|
|
129
|
+
]))
|
|
130
|
+
.unwrap();
|
|
131
|
+
let free = Attribute::new("free", Probe::Missing, "", "", Criteria::default()).unwrap();
|
|
132
|
+
Odd::new("t", vec![speed, free, rain], vec![], None, "").unwrap()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#[test]
|
|
136
|
+
fn buckets_like_the_items() {
|
|
137
|
+
let c = CellCounter::new(&odd());
|
|
138
|
+
assert_eq!(c.attributes, vec!["speed", "rain"]);
|
|
139
|
+
let cell = |v: Vec<Value>| c.cell_of(&v).unwrap();
|
|
140
|
+
assert_eq!(
|
|
141
|
+
cell(vec![
|
|
142
|
+
Value::Float(30.0),
|
|
143
|
+
Value::Int(3),
|
|
144
|
+
Value::Str("none".into())
|
|
145
|
+
]),
|
|
146
|
+
vec!["[30, 60]", "none"]
|
|
147
|
+
);
|
|
148
|
+
assert_eq!(
|
|
149
|
+
cell(vec![
|
|
150
|
+
Value::Float(-1.0),
|
|
151
|
+
Value::Missing,
|
|
152
|
+
Value::Str("fog".into())
|
|
153
|
+
]),
|
|
154
|
+
vec!["<below>", "fog"]
|
|
155
|
+
);
|
|
156
|
+
assert_eq!(
|
|
157
|
+
cell(vec![Value::Float(61.0), Value::Missing, Value::Missing]),
|
|
158
|
+
vec!["<above>", MISSING]
|
|
159
|
+
);
|
|
160
|
+
assert_eq!(
|
|
161
|
+
cell(vec![Value::Str("x".into()), Value::Missing, Value::Open]),
|
|
162
|
+
vec![MISSING, MISSING]
|
|
163
|
+
);
|
|
164
|
+
assert!(c.cell_of(&[Value::Missing]).is_err());
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[test]
|
|
168
|
+
fn counts_and_orders() {
|
|
169
|
+
let mut c = CellCounter::new(&odd());
|
|
170
|
+
let a = vec![
|
|
171
|
+
Value::Float(10.0),
|
|
172
|
+
Value::Missing,
|
|
173
|
+
Value::Str("none".into()),
|
|
174
|
+
];
|
|
175
|
+
let b = vec![
|
|
176
|
+
Value::Float(40.0),
|
|
177
|
+
Value::Missing,
|
|
178
|
+
Value::Str("heavy".into()),
|
|
179
|
+
];
|
|
180
|
+
let z = vec![
|
|
181
|
+
Value::Float(5.0),
|
|
182
|
+
Value::Missing,
|
|
183
|
+
Value::Str("heavy".into()),
|
|
184
|
+
];
|
|
185
|
+
c.add(&a, 0.05, 0.1).unwrap();
|
|
186
|
+
c.add(&b, 0.05, 0.1).unwrap();
|
|
187
|
+
c.add(&b, 0.0333333, 0.2).unwrap();
|
|
188
|
+
c.add(&z, 0.05, -1.0).unwrap();
|
|
189
|
+
assert_eq!(c.ticks(), 4);
|
|
190
|
+
let rows = c.rows();
|
|
191
|
+
assert_eq!(
|
|
192
|
+
rows[0],
|
|
193
|
+
(
|
|
194
|
+
vec!["[30, 60]".to_string(), "heavy".to_string()],
|
|
195
|
+
2,
|
|
196
|
+
0.083,
|
|
197
|
+
0.3
|
|
198
|
+
)
|
|
199
|
+
);
|
|
200
|
+
// ties by labels: "[0, 30)" < ... ("heavy" < "none")
|
|
201
|
+
assert_eq!(rows[1].0, vec!["[0, 30)", "heavy"]);
|
|
202
|
+
assert_eq!(rows[1].3, 0.0); // negative displacement is not distance
|
|
203
|
+
assert_eq!(rows[2].0, vec!["[0, 30)", "none"]);
|
|
204
|
+
assert_eq!(
|
|
205
|
+
c.to_json()["rows"][0],
|
|
206
|
+
json!([["[30, 60]", "heavy"], 2, 0.083, 0.3])
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -65,6 +65,16 @@ impl Default for Criteria {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
impl Criteria {
|
|
68
|
+
/// The measure a bucket's coverage is read from: `cover_by`, or with a
|
|
69
|
+
/// `min_stay` the stays long enough to count (`counted.<cover_by>`).
|
|
70
|
+
pub fn amount(&self) -> String {
|
|
71
|
+
if self.min_stay.is_some() {
|
|
72
|
+
format!("counted.{}", self.cover_by)
|
|
73
|
+
} else {
|
|
74
|
+
self.cover_by.clone()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
68
78
|
/// Refuse a criterion that cannot be met or measured on `event`.
|
|
69
79
|
pub fn check(&self, what: &str, event: &str) -> Result<()> {
|
|
70
80
|
if !COVER_MEASURES.contains(&self.cover_by.as_str()) {
|
|
@@ -133,6 +143,8 @@ pub struct CoverItem {
|
|
|
133
143
|
pub edges: Vec<f64>,
|
|
134
144
|
/// Bucket labels, in order.
|
|
135
145
|
pub labels: Vec<String>,
|
|
146
|
+
/// The ODD attribute the item buckets, when it is one's (`odd.<name>`).
|
|
147
|
+
pub attribute: Option<String>,
|
|
136
148
|
}
|
|
137
149
|
|
|
138
150
|
fn duplicates(names: &[String]) -> Vec<String> {
|
|
@@ -170,6 +182,7 @@ impl CoverItem {
|
|
|
170
182
|
values: Vec::new(),
|
|
171
183
|
edges: Vec::new(),
|
|
172
184
|
labels: Vec::new(),
|
|
185
|
+
attribute: None,
|
|
173
186
|
};
|
|
174
187
|
let name = item.name.clone();
|
|
175
188
|
match buckets {
|
|
@@ -267,8 +280,11 @@ impl CoverItem {
|
|
|
267
280
|
}
|
|
268
281
|
|
|
269
282
|
/// The item's definition, as written to a coverage file.
|
|
283
|
+
///
|
|
284
|
+
/// `amount` names the measure a bucket's coverage is read from (see
|
|
285
|
+
/// [`Criteria::amount`]); `attribute` is there for an ODD attribute's item.
|
|
270
286
|
pub fn describe(&self) -> Json {
|
|
271
|
-
json!({
|
|
287
|
+
let mut out = json!({
|
|
272
288
|
"name": self.name,
|
|
273
289
|
"group": self.group.as_str(),
|
|
274
290
|
"text": self.text,
|
|
@@ -279,7 +295,12 @@ impl CoverItem {
|
|
|
279
295
|
"target": number_json(self.criteria.target),
|
|
280
296
|
"cover_by": self.criteria.cover_by,
|
|
281
297
|
"min_stay": self.criteria.min_stay,
|
|
282
|
-
|
|
298
|
+
"amount": self.criteria.amount(),
|
|
299
|
+
});
|
|
300
|
+
if let Some(attribute) = &self.attribute {
|
|
301
|
+
out["attribute"] = json!(attribute);
|
|
302
|
+
}
|
|
303
|
+
out
|
|
283
304
|
}
|
|
284
305
|
}
|
|
285
306
|
|
|
@@ -251,6 +251,16 @@ impl Bucketing {
|
|
|
251
251
|
fn group(&self) -> &'static str {
|
|
252
252
|
self.inner.group.as_str()
|
|
253
253
|
}
|
|
254
|
+
/// The ODD attribute the item buckets, when it is one's.
|
|
255
|
+
#[getter]
|
|
256
|
+
fn attribute(&self) -> Option<String> {
|
|
257
|
+
self.inner.attribute.clone()
|
|
258
|
+
}
|
|
259
|
+
/// The measure a bucket's coverage is read from.
|
|
260
|
+
#[getter]
|
|
261
|
+
fn amount(&self) -> String {
|
|
262
|
+
self.inner.criteria.amount()
|
|
263
|
+
}
|
|
254
264
|
|
|
255
265
|
/// The label of the bucket `value` falls in.
|
|
256
266
|
fn bucket_of(&self, value: &Bound<'_, PyAny>) -> PyResult<String> {
|
|
@@ -258,6 +268,51 @@ impl Bucketing {
|
|
|
258
268
|
}
|
|
259
269
|
}
|
|
260
270
|
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// Cells
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
/// Steps, seconds and metres per combination of an ODD's buckets.
|
|
276
|
+
#[pyclass(module = "openodd._core")]
|
|
277
|
+
struct Cells {
|
|
278
|
+
inner: crate::cells::CellCounter,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
#[pymethods]
|
|
282
|
+
impl Cells {
|
|
283
|
+
#[new]
|
|
284
|
+
fn new(odd: &Odd) -> Self {
|
|
285
|
+
Cells {
|
|
286
|
+
inner: crate::cells::CellCounter::new(&odd.inner),
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/// The attributes with buckets: the labels of a cell, in order.
|
|
290
|
+
#[getter]
|
|
291
|
+
fn attributes(&self) -> Vec<String> {
|
|
292
|
+
self.inner.attributes.clone()
|
|
293
|
+
}
|
|
294
|
+
/// Steps counted so far.
|
|
295
|
+
#[getter]
|
|
296
|
+
fn ticks(&self) -> u64 {
|
|
297
|
+
self.inner.ticks()
|
|
298
|
+
}
|
|
299
|
+
/// The cell of one step (one value per attribute of the ODD, in order).
|
|
300
|
+
fn cell_of(&self, values: Vec<Bound<'_, PyAny>>) -> PyResult<Vec<String>> {
|
|
301
|
+
let values = values.iter().map(to_value).collect::<PyResult<Vec<_>>>()?;
|
|
302
|
+
self.inner.cell_of(&values).map_err(err)
|
|
303
|
+
}
|
|
304
|
+
/// Count one step that took `seconds` and covered `meters`.
|
|
305
|
+
#[pyo3(signature = (values, seconds=0.0, meters=0.0))]
|
|
306
|
+
fn add(&mut self, values: Vec<Bound<'_, PyAny>>, seconds: f64, meters: f64) -> PyResult<()> {
|
|
307
|
+
let values = values.iter().map(to_value).collect::<PyResult<Vec<_>>>()?;
|
|
308
|
+
self.inner.add(&values, seconds, meters).map_err(err)
|
|
309
|
+
}
|
|
310
|
+
/// `{"attributes": [...], "rows": [[[label, ...], ticks, seconds, meters], ...]}`.
|
|
311
|
+
fn to_json(&self) -> String {
|
|
312
|
+
self.inner.to_json().to_string()
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
261
316
|
// ---------------------------------------------------------------------------
|
|
262
317
|
// Conditions, attributes, modules
|
|
263
318
|
// ---------------------------------------------------------------------------
|
|
@@ -1537,6 +1592,8 @@ fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
1537
1592
|
m.add("CACHE_ENV", crate::sources::CACHE_ENV)?;
|
|
1538
1593
|
m.add_class::<Undecided>()?;
|
|
1539
1594
|
m.add_class::<Bucketing>()?;
|
|
1595
|
+
m.add_class::<Cells>()?;
|
|
1596
|
+
m.add("MISSING", crate::cells::MISSING)?;
|
|
1540
1597
|
m.add_class::<Condition>()?;
|
|
1541
1598
|
m.add_class::<Attribute>()?;
|
|
1542
1599
|
m.add_class::<Module>()?;
|
|
@@ -18,6 +18,9 @@ from typing import Any
|
|
|
18
18
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
19
19
|
from cases import all_cases, contexts # noqa: E402
|
|
20
20
|
|
|
21
|
+
#: Keys of an item's definition the reference does not write.
|
|
22
|
+
NEWER_KEYS = ("amount", "attribute")
|
|
23
|
+
|
|
21
24
|
|
|
22
25
|
def plain(x: Any) -> Any:
|
|
23
26
|
if isinstance(x, float):
|
|
@@ -60,7 +63,9 @@ def run(impl: str, case: dict[str, Any]) -> dict[str, Any]:
|
|
|
60
63
|
for a in odd.attributes
|
|
61
64
|
],
|
|
62
65
|
"modules": [m.describe() for m in odd.modules],
|
|
63
|
-
|
|
66
|
+
# amount / attribute came after the reference: compare what both write.
|
|
67
|
+
"situations": [{k: v for k, v in m.item.describe().items() if k not in NEWER_KEYS}
|
|
68
|
+
for m in odd.situations()],
|
|
64
69
|
"unmeasured": odd.unmeasured(),
|
|
65
70
|
}
|
|
66
71
|
verdicts = []
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Cells: steps per combination of an ODD's buckets, bucketed as the items are."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import enum
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
import openodd
|
|
10
|
+
from openodd import (
|
|
11
|
+
ABOVE_RANGE,
|
|
12
|
+
BELOW_RANGE,
|
|
13
|
+
MISSING,
|
|
14
|
+
CoverItem,
|
|
15
|
+
OddAttribute,
|
|
16
|
+
OddCells,
|
|
17
|
+
OddDefinition,
|
|
18
|
+
OddModule,
|
|
19
|
+
SamplingEvent,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Rain(enum.Enum):
|
|
24
|
+
NONE = "none"
|
|
25
|
+
HEAVY = "heavy"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _odd() -> OddDefinition:
|
|
29
|
+
speed = OddAttribute("speed", lambda c: c["speed"], unit="km/h", buckets=[0, 30, 60],
|
|
30
|
+
cover_by="seconds", min_stay=2)
|
|
31
|
+
free = OddAttribute("free", lambda c: c["free"]) # no buckets: not in a cell
|
|
32
|
+
rain = OddAttribute("rain", lambda c: c["rain"], values=list(Rain))
|
|
33
|
+
root = OddModule("root", include_and=[speed.less_than(60)])
|
|
34
|
+
return OddDefinition("cells", [speed, free, rain], [root])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_a_cell_is_the_items_buckets():
|
|
38
|
+
odd = _odd()
|
|
39
|
+
cells = OddCells(odd)
|
|
40
|
+
assert cells.attributes == ["speed", "rain"]
|
|
41
|
+
speed, rain = (a.item for a in odd.attributes if a.item is not None)
|
|
42
|
+
for value in (0, 29.9, 30, 60):
|
|
43
|
+
assert cells.cell_of([value, 1, Rain.NONE])[0] == speed.bucket_of(value)
|
|
44
|
+
assert cells.cell_of([-1, None, Rain.HEAVY]) == [BELOW_RANGE, rain.bucket_of(Rain.HEAVY)]
|
|
45
|
+
assert cells.cell_of([61, None, None]) == [ABOVE_RANGE, MISSING]
|
|
46
|
+
assert cells.cell_of([None, None, "fog"]) == [MISSING, "fog"]
|
|
47
|
+
assert cells.cell_of({"speed": 10, "rain": Rain.NONE}) == ["[0, 30)", "NONE"]
|
|
48
|
+
with pytest.raises(ValueError):
|
|
49
|
+
cells.cell_of([1, 2])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_cells_count_steps_seconds_and_metres():
|
|
53
|
+
cells = OddCells(_odd())
|
|
54
|
+
for _ in range(2):
|
|
55
|
+
cells.add([40, None, Rain.HEAVY], seconds=0.0333333, meters=0.5)
|
|
56
|
+
cells.add([10, None, Rain.NONE], seconds=0.05, meters=0.25)
|
|
57
|
+
cells.add([10, None, Rain.HEAVY], seconds=0.05)
|
|
58
|
+
assert cells.ticks == 4
|
|
59
|
+
assert cells.to_dict() == {
|
|
60
|
+
"attributes": ["speed", "rain"],
|
|
61
|
+
"rows": [
|
|
62
|
+
[["[30, 60]", "HEAVY"], 2, 0.067, 1.0],
|
|
63
|
+
[["[0, 30)", "HEAVY"], 1, 0.05, 0.0], # ties by labels
|
|
64
|
+
[["[0, 30)", "NONE"], 1, 0.05, 0.25],
|
|
65
|
+
],
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_items_say_their_attribute_and_amount():
|
|
70
|
+
odd = _odd()
|
|
71
|
+
speed = next(a.item for a in odd.attributes if a.name == "speed")
|
|
72
|
+
described = speed.describe()
|
|
73
|
+
assert described["attribute"] == "speed" and described["amount"] == "counted.seconds"
|
|
74
|
+
rain = next(a.item for a in odd.attributes if a.name == "rain").describe()
|
|
75
|
+
assert rain["attribute"] == "rain" and rain["amount"] == "hits"
|
|
76
|
+
plain = CoverItem("gap", lambda c: c, buckets=[0, 1], event=SamplingEvent.TICK,
|
|
77
|
+
cover_by="meters")
|
|
78
|
+
assert plain.describe()["amount"] == "meters" and "attribute" not in plain.describe()
|
|
79
|
+
assert [i.describe()["attribute"] for i in odd.cover_items()] == ["speed", "rain"]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_the_version_is_the_distributions():
|
|
83
|
+
assert openodd.__version__ and openodd.__version__[0].isdigit()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|