Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/drag coeff func #94

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions python/altrios/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

LOCO_LIFESPAN = 20
ANNUAL_LOCO_TURNOVER = 1.0/LOCO_LIFESPAN
DEFAULT_GAP_SIZE = 0.604

DEMAND_FILE = alt.resources_root() / "Default Demand.csv"
FUEL_EMISSIONS_FILE = alt.resources_root() / "metrics_inputs" / "GREET-CA_Emissions_Factors.csv"
Expand Down
4 changes: 2 additions & 2 deletions python/altrios/demos/rollout_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
File = defaults.DEMAND_FILE
#targets = [0,0.05,0.1,0.15,0.2,0.25,0.3,0.35,0.4,0.45,0.5,0.55,0.6,0.65,0.7,0.75, 0.8]
train_planner_config = train_planner.TrainPlannerConfig(
cars_per_locomotive=50,
target_cars_per_train=90)
cars_per_locomotive={"Default": 50},
target_cars_per_train={"Default": 90})
targets = [0.5]
for target in targets:
scenario_infos, metrics = rollout.simulate_prescribed_rollout(
Expand Down
4 changes: 2 additions & 2 deletions python/altrios/demos/sim_manager_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
)

train_planner_config = train_planner.TrainPlannerConfig(
cars_per_locomotive=50,
target_cars_per_train=90)
cars_per_locomotive={"Default": 50},
target_cars_per_train={"Default": 90})

t0_main = time.perf_counter()

Expand Down
211 changes: 117 additions & 94 deletions python/altrios/train_planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,100 +6,102 @@
import polars as pl
import polars.selectors as cs
import math
from typing import Tuple, List, Dict
from typing import Tuple, List, Dict, Callable, Optional
from itertools import repeat
from dataclasses import dataclass, field
import altrios as alt
from altrios import defaults, utilities

pl.enable_string_cache()

