Skip to content
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
82 changes: 39 additions & 43 deletions 1_basic_single_sim/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,25 @@ def step(
rng: np.random.Generator,
metrics: Dict[str, int],
) -> State:
"""Simulate one time step of the bike-sharing system.
#Mailly -> Moulin
if rng.random() < p1:
if state.mailly > 0:
state.mailly -= 1
state.moulin += 1
else:
state.unmet_mailly += 1
metrics["unmet_mailly"] += 1

Args:
state: Current state of the system (bike counts at each station)
p1: Probability of a user wanting to go from Mailly to Moulin
p2: Probability of a user wanting to go from Moulin to Mailly
rng: Random number generator for stochastic events
metrics: Dictionary to track simulation metrics (unmet demand, etc.)
#Moulin -> Mailly
if rng.random() < p2:
if state.moulin > 0:
state.moulin -= 1
state.mailly += 1
else:
state.unmet_moulin += 1
metrics["unmet_moulin"] += 1

Returns:
Updated state after one simulation step

Note:
- If a station has no bikes available, increment the appropriate unmet demand counter
- Update the state by moving bikes between stations based on probabilities
"""
# User tries to go from mailly -> moulin with prob p1
pass
return state


def run_simulation(
Expand All @@ -54,30 +55,25 @@ def run_simulation(
p2: float,
seed: int,
) -> Tuple[pd.DataFrame, Dict[str, int]]:
"""Run a complete bike-sharing simulation.

Args:
initial_mailly: Initial number of bikes at Mailly station
initial_moulin: Initial number of bikes at Moulin station
steps: Number of simulation steps to run
p1: Probability of movement from Mailly to Moulin
p2: Probability of movement from Moulin to Mailly
seed: Random seed for reproducibility

Returns:
Tuple containing:
- DataFrame with columns ['time', 'mailly', 'moulin'] tracking bike counts over time
- Dictionary with metrics including:
- mailly: Number of bikes at Mailly station
- moulin: Number of bikes at Moulin station
- 'unmet_mailly': Number of unmet requests at Mailly
- 'unmet_moulin': Number of unmet requests at Moulin
- 'final_imbalance': Final difference between station bike counts

Note:
- Create the state object with initial bike counts
- Initialize metrics dictionary with appropriate counters
- Record state at each time step for the DataFrame
- Calculate final imbalance as mailly - moulin
"""
pass
rng = np.random.default_rng(seed)

state = State(mailly=int(initial_mailly), moulin=int(initial_moulin))

metrics: Dict[str, int] = {
"unmet_mailly": 0,
"unmet_moulin": 0,
}

rows = [{"time": 0, "mailly": state.mailly, "moulin": state.moulin}]

for t in range(1, steps + 1):
step(state, p1, p2, rng, metrics)
rows.append({"time": t, "mailly": state.mailly, "moulin": state.moulin})

df = pd.DataFrame(rows, columns=["time", "mailly", "moulin"])

metrics["mailly"] = state.mailly
metrics["moulin"] = state.moulin
metrics["final_imbalance"] = state.mailly - state.moulin

return df, metrics
80 changes: 43 additions & 37 deletions 1_basic_single_sim/run_single.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,47 +7,53 @@


def parse_args():
"""Parse command line arguments for single simulation run.

Returns:
Parsed arguments containing:
- steps: Number of simulation steps
- p1: Probability of movement from Mailly to Moulin
- p2: Probability of movement from Moulin to Mailly
- init_mailly: Initial bikes at Mailly station
- init_moulin: Initial bikes at Moulin station
- seed: Random seed (default: 0)
- out_csv: Output CSV file path
- plot: Boolean flag to generate plots

Note:
Use argparse.ArgumentParser to define all required and optional arguments
"""
# TODO: Implement argument parsing
pass
parser = argparse.ArgumentParser(description="Run a single bike-sharing simulation.")

parser.add_argument("--steps", type=int, required=True, help="Number of simulation steps")
parser.add_argument("--p1", type=float, required=True, help="Prob Mailly -> Moulin")
parser.add_argument("--p2", type=float, required=True, help="Prob Moulin -> Mailly")
parser.add_argument("--init-mailly", dest="init_mailly", type=int, required=True, help="Initial bikes at Mailly")
parser.add_argument("--init-moulin", dest="init_moulin", type=int, required=True, help="Initial bikes at Moulin")
parser.add_argument("--seed", type=int, default=0, help="Random seed (default: 0)")
parser.add_argument("--out-csv", dest="out_csv", type=str, required=True, help="Output CSV file path")
parser.add_argument("--plot", action="store_true", help="Generate plots")

return parser.parse_args()


def main():
"""Main function to run a single bike-sharing simulation.

This function should:
1. Parse command line arguments
2. Run the simulation with specified parameters
3. Save results to CSV files (timeseries and metrics)
4. Optionally generate and save plots

Output files:
- Timeseries data: CSV with time, mailly, moulin columns
- Metrics data: CSV with key-value pairs of simulation metrics
- Optional plot: PNG showing bike counts over time for both stations

Note:
Create output directories if they don't exist
Save metrics as tab-separated key-value pairs
"""
# TODO: Implement main simulation workflow
pass
args = parse_args()

out_csv = Path(args.out_csv)
out_dir = out_csv.parent
out_dir.mkdir(parents=True, exist_ok=True)

df, metrics = run_simulation(
initial_mailly=args.init_mailly,
initial_moulin=args.init_moulin,
steps=args.steps,
p1=args.p1,
p2=args.p2,
seed=args.seed,
)

df.to_csv(out_csv, index=False)

metrics_path = out_dir / f"{out_csv.stem}_metrics.tsv"
with open(metrics_path, "w", encoding="utf-8") as f:
for k, v in metrics.items():
f.write(f"{k}\t{v}\n")

if args.plot:
plt.figure()
plt.plot(df["time"], df["mailly"], label="Mailly")
plt.plot(df["time"], df["moulin"], label="Moulin")
plt.xlabel("time")
plt.ylabel("bikes")
plt.legend()
plot_path = out_dir / f"{out_csv.stem}.png"
plt.savefig(plot_path, dpi=150, bbox_inches="tight")
plt.close()

if __name__ == "__main__":
main()
10 changes: 10 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["python", "1_basic_single_sim/run_single.py"]
10 changes: 10 additions & 0 deletions environment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: velo
channels:
- conda-forge
- defaults
dependencies:
- python=3.12
- numpy
- matplotlib
- pandas
- pytest
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
numpy
matplotlib
pandas
11 changes: 11 additions & 0 deletions velo_python_3.12.def
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Bootstrap: docker
From: python:3.12-slim

%post
pip install --no-cache-dir numpy matplotlib pandas pytest

%files
./ /app/

%runscript
python /app/1_basic_single_sim/run_single.py