From 94dcf4fa7d686379da3f7c05e312efd7e8667a6c Mon Sep 17 00:00:00 2001 From: mariegrandclement Date: Mon, 1 Dec 2025 16:05:44 +0100 Subject: [PATCH 1/3] =?UTF-8?q?US=20198=20-=20Travail=20en=20cours=20sur?= =?UTF-8?q?=20les=20param=C3=A8tres=20d'entr=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/quickstart-fr.py.py | 3 +++ mobility/choice_models/population_trips.py | 24 ++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/examples/quickstart-fr.py.py b/examples/quickstart-fr.py.py index ebc32e82..41bcdecd 100644 --- a/examples/quickstart-fr.py.py +++ b/examples/quickstart-fr.py.py @@ -38,3 +38,6 @@ # You can also plot the flows, with labels for the cities that are bigger than their neighbours labels = pop_trips.get_prominent_cities() pop_trips.plot_od_flows(labels=labels) + +rapport = pop_trips.parameters_dict() +print(rapport) diff --git a/mobility/choice_models/population_trips.py b/mobility/choice_models/population_trips.py index 761c7bb5..b7a5617c 100644 --- a/mobility/choice_models/population_trips.py +++ b/mobility/choice_models/population_trips.py @@ -4,6 +4,7 @@ import shutil import random import warnings +import pandas as pd import geopandas as gpd import matplotlib.pyplot as plt @@ -605,8 +606,10 @@ def plot_od_flows(self, mode="all", motive="all", period="weekdays", level_of_de # Put a legend for width on bottom right, title on the top x_min = float(biggest_flows[["x"]].min().iloc[0]) y_min = float(biggest_flows[["y"]].min().iloc[0]) - plt.plot([x_min, x_min+4000], [y_min, y_min], linewidth=2, color=color) - plt.text(x_min+6000, y_min-1000, "1 000", color=color) + plt.plot([x_min-6000, x_min-4000], [y_min, y_min], linewidth=2, color=color) + plt.text(x_min-2000, y_min-200, "1 000", color=color) + plt.text(x_min-6000, y_min-2000, f"hash: {self.inputs_hash}", fontsize=7, color=color) + plt.text(x_min-6000, y_min-4000,self.parameters_dict(),fontsize=7, color=color) plt.title(f"{mode_name} flows between transport zones on {period}") # Draw all origin-destinations @@ -691,3 +694,20 @@ def get_prominent_cities(self, n_cities=20, n_levels=3, distance_km=2): geoflows = geoflows.merge(xy_coords, left_index=True, right_index=True) return geoflows + + def parameters_dict(self) : + params = { + #study area radius + "radius": self.population.transport_zones.inner_radius, + #id number of administrative unit + "local_admin_unit_id": self.population.transport_zones.study_area.local_admin_unit_id, + #sample size of population surveyed + "population_sample_size": self.population.sample_size, + #referenced of survey used for parsing + "survey_used": [s.survey_name for s in self.surveys], + #liste des local admin units + "nombre_local_admin_units": len(self.population.transport_zones.study_area.get()), + #hash path# + "inputs_hash" : self.inputs_hash + } + return pd.DataFrame([params]) From a69cd58961e57404b1470b00c84992365c6b39b6 Mon Sep 17 00:00:00 2001 From: mariegrandclement Date: Mon, 8 Dec 2025 12:06:14 +0100 Subject: [PATCH 2/3] Update parameters_dict MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fonction parameters_dict mise à jour pour séparer les paramètres généraux, liés aux modes, et liés aux motifs --- examples/quickstart-fr.py.py | 2 +- mobility/choice_models/population_trips.py | 41 +++++++++++++++------- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/examples/quickstart-fr.py.py b/examples/quickstart-fr.py.py index 41bcdecd..ac35ebc0 100644 --- a/examples/quickstart-fr.py.py +++ b/examples/quickstart-fr.py.py @@ -40,4 +40,4 @@ pop_trips.plot_od_flows(labels=labels) rapport = pop_trips.parameters_dict() -print(rapport) +print(rapport.T) diff --git a/mobility/choice_models/population_trips.py b/mobility/choice_models/population_trips.py index 7ac5d455..c28c714e 100644 --- a/mobility/choice_models/population_trips.py +++ b/mobility/choice_models/population_trips.py @@ -689,7 +689,6 @@ def plot_od_flows(self, mode="all", motive="all", period="weekdays", level_of_de plt.plot([x_min-6000, x_min-4000], [y_min, y_min], linewidth=2, color=color) plt.text(x_min-2000, y_min-200, "1 000", color=color) plt.text(x_min-6000, y_min-2000, f"hash: {self.inputs_hash}", fontsize=7, color=color) - plt.text(x_min-6000, y_min-4000,self.parameters_dict(),fontsize=7, color=color) plt.title(f"{mode_name} flows between transport zones on {period}") # Draw all origin-destinations @@ -720,7 +719,7 @@ def get_prominent_cities(self, n_cities=20, n_levels=3, distance_km=2): """ Get the most prominent cities, ie the biggest cities that are not close to a bigger city. - Useful to label a map and reducing the number of overlaps without mising an important city. + Useful to label a map and reducing the number of overlaps without missing an important city. Parameters ---------- @@ -776,18 +775,36 @@ def get_prominent_cities(self, n_cities=20, n_levels=3, distance_km=2): return geoflows def parameters_dict(self) : - params = { - #study area radius - "radius": self.population.transport_zones.inner_radius, - #id number of administrative unit + params_general = { + "inner_radius": self.population.transport_zones.inner_radius, "local_admin_unit_id": self.population.transport_zones.study_area.local_admin_unit_id, - #sample size of population surveyed + "level_of_detail" : self.population.transport_zones.level_of_detail, + "nb_local_admin_units": len(self.population.transport_zones.study_area.get()), + "osm_geofabrik_extract_date": self.population.transport_zones.osm_buildings.geofabrik_extract_date, "population_sample_size": self.population.sample_size, - #referenced of survey used for parsing "survey_used": [s.survey_name for s in self.surveys], - #liste des local admin units - "nombre_local_admin_units": len(self.population.transport_zones.study_area.get()), - #hash path# "inputs_hash" : self.inputs_hash - } + } + params_modes = { + key: value + for i, m in enumerate(self.modes, start=1) + for key, value in [ + (f"mode_{i}", m.name), + (f"mode_{i}_filter_max_time",m.travel_costs.routing_parameters.filter_max_time), + (f"mode_{i}_filter_max_speed",m.travel_costs.routing_parameters.filter_max_speed), + (f"mode_{i}_cost_constant",m.generalized_cost.parameters.cost_constant), + (f"mode_{i}_cost_of_distance",m.generalized_cost.parameters.cost_of_distance), + (f"mode_{i}_cost_of_time_intercept",m.generalized_cost.parameters.cost_of_time.intercept) #à voir ce qu'on veut connaitre + ] + } + params_motives = { + key: value + for i, m in enumerate(self.motives, start=1) + for key, value in [ + (f"motive_{i}", m.name), + (f"motive_{i}_value_of_time",m.value_of_time), + (f"motive_{i}_value_of_time_v2",m.value_of_time_v2) + ] + } + params = params_general | params_modes | params_motives return pd.DataFrame([params]) From e95dc4a2936492e3373999ae815411bd9bcba4d3 Mon Sep 17 00:00:00 2001 From: mariegrandclement Date: Wed, 18 Feb 2026 10:01:32 +0100 Subject: [PATCH 3/3] New approach using class Parameter and function get_parameters Created class Parameter (using model_parameters.py made by Capucine) Created function get_parameters in class FileAsset, with a recursive attribute, in order to get all unique parameters of the object (if recursive=false) or the parameters of the object and all upper objects (if recursive=true). Modified function compute_inputs_hash from class Asset in order to manage the fact that some inputs are now Parameters objects Modified class StudyArea in order to define radius as a Parameter Modified class TransportZones in order to define level_of_detail as a Parameter Tested transport_zones.get_parameters() in quickstart --- examples/quickstart-fr.py.py | 9 ++- mobility/asset.py | 2 +- mobility/file_asset.py | 24 +++++++- mobility/model_parameters.py | 114 +++++++++++++++++++++++++++++++++++ mobility/study_area.py | 17 +++++- mobility/transport_zones.py | 15 ++++- 6 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 mobility/model_parameters.py diff --git a/examples/quickstart-fr.py.py b/examples/quickstart-fr.py.py index ac35ebc0..1596e527 100644 --- a/examples/quickstart-fr.py.py +++ b/examples/quickstart-fr.py.py @@ -39,5 +39,10 @@ labels = pop_trips.get_prominent_cities() pop_trips.plot_od_flows(labels=labels) -rapport = pop_trips.parameters_dict() -print(rapport.T) +# You can print a report of all parameters used in the model +#report = pop_trips.parameters_dict() +#print(report.T) + +tz_params = transport_zones.get_parameters() +for p in tz_params: + print(p.to_dict()) \ No newline at end of file diff --git a/mobility/asset.py b/mobility/asset.py index e72a7a05..4308c030 100644 --- a/mobility/asset.py +++ b/mobility/asset.py @@ -82,7 +82,7 @@ def serialize(value): return value hashable_inputs = {k: serialize(v) for k, v in self.inputs.items()} - serialized_inputs = json.dumps(hashable_inputs, sort_keys=True).encode('utf-8') + serialized_inputs = json.dumps(hashable_inputs, sort_keys=True,default=lambda o: o.to_dict() if hasattr(o, 'to_dict') else str(o)).encode('utf-8') return hashlib.md5(serialized_inputs).hexdigest() diff --git a/mobility/file_asset.py b/mobility/file_asset.py index 14ac33a0..e4be2df1 100644 --- a/mobility/file_asset.py +++ b/mobility/file_asset.py @@ -3,7 +3,8 @@ import networkx as nx from mobility.asset import Asset -from typing import Any +from mobility.model_parameters import Parameter +from typing import Any, List from abc import abstractmethod class FileAsset(Asset): @@ -206,4 +207,25 @@ def remove(self): path = pathlib.Path(self.cache_path) if path.exists(): path.unlink() + + def get_parameters(self, recursive: bool = True) -> List[Parameter]: + + params = [] + + for inp in self.inputs.values(): + if isinstance(inp, Parameter) : + params.append(inp) + + if recursive: + for inp in self.inputs.values(): + if isinstance(inp, FileAsset): + params.extend(inp.get_parameters(recursive=True)) + + unique_params = {} + for p in params: + if p.name not in unique_params: + unique_params[p.name] = p + + return list(unique_params.values()) + diff --git a/mobility/model_parameters.py b/mobility/model_parameters.py new file mode 100644 index 00000000..57611a6e --- /dev/null +++ b/mobility/model_parameters.py @@ -0,0 +1,114 @@ +from dataclasses import dataclass, field, fields, asdict +from typing import List, Union + +Number = Union[int, float] + +@dataclass(frozen=True) +class Parameter: + name: str + name_fr: str + value: Number | bool + description: str + parameter_type: type + default_value: Number | bool + possible_values: List[float] | List[int] | tuple = None + min_value: Number = None + max_value: Number = None + unit: str = None + interval: Number = None + source_default: str = "" + parameter_role: str = "" + + def to_dict(self): + # Convert the parameter to a dictionary with serializable values + return { + "name": self.name, + "name_fr": self.name_fr, + "value": self.value, + "description": self.description, + "parameter_type": str(self.parameter_type), # Convert type to string + "possible_values": self.possible_values, + "min_value": self.min_value, + "max_value": self.max_value, + "unit": self.unit, + "interval": self.interval, + "source_default": self.source_default, + "parameter_role": self.parameter_role, + } + + # def get(self): + # """Return parameter value.""" + # val = self.default_value + # self._validate(val) + # return val + + def set(self, new_value): + self.value = new_value + + def validate(self): + if self.value is None: #todo: improve this! + return None + if self.parameter_type is not None: + if not isinstance(self.value, self.parameter_type): + t = type(self.value) + raise TypeError(f"Parameter '{self.name}' must be {self.parameter_type} (currently {t}).") + + if self.min_value is not None and self.value < self.min_value: + raise ValueError( + f"Parameter '{self.name}' below minimum {self.min_value}" + ) + + if self.max_value is not None and self.value > self.max_value: + raise ValueError( + f"Parameter '{self.name}' above maximum {self.max_value}" + ) + + def __repr__(self): + unit_str = f" [{self.unit}]" if self.unit else "" + return f"" + + def get_values_for_sensitivity_analyses(self, i_max=10): + value = self.value + values = [value] + if self.interval is None: + raise ValueError("To run a sensitivity analysis, interval must be specified in the parameter configuration") + i = 0 + if self.max_value is not None: + while i < i_max and value < self.max_value: + value += self.interval + values.append(round(value,3)) + i += 1 + else: + while i < i_max: + value += self.interval + values.append(round(value,3)) + i += 1 + value = self.value + i = 0 + + if self.min_value is not None: + while i < i_max and value > self.min_value: + value -= self.interval + values.append(round(value,3)) + i += 1 + else: + while i < i_max: + value -= self.interval + values.append(round(value,3)) + i += 1 + + values.sort() + return values + +@dataclass +class ParameterSet: + parameters : dict = field(init=False, compare=False) + + def validate(self): + for param in fields(self)[1:]: + param_name = "param_" + param.name + self.parameters[param_name].validate() + self._validate_param_interdependency() + + def _validate_param_interdependency(self): + pass diff --git a/mobility/study_area.py b/mobility/study_area.py index a54a61f1..f60c82dc 100644 --- a/mobility/study_area.py +++ b/mobility/study_area.py @@ -7,6 +7,7 @@ from typing import Union, List from mobility.file_asset import FileAsset +from mobility.model_parameters import Parameter from mobility.parsers.local_admin_units import LocalAdminUnits @@ -38,11 +39,23 @@ def __init__( cutout_geometries: gpd.GeoDataFrame = None ): + radius_param = Parameter( + name="radius", + name_fr="rayon", + value=radius, + description="radius", + default_value=20.0, + parameter_type=float | int, + min_value=5.0, + max_value=100.0, + unit="km" + ) + inputs = { "version": "1", "local_admin_units": LocalAdminUnits(), "local_admin_unit_id": local_admin_unit_id, - "radius": radius, + "radius": radius_param, "cutout_geometries": cutout_geometries } @@ -91,7 +104,7 @@ def create_and_get_asset(self) -> gpd.GeoDataFrame: local_admin_units = self.filter_within_radius( local_admin_units, local_admin_unit_id, - self.inputs["radius"] + self.inputs["radius"].value ) else: diff --git a/mobility/transport_zones.py b/mobility/transport_zones.py index 5825d5fd..738098b9 100644 --- a/mobility/transport_zones.py +++ b/mobility/transport_zones.py @@ -10,6 +10,7 @@ from shapely.geometry import Point from mobility.file_asset import FileAsset +from mobility.model_parameters import Parameter from mobility.study_area import StudyArea from mobility.parsers.osm import OSMData from mobility.r_utils.r_script import RScript @@ -61,6 +62,16 @@ def __init__( cutout_geometries: gpd.GeoDataFrame = None ): + level_of_detail_param = Parameter( + name="level of detail", + name_fr="niveau de détail", + value=level_of_detail, + description="radius", + default_value=0, + possible_values = [0, 1], + parameter_type=bool + ) + # If the user does not choose an inner radius or a list of inner # transport zones, we suppose that there is no inner / outer zones # (= all zones are inner zones) @@ -83,7 +94,7 @@ def __init__( inputs = { "version": "1", "study_area": study_area, - "level_of_detail": level_of_detail, + "level_of_detail": level_of_detail_param, "osm_buildings": osm_buildings, "inner_radius": inner_radius, "inner_local_admin_unit_id": inner_local_admin_unit_id, @@ -138,7 +149,7 @@ def create_and_get_asset(self) -> gpd.GeoDataFrame: args=[ study_area_fp, osm_buildings_fp, - str(self.level_of_detail), + str(self.inputs["level_of_detail"].value), self.cache_path ] )