Skip to content

Commit 30bb59d

Browse files
authored
Merge pull request #598 from EnergySystemsModellingLab/rc-assets
Store assets as reference counted values and rework `AssetPool` API
2 parents c814596 + 10cc711 commit 30bb59d

6 files changed

Lines changed: 149 additions & 157 deletions

File tree

src/asset.rs

Lines changed: 109 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,19 @@ use crate::region::RegionID;
66
use crate::time_slice::TimeSliceID;
77
use anyhow::{ensure, Context, Result};
88
use indexmap::IndexMap;
9-
use std::collections::HashSet;
10-
use std::ops::RangeInclusive;
9+
use std::hash::{Hash, Hasher};
10+
use std::ops::{Deref, RangeInclusive};
1111
use std::rc::Rc;
1212

1313
/// A unique identifier for an asset
1414
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
1515
pub struct AssetID(u32);
1616

17-
impl AssetID {
18-
/// Sentinel value assigned to [`Asset`]s when they are added to the pool
19-
pub const INVALID: AssetID = AssetID(u32::MAX);
20-
}
21-
2217
/// An asset controlled by an agent.
2318
#[derive(Clone, Debug, PartialEq)]
2419
pub struct Asset {
2520
/// A unique identifier for the asset
26-
pub id: AssetID,
21+
id: Option<AssetID>,
2722
/// A unique identifier for the agent
2823
pub agent_id: AgentID,
2924
/// The [`Process`] that this asset corresponds to
@@ -41,8 +36,8 @@ pub struct Asset {
4136
impl Asset {
4237
/// Create a new [`Asset`].
4338
///
44-
/// The `id` field is initially set to [`AssetID::INVALID`], but is changed to a unique value
45-
/// when the asset is stored in an [`AssetPool`].
39+
/// The `id` field is initially set to `None`, but is changed to a unique value when the asset
40+
/// is stored in an [`AssetPool`].
4641
pub fn new(
4742
agent_id: AgentID,
4843
process: Rc<Process>,
@@ -74,7 +69,7 @@ impl Asset {
7469
);
7570

7671
Ok(Self {
77-
id: AssetID::INVALID,
72+
id: None,
7873
agent_id,
7974
process,
8075
process_parameter,
@@ -139,10 +134,62 @@ impl Asset {
139134
}
140135
}
141136

137+
/// A wrapper around [`Asset`] for storing references in maps.
138+
///
139+
/// An [`AssetRef`] is guaranteed to have been commissioned at some point, though it may
140+
/// subsequently have been decommissioned.
141+
///
142+
/// [`AssetRef`]s must be created from `Rc<Asset>`s. If the asset has not been commissioned, this
143+
/// will panic.
144+
#[derive(Clone, Debug)]
145+
pub struct AssetRef(Rc<Asset>);
146+
147+
impl From<Rc<Asset>> for AssetRef {
148+
fn from(value: Rc<Asset>) -> Self {
149+
assert!(value.id.is_some());
150+
Self(value)
151+
}
152+
}
153+
154+
impl From<Asset> for AssetRef {
155+
fn from(value: Asset) -> Self {
156+
Self::from(Rc::new(value))
157+
}
158+
}
159+
160+
impl From<AssetRef> for Rc<Asset> {
161+
fn from(value: AssetRef) -> Self {
162+
value.0
163+
}
164+
}
165+
166+
impl Deref for AssetRef {
167+
type Target = Asset;
168+
169+
fn deref(&self) -> &Self::Target {
170+
&self.0
171+
}
172+
}
173+
174+
impl PartialEq for AssetRef {
175+
fn eq(&self, other: &Self) -> bool {
176+
self.0.id == other.0.id
177+
}
178+
}
179+
180+
impl Eq for AssetRef {}
181+
182+
impl Hash for AssetRef {
183+
/// Hash asset based purely on its ID
184+
fn hash<H: Hasher>(&self, state: &mut H) {
185+
self.0.id.unwrap().hash(state);
186+
}
187+
}
188+
142189
/// A pool of [`Asset`]s
143190
pub struct AssetPool {
144191
/// The pool of active assets
145-
active: Vec<Asset>,
192+
active: Vec<AssetRef>,
146193
/// Assets that have not yet been commissioned, sorted by commission year
147194
future: Vec<Asset>,
148195
/// Next available asset ID number
@@ -162,24 +209,6 @@ impl AssetPool {
162209
}
163210
}
164211

165-
/// Add an asset to the active pool (i.e. commission it immediately).
166-
///
167-
/// The asset's commission year is ignored by this function.
168-
///
169-
/// # Panics
170-
///
171-
/// Panics if the asset has already been commissioned.
172-
pub fn commission(&mut self, mut asset: Asset) {
173-
assert!(
174-
asset.id == AssetID::INVALID,
175-
"Asset has already been commissioned"
176-
);
177-
178-
asset.id = AssetID(self.next_id);
179-
self.next_id += 1;
180-
self.active.push(asset);
181-
}
182-
183212
/// Commission new assets for the specified milestone year from the input data
184213
pub fn commission_new(&mut self, year: u32) {
185214
// Count the number of assets to move
@@ -191,9 +220,9 @@ impl AssetPool {
191220

192221
// Move assets from future to active
193222
for mut asset in self.future.drain(0..count) {
194-
asset.id = AssetID(self.next_id);
223+
asset.id = Some(AssetID(self.next_id));
195224
self.next_id += 1;
196-
self.active.push(asset);
225+
self.active.push(asset.into());
197226
}
198227
}
199228

@@ -206,28 +235,28 @@ impl AssetPool {
206235
///
207236
/// # Returns
208237
///
209-
/// Reference to an [`Asset`] if found, else `None`. The asset may not be found if it has
210-
/// already been decommissioned.
211-
pub fn get(&self, id: AssetID) -> Option<&Asset> {
238+
/// An [`AssetRef`] if found, else `None`. The asset may not be found if it has already been
239+
/// decommissioned.
240+
pub fn get(&self, id: AssetID) -> Option<&AssetRef> {
212241
// The assets in `active` are in order of ID
213242
let idx = self
214243
.active
215-
.binary_search_by(|asset| asset.id.cmp(&id))
244+
.binary_search_by(|asset| asset.id.unwrap().cmp(&id))
216245
.ok()?;
217246

218247
Some(&self.active[idx])
219248
}
220249

221250
/// Iterate over active assets
222-
pub fn iter(&self) -> impl Iterator<Item = &Asset> {
251+
pub fn iter(&self) -> impl Iterator<Item = &AssetRef> {
223252
self.active.iter()
224253
}
225254

226255
/// Iterate over active assets for a particular region
227256
pub fn iter_for_region<'a>(
228257
&'a self,
229258
region_id: &'a RegionID,
230-
) -> impl Iterator<Item = &'a Asset> {
259+
) -> impl Iterator<Item = &'a AssetRef> {
231260
self.iter().filter(|asset| asset.region_id == *region_id)
232261
}
233262

@@ -237,7 +266,7 @@ impl AssetPool {
237266
&'a self,
238267
region_id: &'a RegionID,
239268
commodity_id: &'a CommodityID,
240-
) -> impl Iterator<Item = &'a Asset> {
269+
) -> impl Iterator<Item = &'a AssetRef> {
241270
self.iter_for_region(region_id).filter(|asset| {
242271
asset.process.contains_commodity_flow(
243272
commodity_id,
@@ -247,20 +276,24 @@ impl AssetPool {
247276
})
248277
}
249278

250-
/// Retain all assets whose IDs are in `assets_to_keep`.
251-
///
252-
/// Other assets will be decommissioned. Assets which have not yet been commissioned will not be
253-
/// affected.
254-
pub fn retain(&mut self, assets_to_keep: &HashSet<AssetID>) {
255-
// Sanity check: all IDs should be valid. As this check is slow, only do it for debug
256-
// builds.
257-
debug_assert!(
258-
assets_to_keep.iter().all(|id| self.get(*id).is_some()),
259-
"One or more asset IDs were invalid"
260-
);
279+
/// Replace the active pool with new and/or already commissioned assets
280+
pub fn replace_active_pool<I>(&mut self, assets: I)
281+
where
282+
I: IntoIterator<Item = Rc<Asset>>,
283+
{
284+
let new_pool = assets.into_iter().map(|mut asset| {
285+
if asset.id.is_none() {
286+
// Asset is newly created from process so we need to assign an ID
287+
let asset = Rc::make_mut(&mut asset);
288+
asset.id = Some(AssetID(self.next_id));
289+
self.next_id += 1;
290+
}
291+
292+
asset.into()
293+
});
261294

262-
self.active
263-
.retain(|asset| assets_to_keep.contains(&asset.id));
295+
self.active.clear();
296+
self.active.extend(new_pool);
264297
}
265298
}
266299

@@ -273,6 +306,7 @@ mod tests {
273306
};
274307
use itertools::{assert_equal, Itertools};
275308
use rstest::{fixture, rstest};
309+
use std::collections::HashSet;
276310
use std::iter;
277311
use std::ops::RangeInclusive;
278312

@@ -285,7 +319,7 @@ mod tests {
285319
let agent_id = AgentID("agent1".into());
286320
let region_id = RegionID("GBR".into());
287321
let asset = Asset::new(agent_id, process.into(), region_id, capacity, 2015).unwrap();
288-
assert!(asset.id == AssetID::INVALID);
322+
assert!(asset.id.is_none());
289323
}
290324

291325
#[rstest]
@@ -443,24 +477,15 @@ mod tests {
443477
assert!(asset_pool.iter().next().is_none()); // no active assets
444478
}
445479

446-
#[rstest]
447-
fn test_asset_pool_commission(mut asset_pool: AssetPool, process: Process) {
448-
let mut asset =
449-
Asset::new("agent2".into(), process.into(), "USA".into(), 100.0, 2015).unwrap();
450-
asset_pool.commission(asset.clone());
451-
asset.id = AssetID(0);
452-
assert_equal(asset_pool.iter(), iter::once(&asset));
453-
}
454-
455480
#[rstest]
456481
fn test_asset_pool_decommission_old(mut asset_pool: AssetPool) {
457482
asset_pool.commission_new(2020);
458-
assert!(asset_pool.active.len() == 2);
483+
assert_eq!(asset_pool.active.len(), 2);
459484
asset_pool.decommission_old(2020); // should decommission first asset (lifetime == 5)
460-
assert!(asset_pool.active.len() == 1);
485+
assert_eq!(asset_pool.active.len(), 1);
461486
assert_eq!(asset_pool.active[0].commission_year, 2020);
462487
asset_pool.decommission_old(2022); // nothing to decommission
463-
assert!(asset_pool.active.len() == 1);
488+
assert_eq!(asset_pool.active.len(), 1);
464489
assert_eq!(asset_pool.active[0].commission_year, 2020);
465490
asset_pool.decommission_old(2025); // should decommission second asset
466491
assert!(asset_pool.active.is_empty());
@@ -474,26 +499,30 @@ mod tests {
474499
}
475500

476501
#[rstest]
477-
fn test_asset_pool_retain1(mut asset_pool: AssetPool) {
478-
// Even though we are retaining no assets, none have been commissioned so the asset pool
479-
// should not be changed
480-
asset_pool.retain(&HashSet::new());
481-
assert!(asset_pool.active.is_empty());
482-
483-
// Decommission all active assets
484-
asset_pool.commission_new(2010); // Commission first asset
502+
fn test_asset_pool_replace_active_pool_existing(mut asset_pool: AssetPool) {
503+
asset_pool.commission_new(2020);
504+
assert_eq!(asset_pool.active.len(), 2);
505+
asset_pool.replace_active_pool(iter::once(asset_pool.active[1].clone().into()));
485506
assert_eq!(asset_pool.active.len(), 1);
486-
asset_pool.retain(&HashSet::new());
487-
assert!(asset_pool.active.is_empty());
507+
assert_eq!(asset_pool.active[0].id, Some(AssetID(1)));
488508
}
489509

490510
#[rstest]
491-
fn test_asset_pool_retain2(mut asset_pool: AssetPool) {
492-
// Decommission single asset
493-
asset_pool.commission_new(2020); // Commission all assets
511+
fn test_asset_pool_replace_active_pool_new_asset(mut asset_pool: AssetPool, process: Process) {
512+
let asset = Asset::new(
513+
"some_other_agent".into(),
514+
process.into(),
515+
"GBR".into(),
516+
2.0,
517+
2010,
518+
)
519+
.unwrap();
520+
521+
asset_pool.commission_new(2020);
494522
assert_eq!(asset_pool.active.len(), 2);
495-
asset_pool.retain(&iter::once(AssetID(1)).collect());
523+
asset_pool.replace_active_pool(iter::once(asset.into()));
496524
assert_eq!(asset_pool.active.len(), 1);
497-
assert_eq!(asset_pool.active[0].id, AssetID(1));
525+
assert_eq!(asset_pool.active[0].id, Some(AssetID(2)));
526+
assert_eq!(asset_pool.active[0].agent_id, "some_other_agent".into());
498527
}
499528
}

0 commit comments

Comments
 (0)