Srishti280992 commited on
Commit
ee58ced
·
verified ·
1 Parent(s): b3777d8

Update core/resource.py

Browse files
Files changed (1) hide show
  1. core/resource.py +115 -387
core/resource.py CHANGED
@@ -4,12 +4,10 @@ core.resource
4
 
5
  Generic resource representation for WorldSmithAI.
6
 
7
- This module defines the Resource runtime object used by the simulation engine.
8
  Resources are domain-agnostic quantities that agents and behaviors may observe,
9
  consume, regenerate, exchange, transform, or produce.
10
 
11
- Examples of resources include:
12
-
13
  - food
14
  - grass
15
  - water
@@ -23,26 +21,6 @@ Examples of resources include:
23
 
24
  The Resource class does not know what any resource type means. Domain semantics
25
  are supplied by the DSL, behaviors, policies, and world rules.
26
-
27
- Minimal usage example
28
- ---------------------
29
-
30
- resource = Resource(
31
- id="food_patch_1",
32
- type="food",
33
- amount=10.0,
34
- position=[2.0, 3.0],
35
- metadata={
36
- "capacity": 20.0,
37
- "regeneration_rate": 1.5,
38
- },
39
- )
40
-
41
- depletion = resource.deplete(3.0)
42
- print(depletion.new_amount)
43
-
44
- regeneration = resource.regenerate()
45
- print(regeneration.new_amount)
46
  """
47
 
48
  from __future__ import annotations
@@ -58,42 +36,13 @@ from numpy.typing import NDArray
58
 
59
  logger = logging.getLogger(__name__)
60
 
61
- PositionInput: TypeAlias = Sequence[float] | NDArray[np.float64]
62
  ResourceOperation: TypeAlias = Literal["deplete", "regenerate", "set_amount"]
63
 
64
 
65
  @dataclass(frozen=True, slots=True)
66
  class ResourceOperationResult:
67
- """
68
- Structured result of a resource operation.
69
-
70
- Attributes
71
- ----------
72
- resource_id:
73
- Unique id of the affected resource.
74
- resource_type:
75
- Domain-level resource type string.
76
- operation:
77
- Operation name, such as ``"deplete"`` or ``"regenerate"``.
78
- requested_amount:
79
- Amount requested by the caller.
80
- actual_delta:
81
- Signed change applied to the resource amount.
82
- Regeneration produces a positive delta.
83
- Depletion produces a negative delta.
84
- previous_amount:
85
- Resource amount before the operation.
86
- new_amount:
87
- Resource amount after the operation.
88
- success:
89
- Whether the operation completed successfully.
90
- partial:
91
- Whether the operation applied less than the requested amount.
92
- message:
93
- Human-readable operation summary.
94
- metadata:
95
- Additional structured operation metadata.
96
- """
97
 
98
  resource_id: str
99
  resource_type: str
@@ -108,16 +57,8 @@ class ResourceOperationResult:
108
  metadata: Mapping[str, Any] = field(default_factory=dict)
109
 
110
  def __post_init__(self) -> None:
111
- """
112
- Validate operation result fields.
113
-
114
- Raises
115
- ------
116
- ValueError
117
- If any numeric field is non-finite or invalid.
118
- TypeError
119
- If metadata is not a mapping.
120
- """
121
  numeric_fields = {
122
  "requested_amount": self.requested_amount,
123
  "actual_delta": self.actual_delta,
@@ -148,14 +89,8 @@ class ResourceOperationResult:
148
  object.__setattr__(self, "metadata", dict(self.metadata))
149
 
150
  def to_dict(self) -> dict[str, Any]:
151
- """
152
- Convert the operation result into a JSON-friendly dictionary.
153
-
154
- Returns
155
- -------
156
- dict[str, Any]
157
- Serializable operation result.
158
- """
159
  return {
160
  "resource_id": self.resource_id,
161
  "resource_type": self.resource_type,
@@ -173,113 +108,82 @@ class ResourceOperationResult:
173
 
174
  @dataclass(slots=True)
175
  class Resource:
176
- """
177
- Generic domain-agnostic world resource.
178
-
179
- A resource stores a finite non-negative quantity at a numeric position.
180
- Its semantic meaning is entirely defined by the DSL and behaviors.
181
-
182
- Attributes
183
- ----------
184
- id:
185
- Unique resource identifier.
186
- type:
187
- Domain-level resource type string, such as ``"food"``, ``"money"``,
188
- ``"knowledge"``, or ``"mana"``.
189
- amount:
190
- Current non-negative resource quantity.
191
- position:
192
- Numeric position vector. The engine does not assume 2D only.
193
- metadata:
194
- Optional structured metadata. Common optional keys may include
195
- ``"capacity"``, ``"regeneration_rate"``, ``"owner_id"``, ``"tags"``,
196
- or domain-specific configuration.
197
  """
198
 
199
  id: str
200
  type: str
201
  amount: float
202
- position: PositionInput
203
  metadata: dict[str, Any] = field(default_factory=dict)
 
 
 
204
 
205
  def __post_init__(self) -> None:
206
- """
207
- Validate and normalize resource fields.
208
-
209
- Raises
210
- ------
211
- ValueError
212
- If id, type, amount, or position are invalid.
213
- TypeError
214
- If metadata has invalid type or non-string keys.
215
- """
216
  if not isinstance(self.id, str) or not self.id.strip():
217
  raise ValueError("Resource.id must be a non-empty string.")
218
 
219
  if not isinstance(self.type, str) or not self.type.strip():
220
  raise ValueError("Resource.type must be a non-empty string.")
221
 
 
 
 
222
  self.id = self.id.strip()
223
  self.type = self.type.strip()
224
  self.amount = self._normalize_non_negative_float(self.amount, "amount")
225
  self.position = self._normalize_position(self.position)
226
 
227
- if not isinstance(self.metadata, Mapping):
228
- raise TypeError("Resource.metadata must be a mapping.")
229
-
230
  self.metadata = dict(self.metadata)
231
  self._validate_string_keys(self.metadata, "metadata")
232
 
233
- capacity = self.capacity
234
- if capacity is not None and self.amount > capacity:
 
 
 
 
 
 
 
 
235
  logger.warning(
236
- "Resource amount exceeds metadata capacity: resource_id=%s "
237
- "amount=%s capacity=%s",
238
  self.id,
239
  self.amount,
240
- capacity,
241
  )
242
 
243
  @property
244
  def capacity(self) -> float | None:
245
- """
246
- Optional maximum amount for this resource.
247
-
248
- The value is read from ``metadata["capacity"]`` when present.
249
 