@dataclass
class TrainPlannerConfig:
def __init__(self,
single_train_mode: bool = False,
min_cars_per_train: int = 60,
target_cars_per_train: int = 180,
manifest_empty_return_ratio: float = 0.6,
#TODO single vs double stacked operations on the corridor
cars_per_locomotive: int = 70,
refuelers_per_incoming_corridor: int = 4,
drag_coeff_function: List = None,
hp_required_per_ton: Dict = {
"Default": {
"Unit": 2.0,
"Manifest": 1.5,
"Intermodal": 2.0 + 2.0,
"Unit_Empty": 2.0,
"Manifest_Empty": 1.5,
"Intermodal_Empty": 2.0 + 2.0,
}
},
dispatch_scaling_dict: Dict = {
"time_mult_factor": 1.4,
"hours_add": 2,
"energy_mult_factor": 1.25
},
loco_info = pd.DataFrame({
"Diesel_Large": {
"Capacity_Cars": 20,
"Fuel_Type": "Diesel",
"Min_Servicing_Time_Hr": 3.0,
"Rust_Loco": alt.Locomotive.default(),
"Cost_USD": defaults.DIESEL_LOCO_COST_USD,
"Lifespan_Years": defaults.LOCO_LIFESPAN
},
"BEL": {
"Capacity_Cars": 20,
"Fuel_Type": "Electricity",
"Min_Servicing_Time_Hr": 3.0,
"Rust_Loco": alt.Locomotive.default_battery_electric_loco(),
"Cost_USD": defaults.BEL_MINUS_BATTERY_COST_USD,
"Lifespan_Years": defaults.LOCO_LIFESPAN
}
}).transpose().reset_index(names='Locomotive_Type'),
refueler_info = pd.DataFrame({
"Diesel_Fueler": {
"Locomotive_Type": "Diesel_Large",
"Fuel_Type": "Diesel",
"Refueler_J_Per_Hr": defaults.DIESEL_REFUEL_RATE_J_PER_HR,
"Refueler_Efficiency": defaults.DIESEL_REFUELER_EFFICIENCY,
"Cost_USD": defaults.DIESEL_REFUELER_COST_USD,
"Lifespan_Years": defaults.LOCO_LIFESPAN
},
"BEL_Charger": {
"Locomotive_Type": "BEL",
"Fuel_Type": "Electricity",
"Refueler_J_Per_Hr": defaults.BEL_CHARGE_RATE_J_PER_HR,
"Refueler_Efficiency": defaults.BEL_CHARGER_EFFICIENCY,
"Cost_USD": defaults.BEL_CHARGER_COST_USD,
"Lifespan_Years": defaults.LOCO_LIFESPAN
}
}).transpose().reset_index(names='Refueler_Type')
):
"""
Constructor for train planner configuration objects
Arguments:
----------
min_cars_per_train: the minimum length in number of cars to form a train
target_cars_per_train: the standard train length in number of cars
manifest_empty_return_ratio: Desired railcar reuse ratio to calculate the empty manifest car demand, (E_ij+E_ji)/(L_ij+L_ji)
cars_per_locomotive: Heuristic scaling factor used to size number of locomotives needed based on demand.
refuelers_per_incoming_corridor:
hp_required_per_ton:
dispatch_scaling_dict:
loco_info:
refueler_info:
"""
self.single_train_mode = single_train_mode
self.min_cars_per_train = min_cars_per_train
self.target_cars_per_train = target_cars_per_train
self.manifest_empty_return_ratio = manifest_empty_return_ratio
self.cars_per_locomotive = cars_per_locomotive
self.refuelers_per_incoming_corridor = refuelers_per_incoming_corridor
self.hp_required_per_ton = hp_required_per_ton
self.dispatch_scaling_dict = dispatch_scaling_dict
self.loco_info = loco_info
self.refueler_info = refueler_info
self.drag_coeff_function = drag_coeff_function
"""
Dataclass class for train planner configuration parameters.

Attributes:
----------
- `single_train_mode`: `True` to only run one round-trip train and schedule its charging; `False` to plan train consists
- `min_cars_per_train`: `Dict` of the minimum length in number of cars to form a train for each train type
- `target_cars_per_train`: `Dict` of the standard train length in number of cars for each train type
- `manifest_empty_return_ratio`: Desired railcar reuse ratio to calculate the empty manifest car demand, (E_ij+E_ji)/(L_ij+L_ji)
- `cars_per_locomotive`: Heuristic scaling factor used to size number of locomotives needed based on demand.
- `refuelers_per_incoming_corridor`: Heuristic scaling factor used to scale number of refuelers needed at each node based on number of incoming corridors.
- `stack_type`: Type of stacking (applicable only for intermodal containers)
- `require_diesel`: `True` to require each consist to have at least one diesel locomotive.
- `manifest_empty_return_ratio`: `Dict`
- `drag_coeff_function`: `Dict`
- `hp_required_per_ton`: `Dict`
- `dispatch_scaling_dict`: `Dict`
- `loco_info`: `Dict`
- `refueler_info`: `Dict`
- `return_demand_generators`: `Dict`
"""
single_train_mode: bool = False
min_cars_per_train: Dict = field(default_factory = lambda: {
"Default": 60
})
target_cars_per_train: Dict = field(default_factory = lambda: {
"Default": 180
})
cars_per_locomotive: Dict = field(default_factory = lambda: {
"Default": 50
})
refuelers_per_incoming_corridor: int = 4
require_diesel: bool = False
manifest_empty_return_ratio: float = 0.6
drag_coeff_function: Callable = None
hp_required_per_ton: Dict = field(default_factory = lambda: {
"Default": {
"Unit": 2.0,
"Manifest": 1.5,
"Intermodal": 2.0 + 2.0,
"Unit_Empty": 2.0,
"Manifest_Empty": 1.5,
"Intermodal_Empty": 2.0 + 2.0,
}
})
dispatch_scaling_dict: Dict = field(default_factory = lambda: {
"time_mult_factor": 1.4,
"hours_add": 2,
"energy_mult_factor": 1.25
})
loco_info: pd.DataFrame = field(default_factory = lambda: pd.DataFrame({
"Diesel_Large": {
"Capacity_Cars": 20,
"Fuel_Type": "Diesel",
"Min_Servicing_Time_Hr": 3.0,
"Rust_Loco": alt.Locomotive.default(),
"Cost_USD": defaults.DIESEL_LOCO_COST_USD,
"Lifespan_Years": defaults.LOCO_LIFESPAN
},
"BEL": {
"Capacity_Cars": 20,
"Fuel_Type": "Electricity",
"Min_Servicing_Time_Hr": 3.0,
"Rust_Loco": alt.Locomotive.default_battery_electric_loco(),
"Cost_USD": defaults.BEL_MINUS_BATTERY_COST_USD,
"Lifespan_Years": defaults.LOCO_LIFESPAN
}
}).transpose().reset_index(names='Locomotive_Type'))
refueler_info: pd.DataFrame = field(default_factory = lambda: pd.DataFrame({
"Diesel_Fueler": {
"Locomotive_Type": "Diesel_Large",
"Fuel_Type": "Diesel",
"Refueler_J_Per_Hr": defaults.DIESEL_REFUEL_RATE_J_PER_HR,
"Refueler_Efficiency": defaults.DIESEL_REFUELER_EFFICIENCY,
"Cost_USD": defaults.DIESEL_REFUELER_COST_USD,
"Lifespan_Years": defaults.LOCO_LIFESPAN
},
"BEL_Charger": {
"Locomotive_Type": "BEL",
"Fuel_Type": "Electricity",
"Refueler_J_Per_Hr": defaults.BEL_CHARGE_RATE_J_PER_HR,
"Refueler_Efficiency": defaults.BEL_CHARGER_EFFICIENCY,
"Cost_USD": defaults.BEL_CHARGER_COST_USD,
"Lifespan_Years": defaults.LOCO_LIFESPAN
}
}).transpose().reset_index(names='Refueler_Type'))

