perf: self-describing label dtypes via best_int (widen instead of raise)#38
perf: self-describing label dtypes via best_int (widen instead of raise)#38FBumann wants to merge 13 commits into
Conversation
linopy/constants.py — Added DEFAULT_LABEL_DTYPE = np.int32 linopy/model.py — Variable and constraint label assignment now uses np.arange(..., dtype=DEFAULT_LABEL_DTYPE) with overflow guards that raise ValueError if labels exceed int32 max. linopy/expressions.py — _term coord assignment and all .astype(int) for vars arrays now use DEFAULT_LABEL_DTYPE (int32). linopy/common.py — fill_missing_coords uses np.arange(..., dtype=DEFAULT_LABEL_DTYPE). Polars schema inference now checks array.dtype.itemsize instead of the old OS/numpy-version hack. test/test_constraints.py — Updated 2 dtype assertions to use np.issubdtype instead of == int. test/test_dtypes.py (new) — 7 tests covering int32 labels, expression vars, solve correctness, and overflow guards.
…k to int64 via astype(int), now use DEFAULT_LABEL_DTYPE. Also Variables.to_dataframe arange for map_labels. - linopy/constraints.py: Constraints.to_dataframe arange for map_labels. - linopy/common.py: save_join outer-join fallback was casting to int64.
…ords. Here's what changed: - test_linear_expression_sum / test_linear_expression_sum_with_const: v.loc[:9].add(v.loc[10:], join="override") → v.loc[:9] + v.loc[10:].assign_coords(dim_2=v.loc[:9].coords["dim_2"]) - test_add_join_override → test_add_positional_assign_coords: uses v + disjoint.assign_coords(...) - test_add_constant_join_override → test_add_constant_positional: now uses different coords [5,6,7] + assign_coords to make the test meaningful - test_same_shape_add_join_override → test_same_shape_add_assign_coords: uses + c.to_linexpr().assign_coords(...) - test_add_constant_override_positional → test_add_constant_positional_different_coords: expr + other.assign_coords(...) - test_sub_constant_override → test_sub_constant_positional: expr - other.assign_coords(...) - test_mul_constant_override_positional → test_mul_constant_positional: expr * other.assign_coords(...) - test_div_constant_override_positional → test_div_constant_positional: expr / other.assign_coords(...) - test_variable_mul_override → test_variable_mul_positional: a * other.assign_coords(...) - test_variable_div_override → test_variable_div_positional: a / other.assign_coords(...) - test_add_same_coords_all_joins: removed "override" from loop, added assign_coords variant - test_add_scalar_with_explicit_join → test_add_scalar: simplified to expr + 10
- Move DEFAULT_LABEL_DTYPE from constants.py into options["label_dtype"] - Widen OptionSettings types from int to Any - Add validation: label_dtype only accepts np.int32 or np.int64 - Fix matrices.py empty clabels fallback to use configured dtype - Fix f-string quoting and trailing spaces in overflow error messages - Add -> None annotations and importorskip guard in test_dtypes.py - Add tests for int64 override and invalid dtype rejection - Add release notes entry Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dimension coordinates (fill_missing_coords, _term coord) are small index arrays, not the large label/vars arrays that benefit from int32. xarray's index creation is slower with int32 than the default int64, causing a 13-38% build regression. Revert these to default int while keeping int32 for labels and vars where the memory savings matter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Conflicts: # doc/release_notes.rst # linopy/common.py # linopy/config.py # linopy/matrices.py # linopy/model.py # linopy/variables.py # test/test_constraints.py
Derive each label allocation's int dtype from its known max value (`fitting_label_dtype`), floored at `options["label_dtype"]`. Models that fit the default keep a single predictable dtype (int32); models exceeding the int32 ceiling widen to a larger dtype instead of raising ValueError. Update the label cast-back paths (ffill/bfill/sanitize, save_join, expression combines) to preserve the array's own width rather than hardcoding the default, so widened int64 labels are not silently truncated. ffill/bfill keep the source dtype directly; the float round-trip paths use `astype_labels`, which sizes the result to the actual max value. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merging this PR will improve performance by 25.46%
Performance Changes
Tip Curious why this is faster? Comment Comparing Footnotes
|
|
@codspeedbot explain why this is faster |
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Here's why this PR shows up as faster. It's a memory win, not a CPU win. All 77 improved benchmarks are in Memory mode — there are no wall-time or simulation regressions/improvements, and CodSpeed reports no environment differences between The root cause is the label dtype change. Variable and constraint labels are stored as dense integer arrays. Previously they defaulted to That explains the shape of the results:
So there's no algorithmic speedup here — the models build the same way, they just allocate roughly half as many bytes for label storage, and the biggest, label-heavy models benefit most. Summary
Was this response helpful? React with 👍 or 👎 to help us improve CodSpeedBot. |
Note
The following content was generated by AI.
Follow-up to the int32 default: instead of a fixed dtype guarded by a hard
ValueErrorat the int32 ceiling, derive each label allocation's dtype from its known max value, floored atoptions["label_dtype"].What changed
fitting_label_dtype(max_value)incommon.py: narrowest int dtype holdingmax_value, never narrower than the configuredlabel_dtype. The option becomes a floor.Variable.ffill/bfillpreserve the source label dtype directly (no extra compute).Variable.sanitize,LinearExpressioninit/assign/combine,save_join) useastype_labels, which sizes the result to the actual max value.Why
Per-allocation
best_intis value-correct because the label counters are global and monotonic, soendbounds every label in the group. The only real hazard was the ~8 sites that assumed "array dtype == configured default"; those are fixed here so a promoted int64 array survivesffill/sanitize/etc. without silent truncation.Non-goal: narrowing below the configured default (int8/int16 for tiny models). It saves nothing at solve time (scipy sparse is int32; concat promotes to the widest block) and would make dtypes non-uniform across groups. Flooring at the default keeps the common case predictable.
Tests: old overflow-guard tests replaced with widen-past-int32 tests (labels become int64, no raise); added coverage for
fitting_label_dtypeflooring/widening and forastype_labelsnot truncating values beyond the int32 ceiling. Full suite green locally (1857 passed), ruff + mypy clean.