250
- Returns
251
- -------
252
- float | None
253
- Non-negative finite capacity, or None if no capacity is configured.
254
- """
255
- return self._read_optional_metadata_float("capacity")
256
 
257
- @property
258
- def regeneration_rate(self) -> float:
259
- """
260
- Optional default regeneration amount per resource update.
261
-
262
- The value is read from ``metadata["regeneration_rate"]`` when present.
263
- Missing values default to ``0.0``.
264
 
265
- Returns
266
- -------
267
- float
268
- Non-negative finite regeneration amount.
269
- """
270
- rate = self._read_optional_metadata_float("regeneration_rate")
271
- return 0.0 if rate is None else rate
272
 
273
  @property
274
  def is_empty(self) -> bool:
275
- """
276
- Return whether this resource has no available quantity.
277
-
278
- Returns
279
- -------
280
- bool
281
- True if amount is zero, otherwise False.
282
- """
283
  return self.amount <= 0.0
284
 
285
  def regenerate(
@@ -289,31 +193,8 @@ class Resource:
289
  capacity: float | None = None,
290
  metadata: Mapping[str, Any] | None = None,
291
  ) -> ResourceOperationResult:
292
- """
293
- Increase the resource amount.
294
-
295
- If ``amount`` is omitted, the method uses ``metadata["regeneration_rate"]``
296
- when available, otherwise ``0.0``.
297
-
298
- If ``capacity`` is omitted, the method uses ``metadata["capacity"]`` when
299
- available. If no capacity exists, the resource may grow without an upper
300
- bound.
301
-
302
- Parameters
303
- ----------
304
- amount:
305
- Non-negative amount to regenerate. Defaults to the configured
306
- regeneration rate.
307
- capacity:
308
- Optional non-negative maximum resource amount for this operation.
309
- metadata:
310
- Additional metadata attached to the operation result.
311
-
312
- Returns
313
- -------
314
- ResourceOperationResult
315
- Structured result of the regeneration operation.
316
- """
317
  requested_amount = (
318
  self.regeneration_rate
319
  if amount is None
@@ -376,29 +257,8 @@ class Resource:
376
  allow_partial: bool = True,
377
  metadata: Mapping[str, Any] | None = None,
378
  ) -> ResourceOperationResult:
379
- """
380
- Decrease the resource amount.
381
-
382
- Parameters
383
- ----------
384
- amount:
385
- Non-negative amount requested for depletion.
386
- allow_partial:
387
- Whether to allow depleting the remaining available amount when the
388
- requested amount exceeds the current amount.
389
- metadata:
390
- Additional metadata attached to the operation result.
391
-
392
- Returns
393
- -------
394
- ResourceOperationResult
395
- Structured result of the depletion operation.
396
-
397
- Raises
398
- ------
399
- TypeError
400
- If ``allow_partial`` is not a boolean.
401
- """
402
  requested_amount = self._normalize_non_negative_float(amount, "amount")
403
 
404
  if not isinstance(allow_partial, bool):
@@ -458,26 +318,8 @@ class Resource:
458
  *,
459
  metadata: Mapping[str, Any] | None = None,
460
  ) -> ResourceOperationResult:
461
- """
462
- Set the resource amount directly.
463
-
464
- This method is useful for world events, scripted interventions, tests,
465
- and world factory initialization corrections. Behaviors should usually
466
- prefer ``deplete()`` or ``regenerate()`` so operation semantics remain
467
- explicit.
468
-
469
- Parameters
470
- ----------
471
- amount:
472
- New non-negative finite resource amount.
473
- metadata:
474
- Additional metadata attached to the operation result.
475
-
476
- Returns
477
- -------
478
- ResourceOperationResult
479
- Structured result of the set operation.
480
- """
481
  normalized_amount = self._normalize_non_negative_float(amount, "amount")
482
  previous_amount = self.amount
483
  self.amount = normalized_amount
@@ -494,40 +336,13 @@ class Resource:
494
  )
495
 
496
  def set_position(self, position: PositionInput) -> None:
497
- """
498
- Set the resource position.
499
-
500
- Parameters
501
- ----------
502
- position:
503
- New numeric position vector.
504
-
505
- Raises
506
- ------
507
- ValueError
508
- If the position is invalid.
509
- """
510
  self.position = self._normalize_position(position)
511
 
512
  def distance_to(self, position: PositionInput) -> float:
513
- """
514
- Compute Euclidean distance from this resource to a position.
515
-
516
- Parameters
517
- ----------
518
- position:
519
- Numeric position vector.
520
-
521
- Returns
522
- -------
523
- float
524
- Euclidean distance.
525
-
526
- Raises
527
- ------
528
- ValueError
529
- If dimensionality does not match.
530
- """
531
  other_position = self._normalize_position(position)
532
 
533
  if self.position.shape != other_position.shape:
@@ -540,37 +355,35 @@ class Resource:
540
  return float(np.linalg.norm(self.position - other_position))
541
 
542
  def snapshot(self) -> dict[str, Any]:
543
- """
544
- Return a JSON-friendly snapshot of this resource.
545
-
546
- Returns
547
- -------
548
- dict[str, Any]
549
- Serializable resource snapshot.
550
- """
551
  return {
552
  "id": self.id,
553
  "type": self.type,
554
  "amount": self.amount,
555
  "position": self.position.tolist(),
 
 
556
  "metadata": deepcopy(self.metadata),
557
  }
558
 
 
 
 
 
 
559
  def copy(self) -> Resource:
560
- """
561
- Return a deep copy of this resource.
562
-
563
- Returns
564
- -------
565
- Resource
566
- Independent copy of this resource.
567
- """
568
  return Resource(
569
  id=self.id,
570
  type=self.type,
571
  amount=self.amount,
572
  position=self.position.copy(),
573
  metadata=deepcopy(self.metadata),
 
 
 
574
  )
575
 
576
  def _operation_result(
@@ -585,33 +398,8 @@ class Resource:
585
  message: str,
586
  metadata: Mapping[str, Any] | None,
587
  ) -> ResourceOperationResult:
588
- """
589
- Build a normalized resource operation result.
590
-
591
- Parameters
592
- ----------
593
- operation:
594
- Operation name.
595
- requested_amount:
596
- Requested non-negative amount.
597
- previous_amount:
598
- Amount before the operation.
599
- new_amount:
600
- Amount after the operation.
601
- success:
602
- Whether the operation succeeded.
603
- partial:
604
- Whether less than the requested amount was applied.
605
- message:
606
- Human-readable operation summary.
607
- metadata:
608
- Additional result metadata.
609
-
610
- Returns
611
- -------
612
- ResourceOperationResult
613
- Structured operation result.
614
- """
615
  if metadata is not None and not isinstance(metadata, Mapping):
616
  raise TypeError("metadata must be a mapping when provided.")
617
 
@@ -630,77 +418,50 @@ class Resource:
630
  )
631
 
632
  def _resolve_capacity(self, capacity: float | None) -> float | None:
633
- """
634
- Resolve operation-specific or metadata-defined capacity.
635
-
636
- Parameters
637
- ----------
638
- capacity:
639
- Explicit capacity override.
640
-
641
- Returns
642
- -------
643
- float | None
644
- Non-negative finite capacity, or None.
645
- """
646
  if capacity is not None:
647
  return self._normalize_non_negative_float(capacity, "capacity")
648
 
649
  return self.capacity
650
 
651
- def _read_optional_metadata_float(self, key: str) -> float | None:
652
- """
653
- Read an optional non-negative float from resource metadata.
654
-
655
- Parameters
656
- ----------
657
- key:
658
- Metadata key.
659
-
660
- Returns
661
- -------
662
- float | None
663
- Normalized non-negative float, or None.
664
-
665
- Raises
666
- ------
667
- TypeError
668
- If key is invalid.
669
- ValueError
670
- If the metadata value is not a finite non-negative number.
671
- """
672
- if not isinstance(key, str) or not key:
673
- raise TypeError("metadata key must be a non-empty string.")
674
-
675
- if key not in self.metadata or self.metadata[key] is None:
676
- return None
677
-
678
- return self._normalize_non_negative_float(
679
- self.metadata[key],
680
- f"metadata['{key}']",
681
- )
682
 
683
  @staticmethod
684
  def _normalize_position(position: PositionInput) -> NDArray[np.float64]:
685
- """
686
- Normalize a position input into a one-dimensional numpy float array.
687
-
688
- Parameters
689
- ----------
690
- position:
691
- Sequence or numpy array representing a position.
692
-
693
- Returns
694
- -------
695
- NDArray[np.float64]
696
- Normalized position vector.
697
-
698
- Raises
699
- ------
700
- ValueError
701
- If the position is empty, non-finite, or not one-dimensional.
702
- """
703
- normalized = np.asarray(position, dtype=np.float64)
704
 
705
  if normalized.ndim != 1:
706
  raise ValueError("Resource.position must be a one-dimensional vector.")
@@ -711,32 +472,12 @@ class Resource:
711
  if not np.all(np.isfinite(normalized)):
712
  raise ValueError("Resource.position must contain only finite values.")
713
 
714
- return normalized
715
 
716
  @staticmethod
717
  def _normalize_non_negative_float(value: Any, label: str) -> float:
718
- """
719
- Normalize a value into a finite non-negative float.
720
-
721
- Parameters
722
- ----------
723
- value:
724
- Candidate numeric value.
725
- label:
726
- Human-readable value label used in error messages.
727
-
728
- Returns
729
- -------
730
- float
731
- Normalized non-negative float.
732
-
733
- Raises
734
- ------
735
- TypeError
736
- If value is not numeric.
737
- ValueError
738
- If value is negative or non-finite.
739
- """
740
  if isinstance(value, bool) or not isinstance(value, Real):
741
  raise TypeError(f"{label} must be a real number.")
742
 
@@ -752,21 +493,8 @@ class Resource:
752
 
753
  @staticmethod
754
  def _validate_string_keys(mapping: Mapping[str, Any], label: str) -> None:
755
- """
756
- Validate that all mapping keys are strings.
757
-
758
- Parameters
759
- ----------
760
- mapping:
761
- Mapping to validate.
762
- label:
763
- Human-readable mapping label used in error messages.
764
-
765
- Raises
766
- ------
767
- TypeError
768
- If any mapping key is not a string.
769
- """
770
  for key in mapping:
771
  if not isinstance(key, str):
772
  raise TypeError(f"Resource.{label} keys must be strings.")
 
4
 
5
  Generic resource representation for WorldSmithAI.
6
 
 
7
  Resources are domain-agnostic quantities that agents and behaviors may observe,
8
  consume, regenerate, exchange, transform, or produce.
9
 
10
+ Examples:
 
11
  - food
12
  - grass
13
  - water
 
21
 
22
  The Resource class does not know what any resource type means. Domain semantics
23
  are supplied by the DSL, behaviors, policies, and world rules.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  """
