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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ repos:

- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
rev: 0.11.32
rev: 0.12.0
hooks:
- id: uv-lock

Expand Down
42 changes: 9 additions & 33 deletions cable_thermal_model/model/abstract_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,20 @@ class AbstractModel(ABC, Generic[ModelRunOptionsT, StateT, ScenarioSchemaT, Stat
"""Abstract base class for thermal cable models."""

static_env: StaticEnvT
scenario: DataFrame[ScenarioSchemaT]
_scenario_schema_class: type[ScenarioSchemaT]

def __str__(self):
"""Generates a concise string representation of the model."""
num_circuits = len(self.static_env.circuits)
num_days = (self.scenario.index[-1] - self.scenario.index[0]).days
num_days = round(num_days, 1) if num_days < 7 else int(num_days) # round for readability # noqa: PLR2004
return f"Model with {num_circuits} circuit environment and {num_days} day scenario"
return f"Model with {num_circuits} circuit environment"

def __repr__(self):
"""Generates an informative string representation of the model."""
return (
"Environment\n\n"
+ f"{tab_lines(repr(self.static_env))}\n"
+ "\n\tScenario\n"
+ f"{tab_lines(tab_lines(repr(self.scenario.describe())))}\n"
)
return "Environment\n\n" + f"{tab_lines(repr(self.static_env))}\n"

def __init__(self, static_env: StaticEnvT, scenario: DataFrame[ScenarioSchemaT]):
"""Initialise the model with a static environment and scenario DataFrame."""
# Validate that the scenario dataframe provides the required cable loads and ambient temperature.
def __init__(self, static_env: StaticEnvT):
"""Initialise the model with a static environment."""
self.static_env = static_env
self.set_scenario(scenario=scenario)
self._set_run_options(run_options=None)

def _validate_scenario(self, scenario: pd.DataFrame) -> DataFrame[ScenarioSchemaT]:
Expand All @@ -63,27 +53,9 @@ def _validate_scenario(self, scenario: pd.DataFrame) -> DataFrame[ScenarioSchema

return self._scenario_schema_class.validate(scenario)

def set_scenario(self, scenario: pd.DataFrame):
"""Sets a new scenario and validates it.

Args:
scenario: The new scenario dataframe

"""
self.scenario = self._validate_scenario(scenario=scenario)

# Set up time grids
self.time_max: float = (self.scenario.index[-1] - self.scenario.index[0]).total_seconds()
self.time_grid: list[float] = list((self.scenario.index - self.scenario.index[0]).total_seconds())
self.time_samples: int = len(self.time_grid)

@property
def n_scenario_rows(self) -> int:
"""Returns the number of time steps in the scenario."""
return len(self.scenario.index)

def run(
self,
scenario: pd.DataFrame,
initial_state: StateT | None = None,
run_options: ModelRunOptionsT | dict | None = None,
) -> ModelOutputSchema[StateT]:
Expand All @@ -99,6 +71,7 @@ def run(
- initial_state

Args:
scenario: Scenario dataframe for this run. The dataframe is validated internally before execution.
initial_state: Heating information from a previous computation.
run_options: Run options for the model. If `None` or a dictionary is provided, the
options are validated and default values are applied.
Expand All @@ -110,12 +83,14 @@ def run(
ValueError: If the provided initial state does not match the model environment.

"""
validated_scenario = self._validate_scenario(scenario=scenario)
self._set_run_options(run_options=run_options)

self._validate_initial_state(initial_state=initial_state)

# Compute the temperature solution.
result = self._compute_temperature_solution(
scenario=validated_scenario,
initial_state=initial_state,
)

Expand All @@ -128,6 +103,7 @@ def _set_run_options(self, run_options: ModelRunOptionsT | dict | None) -> None:
@abstractmethod
def _compute_temperature_solution(
self,
scenario: DataFrame[ScenarioSchemaT],
initial_state: StateT | None = None,
) -> ModelOutputSchema[StateT]:
"""Compute and return the full temperature solution for the configured scenario."""
Expand Down
24 changes: 10 additions & 14 deletions cable_thermal_model/model/cables/cable.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ def update_pipe_fill_resistivity(self, temperature_grid: np.ndarray) -> None:
self._update_rho_grid(
start_index=pipe_fill_start_index,
end_index=pipe_fill_end_index,
rho=new_pipe_fill_rho,
rho_values=new_pipe_fill_rho,
)

def get_layer_indices_for_layer(self, layer: CableLayer) -> tuple[int, int]:
Expand Down Expand Up @@ -446,27 +446,27 @@ def _calculate_inter_rhos(self, radii: np.ndarray, inter_radii: np.ndarray, rhos
radii[1:] / radii[:-1]
)

def _update_rho_grid(self, start_index: int, end_index: int, rho: float) -> None:
"""Update a slice of the rho-grid with a new value if significant change is detected.
def _update_rho_grid(self, start_index: int, end_index: int, rho_values: np.ndarray | float) -> None:
"""Update a slice of the rho-grid if any new value differs by more than 1% from the current value.

Args:
start_index (int): The starting index of the slice to update (inclusive).
end_index (int): The ending index of the slice to update (inclusive).
rho (float): The new resistivity value to set for the specified slice.
rho_values (np.ndarray | float): The new resistivity value(s) to set for the specified slice.

"""
if start_index > end_index:
raise ValueError("The start_index exceeds the end_index. Cannot update the rho grid.")

rho_slice = self._rho_grid[start_index : end_index + 1]
if np.all(np.isclose(rho_slice, rho, rtol=1e-2)):
return
old_rho_values = self._rho_grid[start_index : end_index + 1]
rho_values_changed = not np.all(np.isclose(old_rho_values, rho_values, rtol=1e-2))

self._rho_grid[start_index : end_index + 1] = rho
self._invalidate_finite_difference_matrix_diagonals()
if rho_values_changed:
self._rho_grid[start_index : end_index + 1] = rho_values
self._invalidate_finite_difference_matrix_diagonals()

def _update_capacity_grid(self, start_index: int, end_index: int, capacity: float) -> None:
"""Update a slice of the capacity-grid with a new value if significant change is detected.
"""Update a slice of the capacity-grid with a new value.

Args:
start_index (int): The starting index of the slice to update (inclusive).
Expand All @@ -477,10 +477,6 @@ def _update_capacity_grid(self, start_index: int, end_index: int, capacity: floa
if start_index > end_index:
raise ValueError("The start_index exceeds the end_index. Cannot update the capacity grid.")

capacity_slice = self._capacity_grid[start_index : end_index + 1]
if np.all(np.isclose(capacity_slice, capacity, rtol=1e-2)):
return

self._capacity_grid[start_index : end_index + 1] = capacity

def _update_vector_with_heat_generation_for_layer(self, heat_generation: float, layer: CableLayer) -> None:
Expand Down
26 changes: 12 additions & 14 deletions cable_thermal_model/model/cables/cable_soil.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,27 +105,25 @@ def _update_soil_resistivity(self, soil_rho: float, dry_soil_radius: float | Non

Args:
soil_rho (float):
An optional float representing the thermal resistivity of the soil that is not dried out.
The new thermal resistivity of the soil that is not dried out.
dry_soil_radius (float | None):
A float representing the radius of the dried-out soil around the cable.
The radius of the dried-out soil around the cable. If None, no dried-out soil is assumed.

"""
start_index = self._get_soil_grid_start_index()
self._update_rho_grid(
start_index=start_index,
end_index=self.grid_size - 1,
rho=soil_rho,
)
soil_start_index = self._get_soil_grid_start_index()
new_rho_values = np.full(self.grid_size - soil_start_index, soil_rho)

if dry_soil_radius is not None:
dry_soil_rho = 2.5 # mK/W, value taken from NPR3626
end_index = int((self._radii_grid <= dry_soil_radius).sum()) - 1
if end_index > start_index:
self._update_rho_grid(
start_index=start_index,
end_index=end_index,
rho=dry_soil_rho,
)
if end_index > soil_start_index:
new_rho_values[: end_index - soil_start_index + 1] = dry_soil_rho

self._update_rho_grid(
start_index=soil_start_index,
end_index=self.grid_size - 1,
rho_values=new_rho_values,
)

def _get_dry_soil_radius(self, temperature_grid: np.ndarray, soil_drying: bool) -> float | None:
"""Return the radius of dried-out soil based on temperature and scenario settings."""
Expand Down
Loading
Loading