Cycles-utils 4.0.0__tar.gz → 4.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/Cycles_utils.egg-info/PKG-INFO +2 -1
  2. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/PKG-INFO +2 -1
  3. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/README.md +1 -0
  4. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/cycles.py +2 -3
  5. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/cycles_tools/_base_file.py +1 -1
  6. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/cycles_tools/operation_file.py +15 -0
  7. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/cycles_tools/plot_tools.py +10 -6
  8. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/ssurgo/ssurgo.py +57 -61
  9. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/setup.py +1 -1
  10. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/Cycles_utils.egg-info/SOURCES.txt +0 -0
  11. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/Cycles_utils.egg-info/dependency_links.txt +0 -0
  12. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/Cycles_utils.egg-info/requires.txt +0 -0
  13. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/Cycles_utils.egg-info/top_level.txt +0 -0
  14. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/LICENSE +0 -0
  15. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/MANIFEST.in +0 -0
  16. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/__init__.py +0 -0
  17. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/cycles_runner.py +0 -0
  18. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/cycles_tools/__init__.py +0 -0
  19. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/cycles_tools/control_file.py +0 -0
  20. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/cycles_tools/nudge_file.py +0 -0
  21. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/cycles_tools/output_file.py +0 -0
  22. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/cycles_tools/reinit_file.py +0 -0
  23. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/cycles_tools/soil_file.py +0 -0
  24. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/cycles_tools/weather_file.py +0 -0
  25. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/data/GLDASp5_elevation_025d.nc4 +0 -0
  26. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/data/GLDASp5_landmask_025d.nc4 +0 -0
  27. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/data/NLDAS_elevation.nc4 +0 -0
  28. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/data/NLDAS_masks-veg-soil.nc4 +0 -0
  29. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/data/fips_gid_conversion.csv +0 -0
  30. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/data/gridMET_elevation_mask.nc +0 -0
  31. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/data/metdata_elevationdata.nc +0 -0
  32. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/data/us_states.csv +0 -0
  33. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/gadm/__init__.py +0 -0
  34. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/gadm/gadm.py +0 -0
  35. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/rotation_builder.py +0 -0
  36. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/soilgrids/__init__.py +0 -0
  37. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/soilgrids/soilgrids.py +0 -0
  38. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/ssurgo/__init__.py +0 -0
  39. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/weather/__init__.py +0 -0
  40. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/cycles/weather/weather.py +0 -0
  41. {cycles_utils-4.0.0 → cycles_utils-4.0.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Cycles-utils
3
- Version: 4.0.0
3
+ Version: 4.0.2
4
4
  Summary: Python scripts to build Cycles input files and post-process Cycles output files
5
5
  Home-page: https://github.com/PSUmodeling/Cycles-utils
6
6
  Author: Yuning Shi
@@ -42,6 +42,7 @@ Dynamic: summary
42
42
  This package provides a number of tools for users to prepare Cycles simulation input files, run Cycles simulations, and post-process Cycles simulation results.
43
43
 
44
44
  For usage examples, please refer to this [Jupyter notebook](https://github.com/PSUmodeling/Cycles/blob/master/cycles-utils.ipynb).
45
+ For more complex usage examples, please refer to this [Colab notebook](https://colab.research.google.com/drive/1d8TFfZR_t6eo-Lgh8ZdR382RUcAXlawf?usp=sharing).
45
46
 
46
47
  # Installation
47
48
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Cycles-utils
3
- Version: 4.0.0
3
+ Version: 4.0.2
4
4
  Summary: Python scripts to build Cycles input files and post-process Cycles output files
5
5
  Home-page: https://github.com/PSUmodeling/Cycles-utils
6
6
  Author: Yuning Shi
@@ -42,6 +42,7 @@ Dynamic: summary
42
42
  This package provides a number of tools for users to prepare Cycles simulation input files, run Cycles simulations, and post-process Cycles simulation results.
43
43
 
44
44
  For usage examples, please refer to this [Jupyter notebook](https://github.com/PSUmodeling/Cycles/blob/master/cycles-utils.ipynb).
45
+ For more complex usage examples, please refer to this [Colab notebook](https://colab.research.google.com/drive/1d8TFfZR_t6eo-Lgh8ZdR382RUcAXlawf?usp=sharing).
45
46
 
46
47
  # Installation
47
48
 
@@ -4,6 +4,7 @@
4
4
  This package provides a number of tools for users to prepare Cycles simulation input files, run Cycles simulations, and post-process Cycles simulation results.
5
5
 
6
6
  For usage examples, please refer to this [Jupyter notebook](https://github.com/PSUmodeling/Cycles/blob/master/cycles-utils.ipynb).
7
+ For more complex usage examples, please refer to this [Colab notebook](https://colab.research.google.com/drive/1d8TFfZR_t6eo-Lgh8ZdR382RUcAXlawf?usp=sharing).
7
8
 
8
9
  # Installation
9
10
 
@@ -150,7 +150,7 @@ class Cycles:
150
150
  return _plot_yield(self.output['harvest'].data, ax=ax, fontsize=fontsize)
151
151
 
152
152
 
153
- def plot_operations(self, rotation_size: int | None=None, *, axs: Axes | np.ndarray | None=None, fontsize: int | None=None):
153
+ def plot_operations(self, *, axs: Axes | np.ndarray | None=None, fontsize: int | None=None):
154
154
  """Plot operation timelines grouped by rotation year.
155
155
 
156
156
  Args:
@@ -167,7 +167,6 @@ class Cycles:
167
167
  assert self.control is not None
168
168
  assert self.operations is not None
169
169
 
170
- if rotation_size is None:
171
- rotation_size = self.control.simulation_years.rotation_size
170
+ rotation_size = self.control.simulation_years.rotation_size
172
171
 
173
172
  return _plot_operations(self.operations, rotation_size, axs=axs, fontsize=fontsize)
@@ -42,6 +42,6 @@ def parse_value(raw: str, name: str, hint: type) -> int | float | str:
42
42
 
43
43
  def unwrap_optional(t) -> type:
44
44
  origin = getattr(t, '__origin__', None)
45
- if origin is Union or origin is types.UnionType:
45
+ if origin is Union or origin is types.UnionType or isinstance(t, types.UnionType):
46
46
  return next(arg for arg in t.__args__ if arg is not type(None))
47
47
  return t
@@ -8,6 +8,7 @@ class Operation(Protocol):
8
8
  year: int | None
9
9
  doy: int
10
10
  relative_doy: bool
11
+ resolved_doy: int | None
11
12
 
12
13
  @dataclass(kw_only=True)
13
14
  class Planting(Operation):
@@ -33,6 +34,7 @@ class Planting(Operation):
33
34
  harvest_timing: float = -999
34
35
  kill_after_harvest: int = 1
35
36
  relative_doy: bool = field(default=False, metadata={'readable': False})
37
+ resolved_doy: int | None = field(default=None, metadata={'readable': False})
36
38
 
37
39
  @dataclass(kw_only=True)
38
40
  class Tillage(Operation):
@@ -43,6 +45,7 @@ class Tillage(Operation):
43
45
  frac_thermal_time: float = 0.0
44
46
  kill_efficiency: float = 0.0
45
47
  relative_doy: bool = field(default=False, metadata={'readable': False})
48
+ resolved_doy: int | None = field(default=None, metadata={'readable': False})
46
49
 
47
50
  @dataclass(kw_only=True)
48
51
  class Harvest(Operation):
@@ -53,6 +56,7 @@ class Harvest(Operation):
53
56
  frac_thermal_time: float = 0.0
54
57
  kill_efficiency: float = 0.0
55
58
  relative_doy: bool = field(default=False, metadata={'readable': False})
59
+ resolved_doy: int | None = field(default=None, metadata={'readable': False})
56
60
 
57
61
  @dataclass(kw_only=True)
58
62
  class Kill(Operation):
@@ -63,6 +67,7 @@ class Kill(Operation):
63
67
  frac_thermal_time: float = 0.0
64
68
  kill_efficiency: float = 0.0
65
69
  relative_doy: bool = field(default=False, metadata={'readable': False})
70
+ resolved_doy: int | None = field(default=None, metadata={'readable': False})
66
71
 
67
72
  @dataclass(kw_only=True)
68
73
  class FixedFertilization(Operation):
@@ -74,6 +79,7 @@ class FixedFertilization(Operation):
74
79
  method: str = 'Broadcast'
75
80
  depth: float = 0.0
76
81
  relative_doy: bool = field(default=False, metadata={'readable': False})
82
+ resolved_doy: int | None = field(default=None, metadata={'readable': False})
77
83
 
78
84
  @dataclass(kw_only=True)
79
85
  class FixedIrrigation(Operation):
@@ -81,6 +87,7 @@ class FixedIrrigation(Operation):
81
87
  doy: int
82
88
  volume: float = 0.0
83
89
  relative_doy: bool = field(default=False, metadata={'readable': False})
90
+ resolved_doy: int | None = field(default=None, metadata={'readable': False})
84
91
 
85
92
  @dataclass(kw_only=True)
86
93
  class AutoIrrigation:
@@ -99,6 +106,13 @@ OPERATION_PARAMETERS = {
99
106
  }
100
107
 
101
108
 
109
+ def _resolve_reference_doy(operations: list[Operation], relative_doy: int) -> int:
110
+ planting = next((op for op in reversed(operations) if isinstance(op, Planting)), None)
111
+ if planting is None:
112
+ raise ValueError("Relative DOY specified but no planting operation found.")
113
+ return planting.doy + relative_doy
114
+
115
+
102
116
  def read_operation_file(operation: str | Path) -> list:
103
117
  """Parse a Cycles operation file into operation objects.
104
118
 
@@ -137,6 +151,7 @@ def read_operation_file(operation: str | Path) -> list:
137
151
  if isinstance(raw_doy, str) and raw_doy.split()[1].strip().startswith('+'):
138
152
  operation_dict['doy'] = int(raw_doy.split()[1].strip()[1:])
139
153
  operation_dict['relative_doy'] = True
154
+ operation_dict['resolved_doy'] = _resolve_reference_doy(operations, operation_dict['doy'])
140
155
  else:
141
156
  operation_dict['doy'] = parse_value(raw_doy, 'doy', hints['doy'])
142
157
  operation_dict['relative_doy'] = False
@@ -150,30 +150,34 @@ def plot_operations(operations: list, rotation_size: int, *, axs: Axes | np.ndar
150
150
  for key, value in OPERATION_TYPES.items():
151
151
  sub_list = [op for op in operations if type(op).__name__.lower() == key and op.year == y + 1]
152
152
 
153
- if len(sub_list) == 0: continue
153
+ if not sub_list: continue
154
154
 
155
155
  axs[y].plot(
156
- [op.doy for op in sub_list], [value.yloc] * len(sub_list),
156
+ [op.doy if not op.relative_doy else op.resolved_doy for op in sub_list], [value.yloc] * len(sub_list),
157
157
  'o',
158
- label=value.title + ':\n' + '\n'.join(f'{op.doy}: {getattr(op, value.label)}' if value.label is not None else f'{op.doy}' for op in sub_list),
158
+ label=value.title + ':\n' + '\n'.join(f'{op.doy if not op.relative_doy else op.resolved_doy}: {getattr(op, value.label)}' if value.label is not None else f'{op.doy}' for op in sub_list),
159
159
  color=value.color,
160
160
  ms=10,
161
161
  )
162
162
 
163
+ if key == 'planting':
164
+ for op in sub_list:
165
+ if op.end_doy == -999: continue
166
+ axs[y].fill_betweenx([value.yloc - 0.5, value.yloc + 0.5], op.doy, op.end_doy, alpha=0.25, color=value.color)
167
+
163
168
  axs[y].set_xlim(-1, 370)
164
169
  axs[y].grid(False)
165
- axs[y].spines['right'].set_color('none')
166
- axs[y].spines['left'].set_color('none')
167
170
  axs[y].yaxis.set_ticks_position('none')
168
171
  axs[y].yaxis.set_tick_params(left=False, right=False, which='both', labelleft=False)
169
172
  axs[y].set_ylim(-3, 7)
170
173
  axs[y].text(184, 5, f'Year {y + 1}', ha='center')
174
+ for spine in ('right', 'left', 'top'):
175
+ axs[y].spines[spine].set_color('none')
171
176
 
172
177
  # set the y-spine
173
178
  axs[y].spines['bottom'].set_position('zero')
174
179
 
175
180
  # turn off the top spine/ticks
176
- axs[y].spines['top'].set_color('none')
177
181
  axs[y].xaxis.tick_bottom()
178
182
  axs[y].set_xticks(MDOYS)
179
183
  axs[y].set_xticklabels(MONTHS)
@@ -1,8 +1,12 @@
1
1
  from __future__ import annotations
2
2
  import geopandas as gpd
3
+ import matplotlib.colors as colors
4
+ import matplotlib.pyplot as plt
5
+ import numpy as np
3
6
  import pandas as pd
4
7
  import shapely
5
8
  from dataclasses import dataclass
9
+ from matplotlib.axes import Axes
6
10
  from pathlib import Path
7
11
  from shapely.geometry import Point
8
12
  from cycles.cycles_tools import generate_soil_file as _generate_soil_file
@@ -65,11 +69,29 @@ SSURGO_PARAMETERS: dict[str, SsurgoParameter] = {
65
69
 
66
70
  LatLon = tuple[float, float]
67
71
 
72
+
73
+ class MapUnitGeoDataFrame(gpd.GeoDataFrame):
74
+ def plot(self, **kwargs) -> Axes:
75
+ n = len(self)
76
+ kwargs.setdefault('column', 'musym')
77
+ kwargs.setdefault('legend', True)
78
+ kwargs.setdefault(
79
+ 'cmap',
80
+ 'tab20' if n > 20 else _truncate_colormap(plt.get_cmap('tab20'), 0, n / 20),
81
+ )
82
+ ax = super().plot(**kwargs)
83
+ ax.axis('off')
84
+ ax.set_aspect('equal')
85
+ return ax
86
+
87
+
68
88
  class Ssurgo:
69
89
  """Load SSURGO lookup tables and extract soil profiles for locations."""
70
90
 
71
91
  def __init__(self, path: str | Path, state: str, *, lat_lon: LatLon | None=None, boundary: gpd.GeoDataFrame | None=None) -> None:
72
92
  """Initialize SSURGO lookup tables and optional spatial subset.
93
+ When a location (`lat_lon`) or boundary polygon is provided, the map-unit table is filtered to include only those map units that intersect the location or polygon;
94
+ the map-units are also grouped by name and symbol, and the major map unit is selected for profile extraction.
73
95
 
74
96
  Args:
75
97
  path: Directory containing SSURGO geodatabase and lookup CSV files.
@@ -86,10 +108,10 @@ class Ssurgo:
86
108
  _validate_geographic_input(lat_lon, boundary)
87
109
 
88
110
  self.state: str = state
89
- self._mapunits: gpd.GeoDataFrame | pd.DataFrame | None = None
90
- self.grouped_mapunits: gpd.GeoDataFrame | pd.DataFrame | None = None
91
- self.components: pd.DataFrame | None = None
92
- self.horizons: pd.DataFrame | None = None
111
+ self._mapunits: gpd.GeoDataFrame | pd.DataFrame
112
+ self.components: pd.DataFrame
113
+ self.horizons: pd.DataFrame
114
+ self.grouped_mapunits: MapUnitGeoDataFrame | None = None
93
115
  self.mukey: int | None = None
94
116
  self.slope: float | None = None
95
117
  self.hsg: str = ''
@@ -111,21 +133,25 @@ class Ssurgo:
111
133
  )
112
134
  gdf = _read_mupolygon(path, state, boundary)
113
135
  self._mapunits = gdf.merge(luts['mapunit'], on='mukey', how='left')
114
-
115
136
  self.components = self.components[self.components['mukey'].isin(self._mapunits['mukey'].unique())]
116
137
  self.horizons = self.horizons[self.horizons['cokey'].isin(self.components['cokey'].unique())]
117
138
 
139
+ self._group_map_units()
140
+ self._select_major_mapunit()
118
141
  self._average_slope_hsg()
119
142
 
120
143
 
121
144
  @property
122
- def mapunits(self) -> gpd.GeoDataFrame | pd.DataFrame | None:
145
+ def mapunits(self) -> MapUnitGeoDataFrame | pd.DataFrame:
123
146
  """Return loaded map-unit table.
124
147
 
125
148
  Returns:
126
149
  Map-unit table as GeoDataFrame/DataFrame, or None if not loaded.
127
150
  """
128
- return self._mapunits
151
+ if isinstance(self._mapunits, gpd.GeoDataFrame):
152
+ return MapUnitGeoDataFrame(self._mapunits, geometry=self._mapunits.geometry.name, crs=self._mapunits.crs)
153
+ else:
154
+ return self._mapunits
129
155
 
130
156
 
131
157
  @property
@@ -152,39 +178,22 @@ class Ssurgo:
152
178
  return self._mapunits[self._mapunits['mukey'] == self.mukey]['musym'].iloc[0]
153
179
 
154
180
 
155
- def group_map_units(self, *, geometry: bool=False) -> None:
156
- """Group map units by base soil-series name.
157
-
158
- Many map units share a base soil texture but differ in slope class or
159
- minor qualifiers. This method groups by the base ``muname`` text before
160
- the first comma and optionally dissolves geometries.
161
-
162
- Args:
163
- geometry: If True, dissolve polygons by grouped map-unit name.
164
-
165
- Returns:
166
- None.
167
- """
168
- if self.grouped_mapunits is not None:
169
- return
170
- assert self._mapunits is not None
171
-
181
+ def _group_map_units(self) -> None:
172
182
  gmu = self._mapunits.copy()
173
183
  gmu['muname'] = gmu['muname'].map(lambda name: name.split(',')[0]) # type: ignore
174
- gmu['musym'] = gmu['musym'].map(_strip_slope_suffix)
184
+ gmu['musym'] = gmu['musym'].map(_strip_slope_suffix)
175
185
 
176
186
  mask = self.non_soil_mask(gmu)
177
187
  gmu.loc[mask, 'muname'] = 'Water, urban, etc.'
178
188
  gmu.loc[mask, 'mukey'] = -999
179
189
  gmu.loc[mask, 'musym'] = 'N/A'
180
190
 
181
- if geometry:
182
- gmu = gmu.dissolve(
183
- by='muname',
184
- aggfunc={'mukey': 'first', 'musym': 'first', 'shape_area': 'sum'},
185
- ).reset_index() # type: ignore
191
+ gmu = gmu.dissolve(
192
+ by='muname',
193
+ aggfunc={'mukey': 'first', 'musym': 'first', 'shape_area': 'sum'},
194
+ ).reset_index() # type: ignore
186
195
 
187
- self.grouped_mapunits = gmu
196
+ self.grouped_mapunits = MapUnitGeoDataFrame(gmu, geometry=gmu.geometry.name, crs=gmu.crs)
188
197
 
189
198
 
190
199
  def non_soil_mask(self, mapunits: pd.DataFrame | gpd.GeoDataFrame) -> pd.Series:
@@ -203,18 +212,8 @@ class Ssurgo:
203
212
  )
204
213
 
205
214
 
206
- def select_major_mapunit(self) -> None:
207
- """Select the dominant map unit by area.
208
-
209
- Returns:
210
- None.
211
- """
212
- if self.mukey is not None:
213
- return
214
- if self.grouped_mapunits is None:
215
- self.group_map_units(geometry=True)
215
+ def _select_major_mapunit(self) -> None:
216
216
  assert self.grouped_mapunits is not None
217
-
218
217
  gdf = self.grouped_mapunits[~self.non_soil_mask(self.grouped_mapunits)].copy()
219
218
  gdf['area'] = gdf.area
220
219
  self.mukey = int(gdf.loc[gdf['area'].idxmax(), 'mukey']) # type: ignore
@@ -231,7 +230,6 @@ class Ssurgo:
231
230
  Soil profile as a list of ``SoilLayer`` records.
232
231
  """
233
232
  mukey = mukey or self._ensure_mukey()
234
- assert self.components is not None and self.horizons is not None
235
233
 
236
234
  df = self.components[self.components['mukey'] == int(mukey)].copy()
237
235
  if major_only:
@@ -261,19 +259,15 @@ class Ssurgo:
261
259
  Returns:
262
260
  None.
263
261
  """
264
- if mukey is None:
265
- self.group_map_units(geometry=True)
266
- self.select_major_mapunit()
267
- mukey = self.mukey
268
-
269
- assert mukey is not None
270
- assert self._mapunits is not None
262
+ if mukey is not None:
263
+ hsg = self._mapunits[self._mapunits['mukey'] == mukey]['hydgrpdcd'].iloc[0] if hsg is None else hsg
264
+ slope = self._mapunits[self._mapunits['mukey'] == mukey]['slopegradwta'].iloc[0] if slope is None else slope
265
+ else:
266
+ mukey = self._ensure_mukey()
267
+ hsg = self.hsg if hsg is None else hsg
268
+ slope = self.slope if slope is None else slope
271
269
 
272
- if hsg is None:
273
- hsg = self._mapunits[self._mapunits['mukey'] == mukey]['hydgrpdcd'].iloc[0] if not self.hsg else self.hsg
274
- assert hsg is not None
275
- if slope is None:
276
- slope = self._mapunits[self._mapunits['mukey'] == mukey]['slopegradwta'].iloc[0] if not self.slope else self.slope
270
+ assert mukey is not None and hsg is not None and slope is not None
277
271
 
278
272
  profile = self.get_soil_profile(mukey=mukey)
279
273
  desc = desc if desc is not None else _build_desc(self._get_muname(mukey), mukey, hsg)
@@ -281,20 +275,15 @@ class Ssurgo:
281
275
 
282
276
 
283
277
  def _ensure_mukey(self) -> int:
284
- if self.mukey is None:
285
- self.select_major_mapunit()
286
- assert self.mukey is not None
278
+ assert self.mukey is not None, "A major soil map unit has not been selected, because a location or boundary has not been provided."
287
279
  return self.mukey
288
280
 
289
281
 
290
282
  def _get_muname(self, mukey: int) -> str:
291
- assert self._mapunits is not None
292
283
  return self._mapunits[self._mapunits['mukey'] == mukey]['muname'].iloc[0]
293
284
 
294
285
 
295
286
  def _average_slope_hsg(self) -> None:
296
- assert self._mapunits is not None
297
-
298
287
  gdf = self._mapunits[~self.non_soil_mask(self._mapunits)].copy()
299
288
  gdf['area'] = gdf.area
300
289
 
@@ -302,6 +291,13 @@ class Ssurgo:
302
291
  self.hsg = _dominant_hsg(gdf) # type: ignore
303
292
 
304
293
 
294
+ def _truncate_colormap(cmap, minval=0.0, maxval=1.0, n=100):
295
+ return colors.LinearSegmentedColormap.from_list(
296
+ 'trunc({n},{a:.2f},{b:.2f})'.format(n=cmap.name, a=minval, b=maxval),
297
+ cmap(np.linspace(minval, maxval, n))
298
+ )
299
+
300
+
305
301
  def _validate_geographic_input(lat_lon: LatLon | None, boundary: gpd.GeoDataFrame | None) -> None:
306
302
  if lat_lon is not None and boundary is not None:
307
303
  raise ValueError("lat_lon and boundary are mutually exclusive — provide only one.")
@@ -5,7 +5,7 @@ with open('README.md', 'r') as f:
5
5
 
6
6
  setup(
7
7
  name='Cycles-utils',
8
- version='4.0.0',
8
+ version='4.0.2',
9
9
  author='Yuning Shi',
10
10
  author_email="shiyuning@gmail.com",
11
11
  packages=find_packages(),
File without changes
File without changes
File without changes