Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class Emission(Base):
gpu_utilization_percent = Column(Float, nullable=True)
ram_utilization_percent = Column(Float, nullable=True)
wue = Column(Float, nullable=False, default=0)
water_consumed = Column(Float, nullable=False, default=0)
run_id = Column(UUID(as_uuid=True), ForeignKey("runs.id", ondelete="CASCADE"))
run = relationship("Run", back_populates="emissions")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def add_emission(self, emission: EmissionCreate) -> UUID:
gpu_energy=emission.gpu_energy,
ram_energy=emission.ram_energy,
energy_consumed=emission.energy_consumed,
# coalesce None to 0: the column is NOT NULL
water_consumed=emission.water_consumed or 0,
cpu_utilization_percent=emission.cpu_utilization_percent,
gpu_utilization_percent=emission.gpu_utilization_percent,
ram_utilization_percent=emission.ram_utilization_percent,
Expand Down Expand Up @@ -108,6 +110,7 @@ def map_sql_to_schema(emission: sql_models.Emission) -> Emission:
gpu_energy=emission.gpu_energy,
ram_energy=emission.ram_energy,
energy_consumed=emission.energy_consumed,
water_consumed=emission.water_consumed,
cpu_utilization_percent=emission.cpu_utilization_percent,
gpu_utilization_percent=emission.gpu_utilization_percent,
ram_utilization_percent=emission.ram_utilization_percent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ def get_project_global_sums_by_experiment(self, project_id):
SqlModelExperiment.description,
func.sum(SqlModelEmission.emissions_sum).label("emission_sum"),
func.sum(SqlModelEmission.energy_consumed).label("energy_consumed"),
func.sum(SqlModelEmission.water_consumed).label("water_consumed"),
func.sum(SqlModelEmission.duration).label("duration"),
)
.join(
Expand Down Expand Up @@ -133,6 +134,7 @@ def get_project_detailed_sums_by_experiment(
func.sum(SqlModelEmission.gpu_energy).label("gpu_energy"),
func.sum(SqlModelEmission.ram_energy).label("ram_energy"),
func.sum(SqlModelEmission.energy_consumed).label("energy_consumed"),
func.sum(SqlModelEmission.water_consumed).label("water_consumed"),
func.sum(SqlModelEmission.duration).label("duration"),
func.avg(SqlModelEmission.emissions_rate).label("emissions_rate"),
func.avg(SqlModelEmission.cpu_utilization_percent).label(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ def get_organization_detailed_sums(
func.sum(SqlModelEmission.gpu_energy).label("gpu_energy"),
func.sum(SqlModelEmission.ram_energy).label("ram_energy"),
func.sum(SqlModelEmission.energy_consumed).label("energy_consumed"),
func.sum(SqlModelEmission.water_consumed).label("water_consumed"),
func.sum(SqlModelEmission.duration).label("duration"),
func.avg(SqlModelEmission.emissions_rate).label("emissions_rate"),
func.avg(SqlModelEmission.cpu_utilization_percent).label(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ def get_project_detailed_sums(
func.sum(SqlModelEmission.gpu_energy).label("gpu_energy"),
func.sum(SqlModelEmission.ram_energy).label("ram_energy"),
func.sum(SqlModelEmission.energy_consumed).label("energy_consumed"),
func.sum(SqlModelEmission.water_consumed).label("water_consumed"),
func.sum(SqlModelEmission.duration).label("duration"),
func.avg(SqlModelEmission.emissions_rate).label("emissions_rate"),
func.avg(SqlModelEmission.cpu_utilization_percent).label(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ def get_experiment_detailed_sums_by_run(
func.sum(SqlModelEmission.gpu_energy).label("gpu_energy"),
func.sum(SqlModelEmission.ram_energy).label("ram_energy"),
func.sum(SqlModelEmission.energy_consumed).label("energy_consumed"),
func.sum(SqlModelEmission.water_consumed).label("water_consumed"),
func.sum(SqlModelEmission.duration).label("duration"),
func.avg(SqlModelEmission.emissions_rate).label("emissions_rate"),
func.avg(SqlModelEmission.cpu_utilization_percent).label(
Expand Down
10 changes: 10 additions & 0 deletions carbonserver/carbonserver/api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ class EmissionBase(BaseModel):
ge=0,
description="The WUE (Water Usage Effectiveness) must be greater than or equal to zero",
)
water_consumed: Optional[float] = Field(
default=0,
ge=0,
description="The water consumed (L) must be greater than or equal to zero",
)

model_config = ConfigDict(
json_schema_extra={
Expand All @@ -117,6 +122,7 @@ class EmissionBase(BaseModel):
"ram_energy": 2.0,
"energy_consumed": 57.21874,
"wue": 0,
"water_consumed": 0,
}
}
)
Expand Down Expand Up @@ -190,6 +196,7 @@ class RunReport(RunBase):
gpu_energy: float
ram_energy: float
energy_consumed: float
water_consumed: float = 0
duration: float
emissions_rate: float
emissions_count: int
Expand Down Expand Up @@ -283,6 +290,7 @@ class ExperimentReport(ExperimentBase):
gpu_energy: float
ram_energy: float
energy_consumed: float
water_consumed: float = 0
duration: int
emissions_rate: float
emissions_count: int
Expand Down Expand Up @@ -396,6 +404,7 @@ class ProjectReport(ProjectBase):
gpu_energy: float
ram_energy: float
energy_consumed: float
water_consumed: float = 0
duration: int
emissions_rate: float
emissions_count: int
Expand Down Expand Up @@ -443,6 +452,7 @@ class OrganizationReport(OrganizationBase):
gpu_energy: float
ram_energy: float
energy_consumed: float
water_consumed: float = 0
duration: int
emissions_rate: float
emissions_count: int
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""add water_consumed to emissions

Revision ID: 20260902_add_water
Revises: 20251119_add_utilization
Create Date: 2026-09-02

"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "20260902_add_water"
down_revision = "20251119_add_utilization"
branch_labels = None
depends_on = None


def upgrade():
op.add_column(
"emissions",
sa.Column("water_consumed", sa.Float(), nullable=False, server_default="0"),
)


def downgrade():
op.drop_column("emissions", "water_consumed")
13 changes: 12 additions & 1 deletion carbonserver/tests/api/routers/test_emissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"ram_energy": 2.0,
"energy_consumed": 57.21874,
"wue": 0,
"water_consumed": 0.0,
}

EMISSION_1 = {
Expand All @@ -57,6 +58,7 @@
"ram_energy": 2.0,
"energy_consumed": 57.21874,
"wue": 0,
"water_consumed": 0.0,
"cpu_utilization_percent": None,
"gpu_utilization_percent": None,
"ram_utilization_percent": None,
Expand All @@ -77,6 +79,7 @@
"ram_energy": 2.0,
"energy_consumed": 57.21874,
"wue": 0,
"water_consumed": 0.0,
}


Expand All @@ -95,6 +98,7 @@
"ram_energy": 2.0,
"energy_consumed": 57.21874,
"wue": 0,
"water_consumed": 0.0,
}


Expand Down Expand Up @@ -229,7 +233,7 @@ def test_add_emission_with_default_wue_value(client, custom_test_server):
"gpu_energy": 0.0,
"ram_energy": 2.0,
"energy_consumed": 57.21874,
# Note: wue is not provided, should default to 0
# Note: wue and water_consumed are not provided, should default to 0
}

repository_mock = mock.Mock(spec=EmissionRepository)
Expand Down Expand Up @@ -266,6 +270,9 @@ def test_add_emission_with_default_wue_value(client, custom_test_server):
# Verify that the repository was called with WUE defaulting to 0
called_emission = repository_mock.add_emission.call_args[0][0]
assert called_emission.wue == 0, "WUE should default to 0 when not provided"
assert (
called_emission.water_consumed == 0
), "water_consumed should default to 0 when not provided"


def test_add_emission_with_custom_wue_value(client, custom_test_server):
Expand All @@ -285,6 +292,7 @@ def test_add_emission_with_custom_wue_value(client, custom_test_server):
"ram_energy": 2.0,
"energy_consumed": 57.21874,
"wue": 1.5,
"water_consumed": 12.5,
}

repository_mock = mock.Mock(spec=EmissionRepository)
Expand Down Expand Up @@ -321,3 +329,6 @@ def test_add_emission_with_custom_wue_value(client, custom_test_server):
# Verify that the repository was called with the correct WUE value
called_emission = repository_mock.add_emission.call_args[0][0]
assert called_emission.wue == 1.5, "WUE should be set to the provided value"
assert (
called_emission.water_consumed == 12.5
), "water_consumed should be set to the provided value"
54 changes: 47 additions & 7 deletions codecarbon/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,34 @@ def config():
)


def _cli_provided(ctx, name: str) -> bool:
"""
Whether the option `name` was typed on the command line (or set through its
environment variable) rather than left at its Typer default.

Options left at their default must not be forwarded to the tracker: doing so
would silently override the values coming from `.codecarbon.config` and the
`CODECARBON_*` environment variables, which the tracker reads itself.
"""
get_source = getattr(ctx, "get_parameter_source", None)
if get_source is None:
# `monitor` called directly from Python, not through Click: every value
# given is explicit.
return True
source = get_source(name)
return source is None or source.name in ("COMMANDLINE", "ENVIRONMENT")


def _external_config() -> dict:
"""The configuration files and CODECARBON_* variables, as a plain dict."""
from codecarbon.core.config import get_hierarchical_config

try:
return dict(get_hierarchical_config())
except Exception:
return {}


@codecarbon.command(
"monitor",
short_help="Monitor your machine's carbon emissions.",
Expand Down Expand Up @@ -393,15 +421,27 @@ def monitor(
):
"""Monitor your machine's carbon emissions."""

# Shared tracker args so monitor and run_and_monitor behave the same
external_conf = _external_config()

# Shared tracker args so monitor and run_and_monitor behave the same.
# Only the options actually given are forwarded: the others are left to the
# tracker, which resolves them from the configuration file and environment.
tracker_args = {
"measure_power_secs": measure_power_secs,
"api_call_interval": api_call_interval,
"log_level": log_level,
name: value
for name, value in (
("measure_power_secs", measure_power_secs),
("api_call_interval", api_call_interval),
("log_level", log_level),
)
if _cli_provided(ctx, name)
}
if "log_level" not in tracker_args and "log_level" not in external_conf:
# Nothing configures it: keep the unattended monitor quiet.
tracker_args["log_level"] = log_level

# Set up the tracker arguments based on mode (offline vs online) and validate required args for each mode
if offline:
if not country_iso_code:
if not country_iso_code and "country_iso_code" not in external_conf:
print(
"ERROR: Country ISO code is required for offline mode. Add it to your configuration or provide it via the command line: `--country-iso-code FRA`",
file=sys.stderr,
Expand All @@ -410,8 +450,8 @@ def monitor(

tracker_args = {
**tracker_args,
"country_iso_code": country_iso_code,
"region": region,
**({"country_iso_code": country_iso_code} if country_iso_code else {}),
**({"region": region} if region else {}),
}
else:
experiment_id = get_existing_exp_id()
Expand Down
12 changes: 8 additions & 4 deletions codecarbon/cli/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
import subprocess
import sys
from typing import Optional

import typer
from rich import print
Expand All @@ -12,9 +13,9 @@
def run_and_monitor(
ctx: typer.Context,
log_level: Annotated[
str,
Optional[str],
typer.Option(help="Log level (critical, error, warning, info, debug)"),
] = "error",
] = None,
offline: bool = False,
**tracker_args,
):
Expand Down Expand Up @@ -51,7 +52,11 @@ def run_and_monitor(
from codecarbon.emissions_tracker import EmissionsTracker, OfflineEmissionsTracker
from codecarbon.external.logger import set_logger_level

set_logger_level(log_level)
# `log_level` is None when nothing set it: leave it to the tracker, which
# resolves it from the configuration file and the environment.
if log_level is not None:
set_logger_level(log_level)
tracker_args["log_level"] = log_level

# Get the command from remaining args (strip nested subcommand / `--` leftovers)
command = list(getattr(ctx, "args", None) or [])
Expand All @@ -67,7 +72,6 @@ def run_and_monitor(

tracker_cls = OfflineEmissionsTracker if offline else EmissionsTracker
tracker = tracker_cls(
log_level=log_level,
save_to_logger=False,
tracking_mode="process",
**tracker_args,
Expand Down
1 change: 1 addition & 0 deletions codecarbon/core/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ def add_emission(self, carbon_emission: dict):
gpu_utilization_percent=carbon_emission.get("gpu_utilization_percent"),
ram_utilization_percent=carbon_emission.get("ram_utilization_percent"),
wue=carbon_emission.get("wue", 0),
water_consumed=carbon_emission.get("water_consumed", 0),
)
try:
payload = dataclasses.asdict(emission)
Expand Down
1 change: 1 addition & 0 deletions codecarbon/core/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class EmissionBase:
gpu_utilization_percent: Optional[float] = None
ram_utilization_percent: Optional[float] = None
wue: Optional[float] = 0
water_consumed: Optional[float] = 0


class EmissionCreate(EmissionBase):
Expand Down
15 changes: 15 additions & 0 deletions codecarbon/core/units.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ def from_kgs_per_kWh(cls, kgs_per_kWh: float) -> "EmissionsPerKWh":
return cls(kgs_per_kWh=kgs_per_kWh)


@dataclass
class WaterPerKWh:
"""
Measured in L/kWh
"""

GALUS_TO_L = 3.785411784

l_per_kWh: float

@classmethod
def from_gal_us_per_MWh(cls, gal_us_per_MWh: float) -> "WaterPerKWh":
return cls(l_per_kWh=gal_us_per_MWh * WaterPerKWh.GALUS_TO_L * 0.001)


@dataclass(order=True)
class Energy:
"""
Expand Down
Loading
Loading