25
 
26
  from __future__ import annotations
 
36
 
37
  logger = logging.getLogger(__name__)
38
 
39
+ PositionInput: TypeAlias = Sequence[float] | NDArray[np.float64] | None
40
  ResourceOperation: TypeAlias = Literal["deplete", "regenerate", "set_amount"]
41
 
42
 
43
  @dataclass(frozen=True, slots=True)
44
  class ResourceOperationResult:
45
+ """Structured result of a resource operation."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  resource_id: str
48
  resource_type: str
 
57
  metadata: Mapping[str, Any] = field(default_factory=dict)
58
 
59
  def __post_init__(self) -> None:
60
+ """Validate operation result fields."""
61
+
 
 
 
 
 
 
 
 
62
  numeric_fields = {
63
  "requested_amount": self.requested_amount,
64
  "actual_delta": self.actual_delta,
 
89
  object.__setattr__(self, "metadata", dict(self.metadata))
90
 
91
  def to_dict(self) -> dict[str, Any]:
92
+ """Convert the operation result into a JSON-friendly dictionary."""
93
+
 
 
 
 
 
 
94
  return {
95
  "resource_id": self.resource_id,
96
  "resource_type": self.resource_type,
 
108
 
109
  @dataclass(slots=True)
110
  class Resource:
111
+ """Generic domain-agnostic world resource.
112
+
113
+ Runtime compatibility:
114
+ - ``position`` is always normalized to a NumPy array.
115
+ - ``regeneration_rate`` and ``max_amount`` are accepted directly because
116
+ WorldFactory may pass or attach them.
117
+ - ``max_amount`` is mirrored into metadata as both ``max_amount`` and
118
+ ``capacity`` for compatibility with older code.
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  """
120
 
121
  id: str
122
  type: str
123
  amount: float
124
+ position: PositionInput = None
125
  metadata: dict[str, Any] = field(default_factory=dict)
126
+ regeneration_rate: float = 0.0
127
+ max_amount: float | None = None
128
+ dsl_spec: Any | None = None
129
 
130
  def __post_init__(self) -> None:
131
+ """Validate and normalize resource fields."""
132
+
 
 
 
 
 
 
 
 
133
  if not isinstance(self.id, str) or not self.id.strip():
134
  raise ValueError("Resource.id must be a non-empty string.")
135
 
136
  if not isinstance(self.type, str) or not self.type.strip():
137
  raise ValueError("Resource.type must be a non-empty string.")
138
 
139
+ if not isinstance(self.metadata, Mapping):
140
+ raise TypeError("Resource.metadata must be a mapping.")
141
+
142
  self.id = self.id.strip()
143
  self.type = self.type.strip()
144
  self.amount = self._normalize_non_negative_float(self.amount, "amount")
145
  self.position = self._normalize_position(self.position)
146
 
 
 
 
147
  self.metadata = dict(self.metadata)
148
  self._validate_string_keys(self.metadata, "metadata")
149
 
150
+ self.regeneration_rate = self._resolve_regeneration_rate(self.regeneration_rate)
151
+ self.max_amount = self._resolve_max_amount(self.max_amount)
152
+
153
+ self.metadata.setdefault("regeneration_rate", self.regeneration_rate)
154
+
155
+ if self.max_amount is not None:
156
+ self.metadata.setdefault("max_amount", self.max_amount)
157
+ self.metadata.setdefault("capacity", self.max_amount)
158
+
159
+ if self.capacity is not None and self.amount > self.capacity:
160
  logger.warning(
161
+ "Resource amount exceeds capacity: resource_id=%s amount=%s capacity=%s",
 
162
  self.id,
163
  self.amount,
164
+ self.capacity,
165
  )
166
 
167
  @property
168
  def capacity(self) -> float | None:
169
+ """Optional maximum amount for this resource."""
 
 
 
170
 
171
+ if self.max_amount is not None:
172
+ return self.max_amount
 
 
 
 
173
 
174
+ for key in ("capacity", "max_amount"):
175
+ if key in self.metadata and self.metadata[key] is not None:
176
+ return self._normalize_non_negative_float(
177
+ self.metadata[key],
178
+ f"metadata['{key}']",
179
+ )
 
180
 
181
+ return None
 
 
 
 
 
 
182
 
183
  @property
184
  def is_empty(self) -> bool:
185
+ """Return whether this resource has no available quantity."""
186
+
 
 
 
 
 
 
187
  return self.amount <= 0.0
188
 
189
  def regenerate(
 
193
  capacity: float | None = None,
194
  metadata: Mapping[str, Any] | None = None,
195
  ) -> ResourceOperationResult:
196
+ """Increase the resource amount."""
197
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  requested_amount = (
199
  self.regeneration_rate
200
  if amount is None
 
257
  allow_partial: bool = True,
258
  metadata: Mapping[str, Any] | None = None,
259
  ) -> ResourceOperationResult:
260
+ """Decrease the resource amount."""
261
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  requested_amount = self._normalize_non_negative_float(amount, "amount")
263
 
264
  if not isinstance(allow_partial, bool):
 
318
  *,
319
  metadata: Mapping[str, Any] | None = None,
320
  ) -> ResourceOperationResult:
321
+ """Set the resource amount directly."""
322
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  normalized_amount = self._normalize_non_negative_float(amount, "amount")
324
  previous_amount = self.amount
325
  self.amount = normalized_amount
 
336
  )
337
 
338
  def set_position(self, position: PositionInput) -> None:
339
+ """Set the resource position."""
340
+
 
 
 
 
 
 
 
 
 
 
 
341
  self.position = self._normalize_position(position)
342
 
343
  def distance_to(self, position: PositionInput) -> float:
344
+ """Compute Euclidean distance from this resource to a position."""
345
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  other_position = self._normalize_position(position)
347
 
348
  if self.position.shape != other_position.shape:
 
355
  return float(np.linalg.norm(self.position - other_position))
356
 
357
  def snapshot(self) -> dict[str, Any]:
358
+ """Return a JSON-friendly snapshot of this resource."""
359
+
 
 
 
 
 
 
360
  return {
361
  "id": self.id,
362
  "type": self.type,
363
  "amount": self.amount,
364
  "position": self.position.tolist(),
365
+ "regeneration_rate": self.regeneration_rate,
366
+ "max_amount": self.max_amount,
367
  "metadata": deepcopy(self.metadata),
368
  }
369
 
370
+ def to_dict(self) -> dict[str, Any]:
371
+ """Return a JSON-friendly dictionary representation."""
372
+
373
+ return self.snapshot()
374
+
375
  def copy(self) -> Resource:
376
+ """Return a deep copy of this resource."""
377
+
 
 
 
 
 
 
378
  return Resource(
379
  id=self.id,
380
  type=self.type,
381
  amount=self.amount,
382
  position=self.position.copy(),
383
  metadata=deepcopy(self.metadata),
384
+ regeneration_rate=self.regeneration_rate,
385
+ max_amount=self.max_amount,
386
+ dsl_spec=self.dsl_spec,
387
  )
388
 
389
  def _operation_result(
 
398
  message: str,
399
  metadata: Mapping[str, Any] | None,
400
  ) -> ResourceOperationResult:
401
+ """Build a normalized resource operation result."""
402
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  if metadata is not None and not isinstance(metadata, Mapping):
404
  raise TypeError("metadata must be a mapping when provided.")
405
 
 
418
  )
419
 
420
  def _resolve_capacity(self, capacity: float | None) -> float | None:
421
+ """Resolve operation-specific or resource-defined capacity."""
422
+
 
 
 
 
 
 
 
 
 
 
 
423
  if capacity is not None:
424
  return self._normalize_non_negative_float(capacity, "capacity")
425
 
426
  return self.capacity
427
 
428
+ def _resolve_regeneration_rate(self, value: Any) -> float:
429
+ """Resolve regeneration rate from explicit value or metadata."""
430
+
431
+ if value is not None and float(value) != 0.0:
432
+ return self._normalize_non_negative_float(value, "regeneration_rate")
433
+
434
+ if "regeneration_rate" in self.metadata and self.metadata["regeneration_rate"] is not None:
435
+ return self._normalize_non_negative_float(
436
+ self.metadata["regeneration_rate"],
437
+ "metadata['regeneration_rate']",
438
+ )
439
+
440
+ return self._normalize_non_negative_float(value or 0.0, "regeneration_rate")
441
+
442
+ def _resolve_max_amount(self, value: Any) -> float | None:
443
+ """Resolve max amount from explicit value or metadata aliases."""
444
+
445
+ if value is not None:
446
+ return self._normalize_non_negative_float(value, "max_amount")
447
+
448
+ for key in ("max_amount", "capacity"):
449
+ if key in self.metadata and self.metadata[key] is not None:
450
+ return self._normalize_non_negative_float(
451
+ self.metadata[key],
452
+ f"metadata['{key}']",
453
+ )
454
+
455
+ return None
 
 
 
456
 
457
  @staticmethod
458
  def _normalize_position(position: PositionInput) -> NDArray[np.float64]:
459
+ """Normalize a position input into a one-dimensional numpy float array."""
460
+
461
+ if position is None:
462
+ return np.zeros(2, dtype=np.float64)
463
+
464
+ normalized = np.asarray(position, dtype=np.float64).reshape(-1)
 
 
 
 
 
 
 
 
 
 
 
 
 
465
 
466
  if normalized.ndim != 1:
467
  raise ValueError("Resource.position must be a one-dimensional vector.")
 
472
  if not np.all(np.isfinite(normalized)):
473
  raise ValueError("Resource.position must contain only finite values.")
474
 
475
+ return normalized.astype(np.float64, copy=True)
476
 
477
  @staticmethod
478
  def _normalize_non_negative_float(value: Any, label: str) -> float:
479
+ """Normalize a value into a finite non-negative float."""
480
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
  if isinstance(value, bool) or not isinstance(value, Real):
482
  raise TypeError(f"{label} must be a real number.")
483
 
 
493
 
494
  @staticmethod
495
  def _validate_string_keys(mapping: Mapping[str, Any], label: str) -> None:
496
+ """Validate that all mapping keys are strings."""
497
+
 
 
 
 
 
 
 
 
 
 
 
 
 
498
  for key in mapping:
499
  if not isinstance(key, str):
500
  raise TypeError(f"Resource.{label} keys must be strings.")