mobev 0.1.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.
- mobev-0.1.0/LICENSE +25 -0
- mobev-0.1.0/PKG-INFO +196 -0
- mobev-0.1.0/README.md +172 -0
- mobev-0.1.0/mobev/__init__.py +0 -0
- mobev-0.1.0/mobev/charging/__init__.py +1 -0
- mobev-0.1.0/mobev/charging/charging_availability_assigner.py +187 -0
- mobev-0.1.0/mobev/charging/ev_fleet_sampler.py +140 -0
- mobev-0.1.0/mobev/charging/parking_interval_builder.py +207 -0
- mobev-0.1.0/mobev/charging/parking_location_normalizer.py +83 -0
- mobev-0.1.0/mobev/charging/trip_schedule_validator.py +53 -0
- mobev-0.1.0/mobev/core/charging_resolver.py +149 -0
- mobev-0.1.0/mobev/core/config.py +579 -0
- mobev-0.1.0/mobev/core/pipeline.py +129 -0
- mobev-0.1.0/mobev/core/profile_simulator.py +150 -0
- mobev-0.1.0/mobev/core/result.py +144 -0
- mobev-0.1.0/mobev/core/trip_preparer.py +142 -0
- mobev-0.1.0/mobev/load_profiles/__init__.py +1 -0
- mobev-0.1.0/mobev/load_profiles/battery_state_initializer.py +46 -0
- mobev-0.1.0/mobev/load_profiles/charging_session_builder.py +775 -0
- mobev-0.1.0/mobev/load_profiles/charging_start_planner.py +78 -0
- mobev-0.1.0/mobev/load_profiles/load_profile_aggregator.py +138 -0
- mobev-0.1.0/mobev/load_profiles/tariff_manager.py +442 -0
- mobev-0.1.0/mobev/trips/__init__.py +1 -0
- mobev-0.1.0/mobev/trips/trip_feature_builder.py +180 -0
- mobev-0.1.0/mobev/trips/trip_location_indexer.py +116 -0
- mobev-0.1.0/mobev/trips/weekly_trip_loader.py +134 -0
- mobev-0.1.0/mobev/utils/logger.py +96 -0
- mobev-0.1.0/mobev/utils/plot_profile.py +1132 -0
- mobev-0.1.0/mobev/utils/plot_trips.py +365 -0
- mobev-0.1.0/mobev/utils/results_indicators.py +132 -0
- mobev-0.1.0/mobev.egg-info/PKG-INFO +196 -0
- mobev-0.1.0/mobev.egg-info/SOURCES.txt +41 -0
- mobev-0.1.0/mobev.egg-info/dependency_links.txt +1 -0
- mobev-0.1.0/mobev.egg-info/requires.txt +9 -0
- mobev-0.1.0/mobev.egg-info/top_level.txt +1 -0
- mobev-0.1.0/pyproject.toml +63 -0
- mobev-0.1.0/setup.cfg +4 -0
- mobev-0.1.0/tests/test_charging_pipeline.py +136 -0
- mobev-0.1.0/tests/test_config.py +95 -0
- mobev-0.1.0/tests/test_pipeline.py +72 -0
- mobev-0.1.0/tests/test_profile_pipeline.py +125 -0
- mobev-0.1.0/tests/test_tariff_pipeline.py +51 -0
- mobev-0.1.0/tests/test_trip_pipeline.py +99 -0
mobev-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
=====================
|
|
3
|
+
|
|
4
|
+
- Copyright © `2026` `Yoann CHICHE`
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person
|
|
7
|
+
obtaining a copy of this software and associated documentation
|
|
8
|
+
files (the “Software”), to deal in the Software without
|
|
9
|
+
restriction, including without limitation the rights to use,
|
|
10
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the
|
|
12
|
+
Software is furnished to do so, subject to the following
|
|
13
|
+
conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be
|
|
16
|
+
included in all copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
|
|
19
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
20
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
21
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
22
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
23
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
24
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
25
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
mobev-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mobev
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MobEV: mobility-based, geolocalized EV charging-demand model.
|
|
5
|
+
Author-email: Yoann Chiche <yoann.chiche@minesparis.psl.eu>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://git.persee.minesparis.psl.eu/planeterr/mobev
|
|
8
|
+
Project-URL: Documentation, https://pages.persee.minesparis.psl.eu/planeterr/mobev/
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.13
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: pandas
|
|
15
|
+
Requires-Dist: numpy
|
|
16
|
+
Requires-Dist: geopandas
|
|
17
|
+
Requires-Dist: shapely>=2
|
|
18
|
+
Requires-Dist: polars
|
|
19
|
+
Requires-Dist: PyYAML
|
|
20
|
+
Requires-Dist: matplotlib
|
|
21
|
+
Requires-Dist: pyarrow
|
|
22
|
+
Requires-Dist: colorlog
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# MobEV
|
|
26
|
+
|
|
27
|
+
[](https://www.python.org/)
|
|
28
|
+
[](https://pypi.org/project/mobev/)
|
|
29
|
+
[](https://docs.astral.sh/uv/)
|
|
30
|
+
[](https://git.persee.minesparis.psl.eu/planeterr/mobev/-/tree/main/tests)
|
|
31
|
+
[](https://pages.persee.minesparis.psl.eu/planeterr/mobev/)
|
|
32
|
+
|
|
33
|
+
MobEV is a mobility-based, geolocalized EV charging-demand model. It
|
|
34
|
+
generates EV charging-demand profiles for distribution-network studies from
|
|
35
|
+
weekly car-trip data. The model samples an EV fleet and charging access,
|
|
36
|
+
simulates battery state of charge and charging sessions, and aggregates the
|
|
37
|
+
resulting demand at the configured timestep.
|
|
38
|
+
|
|
39
|
+
Full reference documentation: **[https://pages.persee.minesparis.psl.eu/planeterr/mobev/](https://pages.persee.minesparis.psl.eu/planeterr/mobev/)**.
|
|
40
|
+
|
|
41
|
+
## Requirements and installation
|
|
42
|
+
|
|
43
|
+
MobEV requires Python 3.13 or newer.
|
|
44
|
+
|
|
45
|
+
Install the published package with pip:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python -m pip install mobev
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or add it to an existing uv-managed project:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv add mobev
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
For development from a repository checkout, use the included lock file:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uv sync
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
To build the documentation locally:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv sync --group docs
|
|
67
|
+
make -C docs html
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Inputs
|
|
71
|
+
|
|
72
|
+
Each simulation needs a trip directory containing:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
weekday_trips.gpkg
|
|
76
|
+
weekend_trips.gpkg
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The source data must provide person and activity identifiers, departure and
|
|
80
|
+
arrival times, activity purposes, car mode, and trip geometries. They are generated using a modified version of the EQASIM tool : <https://github.com/RatonMagique/eqasim-france/tree/feat/iris-commune-weekday-pipeline-improvments> . See
|
|
81
|
+
[input documentation](https://pages.persee.minesparis.psl.eu/planeterr/mobev/inputs.html) for the complete input
|
|
82
|
+
schema.
|
|
83
|
+
|
|
84
|
+
When tariff-aware charging is enabled, the tariff CSV must contain a
|
|
85
|
+
contiguous integer index and one numeric `price` or `price_eur_kwh` column.
|
|
86
|
+
|
|
87
|
+
## Configuration
|
|
88
|
+
|
|
89
|
+
The authoritative configuration template is available in the
|
|
90
|
+
[configuration documentation](https://pages.persee.minesparis.psl.eu/planeterr/mobev/configuration.html).
|
|
91
|
+
Repository users can copy [config/config.yaml](https://git.persee.minesparis.psl.eu/planeterr/mobev/-/blob/main/config/config.yaml)
|
|
92
|
+
when maintaining separate experiments, then pass the copy to the pipeline.
|
|
93
|
+
The repository template uses project-relative data paths. When using the
|
|
94
|
+
installed package, provide an absolute path for `tariff.file` (or disable the
|
|
95
|
+
tariff) and pass the trip-directory path explicitly.
|
|
96
|
+
|
|
97
|
+
The main sections are:
|
|
98
|
+
|
|
99
|
+
- `simulation`: timestep, start datetime, random seed, distance factor, CRS,
|
|
100
|
+
and seasonal consumption.
|
|
101
|
+
- `fleet`: EV penetration and fixed or segmented vehicle attributes.
|
|
102
|
+
- `battery`: usable SOC limits, charging efficiency, and initial SOC.
|
|
103
|
+
- `charging`: minimum parking duration and SOC thresholds.
|
|
104
|
+
- `tariff`: optional time-of-use charging and tariff CSV settings.
|
|
105
|
+
- `locations`: charger availability and charger-power distributions by
|
|
106
|
+
location type.
|
|
107
|
+
|
|
108
|
+
Edit the YAML values directly, for example:
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
simulation:
|
|
112
|
+
timestep_minutes: 15
|
|
113
|
+
random_seed: 42
|
|
114
|
+
|
|
115
|
+
fleet:
|
|
116
|
+
ev_penetration: 0.10
|
|
117
|
+
|
|
118
|
+
tariff:
|
|
119
|
+
enabled: false
|
|
120
|
+
file: path/to/tariff.csv
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Set `tariff.enabled` to `true` only when `tariff.file` points to a valid CSV.
|
|
124
|
+
The random seed makes fleet sampling, charger assignment, and initial SOC
|
|
125
|
+
reproducible for the same inputs and dependency versions.
|
|
126
|
+
|
|
127
|
+
## Run the complete pipeline
|
|
128
|
+
|
|
129
|
+
The main entry point is `SimulationPipeline`. It loads the configuration and
|
|
130
|
+
runs all three stages in order: trip preparation, charging resolution, and
|
|
131
|
+
load-profile simulation.
|
|
132
|
+
|
|
133
|
+
From Python:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from mobev.core.pipeline import SimulationPipeline
|
|
137
|
+
|
|
138
|
+
pipeline = SimulationPipeline.from_yaml("config/config.yaml")
|
|
139
|
+
result = pipeline.run("data/input/trips/YOUR_FEEDER")
|
|
140
|
+
|
|
141
|
+
print(result.load_timeseries_df)
|
|
142
|
+
print(result.tariff_curve_df) # empty when tariffs are disabled
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The checked-in runner can also be used after updating `config_file` and
|
|
146
|
+
`trip_folder` in [scripts/run_pipeline.py](https://git.persee.minesparis.psl.eu/planeterr/mobev/-/blob/main/scripts/run_pipeline.py):
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
python scripts/run_pipeline.py
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`EVSimulationResult` exposes all intermediate and final tables, including
|
|
153
|
+
trips, locations, vehicles, parking events, charging stations, charging
|
|
154
|
+
opportunities, SOC traces, charging sessions, load timeseries, and the tariff
|
|
155
|
+
curve.
|
|
156
|
+
|
|
157
|
+
## Run a decomposed pipeline
|
|
158
|
+
|
|
159
|
+
Run the stages separately when you need to inspect or persist intermediate
|
|
160
|
+
results, customize a stage, or integrate MobEV into a larger workflow:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from mobev.core.charging_resolver import ChargingResolver
|
|
164
|
+
from mobev.core.config import Config
|
|
165
|
+
from mobev.core.profile_simulator import ProfileSimulator
|
|
166
|
+
from mobev.core.trip_preparer import TripPreparer
|
|
167
|
+
|
|
168
|
+
config = Config.from_yaml("config/config.yaml")
|
|
169
|
+
trip_results = TripPreparer(config).run("data/input/trips/YOUR_FEEDER")
|
|
170
|
+
charging_results = ChargingResolver(config).run(trip_results)
|
|
171
|
+
profile_results = ProfileSimulator(config).run(trip_results, charging_results)
|
|
172
|
+
|
|
173
|
+
print(profile_results.load_timeseries_df)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The stage contracts are:
|
|
177
|
+
|
|
178
|
+
```text
|
|
179
|
+
TripPreparer -> TripResults
|
|
180
|
+
ChargingResolver -> ChargingResults
|
|
181
|
+
ProfileSimulator -> ProfileResults
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Testing
|
|
185
|
+
|
|
186
|
+
Run the test suite with:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
.venv/bin/pytest -q
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
## Further documentation
|
|
194
|
+
|
|
195
|
+
The [Sphinx documentation](https://pages.persee.minesparis.psl.eu/planeterr/mobev/) covers inputs,
|
|
196
|
+
configuration, workflow, tariffs, results, visualization, and the public API.
|
mobev-0.1.0/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# MobEV
|
|
2
|
+
|
|
3
|
+
[](https://www.python.org/)
|
|
4
|
+
[](https://pypi.org/project/mobev/)
|
|
5
|
+
[](https://docs.astral.sh/uv/)
|
|
6
|
+
[](https://git.persee.minesparis.psl.eu/planeterr/mobev/-/tree/main/tests)
|
|
7
|
+
[](https://pages.persee.minesparis.psl.eu/planeterr/mobev/)
|
|
8
|
+
|
|
9
|
+
MobEV is a mobility-based, geolocalized EV charging-demand model. It
|
|
10
|
+
generates EV charging-demand profiles for distribution-network studies from
|
|
11
|
+
weekly car-trip data. The model samples an EV fleet and charging access,
|
|
12
|
+
simulates battery state of charge and charging sessions, and aggregates the
|
|
13
|
+
resulting demand at the configured timestep.
|
|
14
|
+
|
|
15
|
+
Full reference documentation: **[https://pages.persee.minesparis.psl.eu/planeterr/mobev/](https://pages.persee.minesparis.psl.eu/planeterr/mobev/)**.
|
|
16
|
+
|
|
17
|
+
## Requirements and installation
|
|
18
|
+
|
|
19
|
+
MobEV requires Python 3.13 or newer.
|
|
20
|
+
|
|
21
|
+
Install the published package with pip:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
python -m pip install mobev
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or add it to an existing uv-managed project:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv add mobev
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
For development from a repository checkout, use the included lock file:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv sync
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
To build the documentation locally:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv sync --group docs
|
|
43
|
+
make -C docs html
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Inputs
|
|
47
|
+
|
|
48
|
+
Each simulation needs a trip directory containing:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
weekday_trips.gpkg
|
|
52
|
+
weekend_trips.gpkg
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The source data must provide person and activity identifiers, departure and
|
|
56
|
+
arrival times, activity purposes, car mode, and trip geometries. They are generated using a modified version of the EQASIM tool : <https://github.com/RatonMagique/eqasim-france/tree/feat/iris-commune-weekday-pipeline-improvments> . See
|
|
57
|
+
[input documentation](https://pages.persee.minesparis.psl.eu/planeterr/mobev/inputs.html) for the complete input
|
|
58
|
+
schema.
|
|
59
|
+
|
|
60
|
+
When tariff-aware charging is enabled, the tariff CSV must contain a
|
|
61
|
+
contiguous integer index and one numeric `price` or `price_eur_kwh` column.
|
|
62
|
+
|
|
63
|
+
## Configuration
|
|
64
|
+
|
|
65
|
+
The authoritative configuration template is available in the
|
|
66
|
+
[configuration documentation](https://pages.persee.minesparis.psl.eu/planeterr/mobev/configuration.html).
|
|
67
|
+
Repository users can copy [config/config.yaml](https://git.persee.minesparis.psl.eu/planeterr/mobev/-/blob/main/config/config.yaml)
|
|
68
|
+
when maintaining separate experiments, then pass the copy to the pipeline.
|
|
69
|
+
The repository template uses project-relative data paths. When using the
|
|
70
|
+
installed package, provide an absolute path for `tariff.file` (or disable the
|
|
71
|
+
tariff) and pass the trip-directory path explicitly.
|
|
72
|
+
|
|
73
|
+
The main sections are:
|
|
74
|
+
|
|
75
|
+
- `simulation`: timestep, start datetime, random seed, distance factor, CRS,
|
|
76
|
+
and seasonal consumption.
|
|
77
|
+
- `fleet`: EV penetration and fixed or segmented vehicle attributes.
|
|
78
|
+
- `battery`: usable SOC limits, charging efficiency, and initial SOC.
|
|
79
|
+
- `charging`: minimum parking duration and SOC thresholds.
|
|
80
|
+
- `tariff`: optional time-of-use charging and tariff CSV settings.
|
|
81
|
+
- `locations`: charger availability and charger-power distributions by
|
|
82
|
+
location type.
|
|
83
|
+
|
|
84
|
+
Edit the YAML values directly, for example:
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
simulation:
|
|
88
|
+
timestep_minutes: 15
|
|
89
|
+
random_seed: 42
|
|
90
|
+
|
|
91
|
+
fleet:
|
|
92
|
+
ev_penetration: 0.10
|
|
93
|
+
|
|
94
|
+
tariff:
|
|
95
|
+
enabled: false
|
|
96
|
+
file: path/to/tariff.csv
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Set `tariff.enabled` to `true` only when `tariff.file` points to a valid CSV.
|
|
100
|
+
The random seed makes fleet sampling, charger assignment, and initial SOC
|
|
101
|
+
reproducible for the same inputs and dependency versions.
|
|
102
|
+
|
|
103
|
+
## Run the complete pipeline
|
|
104
|
+
|
|
105
|
+
The main entry point is `SimulationPipeline`. It loads the configuration and
|
|
106
|
+
runs all three stages in order: trip preparation, charging resolution, and
|
|
107
|
+
load-profile simulation.
|
|
108
|
+
|
|
109
|
+
From Python:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from mobev.core.pipeline import SimulationPipeline
|
|
113
|
+
|
|
114
|
+
pipeline = SimulationPipeline.from_yaml("config/config.yaml")
|
|
115
|
+
result = pipeline.run("data/input/trips/YOUR_FEEDER")
|
|
116
|
+
|
|
117
|
+
print(result.load_timeseries_df)
|
|
118
|
+
print(result.tariff_curve_df) # empty when tariffs are disabled
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The checked-in runner can also be used after updating `config_file` and
|
|
122
|
+
`trip_folder` in [scripts/run_pipeline.py](https://git.persee.minesparis.psl.eu/planeterr/mobev/-/blob/main/scripts/run_pipeline.py):
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
python scripts/run_pipeline.py
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`EVSimulationResult` exposes all intermediate and final tables, including
|
|
129
|
+
trips, locations, vehicles, parking events, charging stations, charging
|
|
130
|
+
opportunities, SOC traces, charging sessions, load timeseries, and the tariff
|
|
131
|
+
curve.
|
|
132
|
+
|
|
133
|
+
## Run a decomposed pipeline
|
|
134
|
+
|
|
135
|
+
Run the stages separately when you need to inspect or persist intermediate
|
|
136
|
+
results, customize a stage, or integrate MobEV into a larger workflow:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from mobev.core.charging_resolver import ChargingResolver
|
|
140
|
+
from mobev.core.config import Config
|
|
141
|
+
from mobev.core.profile_simulator import ProfileSimulator
|
|
142
|
+
from mobev.core.trip_preparer import TripPreparer
|
|
143
|
+
|
|
144
|
+
config = Config.from_yaml("config/config.yaml")
|
|
145
|
+
trip_results = TripPreparer(config).run("data/input/trips/YOUR_FEEDER")
|
|
146
|
+
charging_results = ChargingResolver(config).run(trip_results)
|
|
147
|
+
profile_results = ProfileSimulator(config).run(trip_results, charging_results)
|
|
148
|
+
|
|
149
|
+
print(profile_results.load_timeseries_df)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The stage contracts are:
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
TripPreparer -> TripResults
|
|
156
|
+
ChargingResolver -> ChargingResults
|
|
157
|
+
ProfileSimulator -> ProfileResults
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Testing
|
|
161
|
+
|
|
162
|
+
Run the test suite with:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
.venv/bin/pytest -q
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
## Further documentation
|
|
170
|
+
|
|
171
|
+
The [Sphinx documentation](https://pages.persee.minesparis.psl.eu/planeterr/mobev/) covers inputs,
|
|
172
|
+
configuration, workflow, tariffs, results, visualization, and the public API.
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Provide support code for the MobEV pipeline."""
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Assign persistent charging stations and enrich parking opportunities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import polars as pl
|
|
9
|
+
|
|
10
|
+
from mobev.core.config import Config
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ChargingAvailabilityAssigner:
|
|
16
|
+
"""Assign persistent chargers to location and location-type pairs.
|
|
17
|
+
|
|
18
|
+
Inputs: the global configuration, deduplicated location-type pairs,
|
|
19
|
+
normalized parking events, and a random number generator.
|
|
20
|
+
|
|
21
|
+
Process: samples one charger assignment for every ``(location_id,
|
|
22
|
+
location_type)`` pair, then joins that persistent assignment to each
|
|
23
|
+
matching parking event and applies the minimum parking-duration rule.
|
|
24
|
+
|
|
25
|
+
Outputs: a charging-station table and a parking-event table enriched with
|
|
26
|
+
charger availability, chargeability flags, and charging power.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, config: Config):
|
|
30
|
+
"""Store the simulation configuration."""
|
|
31
|
+
self.config = config
|
|
32
|
+
|
|
33
|
+
def assign(
|
|
34
|
+
self,
|
|
35
|
+
location_types_df: pl.DataFrame,
|
|
36
|
+
parking_df: pl.DataFrame,
|
|
37
|
+
rng: np.random.Generator,
|
|
38
|
+
) -> tuple[pl.DataFrame, pl.DataFrame]:
|
|
39
|
+
"""Return persistent station assignments and enriched parking events.
|
|
40
|
+
|
|
41
|
+
RNG contract: availability is drawn once for every sorted unique
|
|
42
|
+
``(location_id, location_type)`` pair. Power is then drawn by sorted
|
|
43
|
+
location type for available stations only. A fixed seed and identical
|
|
44
|
+
location-type inventory therefore produce identical assignments.
|
|
45
|
+
"""
|
|
46
|
+
charging_station_df = self._sample_stations(location_types_df, rng)
|
|
47
|
+
minimum_parking_s = self.config.minimum_parking_time_minutes * 60.0
|
|
48
|
+
|
|
49
|
+
if parking_df.is_empty():
|
|
50
|
+
logger.debug(
|
|
51
|
+
"Assigned %s charging stations. No parking events.",
|
|
52
|
+
charging_station_df.height,
|
|
53
|
+
)
|
|
54
|
+
return charging_station_df, parking_df.with_columns(
|
|
55
|
+
[
|
|
56
|
+
pl.lit(None, dtype=pl.Boolean).alias("charger_available"),
|
|
57
|
+
pl.lit(None, dtype=pl.Float64).alias("charging_power_kw"),
|
|
58
|
+
pl.lit(None, dtype=pl.Boolean).alias("is_chargeable"),
|
|
59
|
+
]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
opportunities_df = parking_df.join(
|
|
63
|
+
charging_station_df,
|
|
64
|
+
on=["location_id", "location_type"],
|
|
65
|
+
how="left",
|
|
66
|
+
).with_columns(
|
|
67
|
+
(
|
|
68
|
+
(pl.col("parking_duration_s") >= minimum_parking_s)
|
|
69
|
+
& pl.col("charger_available")
|
|
70
|
+
).alias("is_chargeable")
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
logger.debug(
|
|
74
|
+
"Assigned %s charging stations to %s unique locations. Charging power distribution: %s.",
|
|
75
|
+
int(charging_station_df.get_column("charger_available").sum() or 0),
|
|
76
|
+
charging_station_df.height,
|
|
77
|
+
charging_station_df.group_by("charging_power_kw")
|
|
78
|
+
.len()
|
|
79
|
+
.rename({"len": "count"})
|
|
80
|
+
.sort("charging_power_kw"),
|
|
81
|
+
)
|
|
82
|
+
logger.debug(
|
|
83
|
+
"Across %s parking event(s), %s are chargeable.",
|
|
84
|
+
opportunities_df.height,
|
|
85
|
+
int(opportunities_df.get_column("is_chargeable").sum() or 0),
|
|
86
|
+
)
|
|
87
|
+
return charging_station_df, opportunities_df
|
|
88
|
+
|
|
89
|
+
def _sample_stations(
|
|
90
|
+
self,
|
|
91
|
+
location_types_df: pl.DataFrame,
|
|
92
|
+
rng: np.random.Generator,
|
|
93
|
+
) -> pl.DataFrame:
|
|
94
|
+
"""Sample one persistent charger for every location and type pair."""
|
|
95
|
+
required_columns = {"location_id", "location_type"}
|
|
96
|
+
missing_columns = required_columns.difference(location_types_df.columns)
|
|
97
|
+
if missing_columns:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"Location-type inventory is missing columns: {sorted(missing_columns)}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
stations_df = (
|
|
103
|
+
location_types_df.select(["location_id", "location_type"])
|
|
104
|
+
.with_columns(
|
|
105
|
+
[
|
|
106
|
+
pl.col("location_id").cast(pl.String),
|
|
107
|
+
pl.col("location_type").cast(pl.String),
|
|
108
|
+
]
|
|
109
|
+
)
|
|
110
|
+
.unique()
|
|
111
|
+
.sort(["location_id", "location_type"])
|
|
112
|
+
)
|
|
113
|
+
if stations_df.is_empty():
|
|
114
|
+
return pl.DataFrame(
|
|
115
|
+
schema={
|
|
116
|
+
"location_id": pl.String,
|
|
117
|
+
"location_type": pl.String,
|
|
118
|
+
"charger_available": pl.Boolean,
|
|
119
|
+
"charging_power_kw": pl.Float64,
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
station_rows = stations_df.to_dicts()
|
|
124
|
+
availability_draws = rng.random(len(station_rows))
|
|
125
|
+
charger_available = np.zeros(len(station_rows), dtype=bool)
|
|
126
|
+
charging_power_kw = np.zeros(len(station_rows), dtype=float)
|
|
127
|
+
location_types = sorted({str(row["location_type"]) for row in station_rows})
|
|
128
|
+
|
|
129
|
+
for index, station in enumerate(station_rows):
|
|
130
|
+
probability = self.config.get_location_config(
|
|
131
|
+
str(station["location_type"])
|
|
132
|
+
)["charger_availability_probability"]
|
|
133
|
+
charger_available[index] = availability_draws[index] <= probability
|
|
134
|
+
|
|
135
|
+
for location_type in location_types:
|
|
136
|
+
available_indices = np.array(
|
|
137
|
+
[
|
|
138
|
+
index
|
|
139
|
+
for index, station in enumerate(station_rows)
|
|
140
|
+
if station["location_type"] == location_type
|
|
141
|
+
and charger_available[index]
|
|
142
|
+
],
|
|
143
|
+
dtype=int,
|
|
144
|
+
)
|
|
145
|
+
if available_indices.size == 0:
|
|
146
|
+
continue
|
|
147
|
+
distribution = self.config.get_location_config(location_type)[
|
|
148
|
+
"charging_power_distribution"
|
|
149
|
+
]
|
|
150
|
+
charging_power_kw[available_indices] = self._sample_power_distribution(
|
|
151
|
+
distribution,
|
|
152
|
+
available_indices.size,
|
|
153
|
+
rng,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return stations_df.with_columns(
|
|
157
|
+
[
|
|
158
|
+
pl.Series("charger_available", charger_available),
|
|
159
|
+
pl.Series("charging_power_kw", charging_power_kw),
|
|
160
|
+
]
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def _sample_power_distribution(
|
|
165
|
+
distribution: dict[str, object],
|
|
166
|
+
size: int,
|
|
167
|
+
rng: np.random.Generator,
|
|
168
|
+
) -> np.ndarray:
|
|
169
|
+
"""Draw ``size`` charging powers for one configured location type."""
|
|
170
|
+
distribution_type = distribution["type"]
|
|
171
|
+
|
|
172
|
+
if distribution_type == "fixed":
|
|
173
|
+
return np.full(size, float(distribution["value"]), dtype=float)
|
|
174
|
+
|
|
175
|
+
if distribution_type == "categorical":
|
|
176
|
+
values = np.asarray(distribution["values"], dtype=float)
|
|
177
|
+
weights = np.asarray(distribution["weights"], dtype=float)
|
|
178
|
+
return rng.choice(values, size=size, p=weights / weights.sum())
|
|
179
|
+
|
|
180
|
+
if distribution_type == "uniform":
|
|
181
|
+
return rng.uniform(
|
|
182
|
+
float(distribution["low"]),
|
|
183
|
+
float(distribution["high"]),
|
|
184
|
+
size=size,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
raise ValueError(f"Unsupported distribution type: {distribution_type!r}")
|