def demand_loader(
demand_table: Union[pl.DataFrame, Path, str]
Expand Down Expand Up @@ -287,7 +289,20 @@ def generate_demand_trains(
----------
demand: Tabulated demand for each demand pair in terms of number of cars and number of trains
"""

cars_per_train_min = (pl.from_dict(config.min_cars_per_train)
.melt(variable_name="Train_Type", value_name="Cars_Per_Train_Min")
)
cars_per_train_min_default = (cars_per_train_min
.filter(pl.col("Train_Type") == pl.lit("Default"))
.select("Cars_Per_Train_Min").item()
)
cars_per_train_target = (pl.from_dict(config.target_cars_per_train)
.melt(variable_name="Train_Type", value_name="Cars_Per_Train_Target")
)
cars_per_train_target_default = (cars_per_train_target
.filter(pl.col("Train_Type") == pl.lit("Default"))
.select("Cars_Per_Train_Target").item()
)
demand = pl.concat([
demand.drop("Number_of_Containers"),
demand_returns.drop("Number_of_Containers"),
Expand Down Expand Up @@ -350,7 +365,7 @@ def get_kg(veh):
.then(0)
.otherwise(
pl.max_horizontal([1,
((pl.col("Number_of_Cars").floordiv(pl.lit(config.target_cars_per_train)) + 1))
((pl.col("Number_of_Cars").floordiv(pl.lit(cars_per_train_target_default)) + 1))
])
).cast(pl.UInt32).alias("Number_of_Trains"))
# Calculate per-train car counts and tonnage
Expand Down Expand Up @@ -432,11 +447,11 @@ def build_locopool(
num_ods = demand.height
cars_per_od = demand.get_column("Number_of_Cars").mean()
if config.single_train_mode:
initial_size = math.ceil(cars_per_od / config.cars_per_locomotive)
initial_size = math.ceil(cars_per_od / config.cars_per_locomotive["Default"])
rows = initial_size
else:
num_destinations_per_node = num_ods*1.0 / num_nodes*1.0
initial_size = math.ceil((cars_per_od / config.cars_per_locomotive) *
initial_size = math.ceil((cars_per_od / config.cars_per_locomotive["Default"]) *
num_destinations_per_node) # number of locomotives per node
rows = initial_size * num_nodes # number of locomotives in total

Expand Down Expand Up @@ -927,10 +942,11 @@ def run_train_planner(
dispatch_times = calculate_dispatch_times(demand, simulation_days * 24)

#TODO eliminate the naming convention that rail vehicles (train types from demand file) must end in `_Loaded` or `_Empty`
#TODO: Make 'Loaded' and 'Empty' suffix manipulation case independent
dispatch_times = (dispatch_times.with_columns(
pl.when(pl.col("Train_Type").str.ends_with("_Empty"))
pl.when(pl.col("Train_Type").str.to_lowercase().str.ends_with("_empty"))
.then(pl.col("Train_Type"))
.otherwise(pl.concat_str(pl.col("Train_Type").str.strip_suffix("_Loaded"),
.otherwise(pl.concat_str(pl.col("Train_Type").str.strip_suffix("_loaded"),
pl.lit("_Loaded")))
.alias("Train_Type")
)
Expand Down Expand Up @@ -983,13 +999,20 @@ def run_train_planner(
)
dispatched = loco_pool.filter(selected)

if config.drag_coeff_function is not None:
cd_area_vec = config.drag_coeff_function(
this_train['Number_of_Cars'],
gap_size = defaults.DEFAULT_GAP_SIZE
)
else:
cd_area_vec = None
train_config = alt.TrainConfig(
rail_vehicles = [vehicle for vehicle in rail_vehicles if vehicle.car_type==this_train['Train_Type']],
n_cars_by_type = {
this_train['Train_Type']: this_train['Number_of_Cars']
},
train_type = train_type,
cd_area_vec = config.drag_coeff_function
cd_area_vec = cd_area_vec,
)

loco_start_soc_j = dispatched.get_column("SOC_J")
Expand Down
3 changes: 2 additions & 1 deletion rust/altrios-core/src/train/train_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ pub struct TrainConfig {
/// Optional vector of drag areas (i.e. drag coeff. times frontal area)
/// for each car. If provided, the total drag area (drag coefficient
/// times frontal area) calculated from this vector is the sum of these
/// coefficients.
/// coefficients. Otherwise, each rail car's drag contribution based on its
/// drag coefficient and frontal area will be summed across the train.
pub cd_area_vec: Option<Vec<si::Area>>,
}

Expand Down
Loading