RunFeemsSim 0.6.0__tar.gz → 0.7.0__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.
- {runfeemssim-0.6.0 → runfeemssim-0.7.0}/PKG-INFO +1 -1
- {runfeemssim-0.6.0 → runfeemssim-0.7.0}/RunFeemsSim/machinery_calculation.py +40 -16
- {runfeemssim-0.6.0 → runfeemssim-0.7.0}/RunFeemsSim.egg-info/PKG-INFO +1 -1
- {runfeemssim-0.6.0 → runfeemssim-0.7.0}/pyproject.toml +1 -1
- {runfeemssim-0.6.0 → runfeemssim-0.7.0}/tests/test_machinery_calculation.py +208 -0
- {runfeemssim-0.6.0 → runfeemssim-0.7.0}/LICENSE +0 -0
- {runfeemssim-0.6.0 → runfeemssim-0.7.0}/README.md +0 -0
- {runfeemssim-0.6.0 → runfeemssim-0.7.0}/RunFeemsSim/__init__.py +0 -0
- {runfeemssim-0.6.0 → runfeemssim-0.7.0}/RunFeemsSim/pms_basic.py +0 -0
- {runfeemssim-0.6.0 → runfeemssim-0.7.0}/RunFeemsSim.egg-info/SOURCES.txt +0 -0
- {runfeemssim-0.6.0 → runfeemssim-0.7.0}/RunFeemsSim.egg-info/dependency_links.txt +0 -0
- {runfeemssim-0.6.0 → runfeemssim-0.7.0}/RunFeemsSim.egg-info/requires.txt +0 -0
- {runfeemssim-0.6.0 → runfeemssim-0.7.0}/RunFeemsSim.egg-info/top_level.txt +0 -0
- {runfeemssim-0.6.0 → runfeemssim-0.7.0}/setup.cfg +0 -0
- {runfeemssim-0.6.0 → runfeemssim-0.7.0}/tests/test_pms_basic.py +0 -0
|
@@ -290,6 +290,25 @@ class MachineryCalculation:
|
|
|
290
290
|
fuel_option=fuel_option,
|
|
291
291
|
)
|
|
292
292
|
|
|
293
|
+
def _set_boiler_steam_demand(self, steam_demand_kg_per_h: Optional[np.ndarray]) -> None:
|
|
294
|
+
"""Set the boiler steam demand aligned to the simulation's integration intervals.
|
|
295
|
+
|
|
296
|
+
The number of integration points equals ``len(time_interval_s)``, which is one less
|
|
297
|
+
than the number of timestamps when the load is given as a timestamp series (the last
|
|
298
|
+
point is dropped, mirroring how the propulsion power is handled). The steam demand is
|
|
299
|
+
therefore truncated to the same number of points so the boiler fuel integration matches
|
|
300
|
+
the engine integration. No-op when the system has no boiler.
|
|
301
|
+
"""
|
|
302
|
+
if self.system_feems.boiler is None:
|
|
303
|
+
return
|
|
304
|
+
n_points = len(np.atleast_1d(self.electric_system.time_interval_s))
|
|
305
|
+
if steam_demand_kg_per_h is None:
|
|
306
|
+
self.system_feems.boiler.steam_out_kg_per_h = np.zeros(n_points)
|
|
307
|
+
else:
|
|
308
|
+
self.system_feems.boiler.steam_out_kg_per_h = (
|
|
309
|
+
np.asarray(steam_demand_kg_per_h, dtype=float)[:n_points]
|
|
310
|
+
)
|
|
311
|
+
|
|
293
312
|
def calculate_machinery_system_output_from_propulsion_power_time_series(
|
|
294
313
|
self,
|
|
295
314
|
*,
|
|
@@ -362,11 +381,7 @@ class MachineryCalculation:
|
|
|
362
381
|
propulsion_power_time_series=propulsion_power,
|
|
363
382
|
auxiliary_load_kw=auxiliary_power_kw,
|
|
364
383
|
)
|
|
365
|
-
|
|
366
|
-
self.system_feems.boiler.steam_out_kg_per_h = (
|
|
367
|
-
steam_demand_kg_per_h if steam_demand_kg_per_h is not None
|
|
368
|
-
else np.zeros(n_steps)
|
|
369
|
-
)
|
|
384
|
+
self._set_boiler_steam_demand(steam_demand_kg_per_h)
|
|
370
385
|
return self._run_simulation(
|
|
371
386
|
fuel_specified_by=fuel_specified_by,
|
|
372
387
|
ignore_power_balance=ignore_power_balance,
|
|
@@ -394,7 +409,10 @@ class MachineryCalculation:
|
|
|
394
409
|
fuel_option(FuelOption): The fuel option to be used in the simulation. If None, the
|
|
395
410
|
default fuel option in the system will be used.
|
|
396
411
|
steam_demand_kg_per_h: Steam demand time series (kg/h). Must have the same length as
|
|
397
|
-
the time series.
|
|
412
|
+
the time series. Takes precedence over the boiler_steam_demand_kg_per_h carried by
|
|
413
|
+
the time_series proto; if None, that proto value is used instead (per-instance, or
|
|
414
|
+
the top-level constant when the per-instance values are all zero). If both are
|
|
415
|
+
absent and a boiler is attached, demand is treated as zero.
|
|
398
416
|
|
|
399
417
|
Returns:
|
|
400
418
|
The result of the simulation. FEEMSResult or FEEMSResultForMachinery system.
|
|
@@ -423,11 +441,18 @@ class MachineryCalculation:
|
|
|
423
441
|
f"The length of steam_demand_kg_per_h ({len(steam_demand_kg_per_h)}) must match "
|
|
424
442
|
f"the time series ({n_steps})."
|
|
425
443
|
)
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
444
|
+
# The explicit steam_demand_kg_per_h argument takes precedence; otherwise fall back to the
|
|
445
|
+
# boiler_steam_demand_kg_per_h carried by the time-series proto (per-instance values, with a
|
|
446
|
+
# top-level constant fallback resolved by the converter).
|
|
447
|
+
steam_demand_from_proto = (
|
|
448
|
+
df["boiler_steam_demand_kg_per_h"].values
|
|
449
|
+
if "boiler_steam_demand_kg_per_h" in df.columns
|
|
450
|
+
else None
|
|
451
|
+
)
|
|
452
|
+
effective_steam_demand = (
|
|
453
|
+
steam_demand_kg_per_h if steam_demand_kg_per_h is not None else steam_demand_from_proto
|
|
454
|
+
)
|
|
455
|
+
self._set_boiler_steam_demand(effective_steam_demand)
|
|
431
456
|
return self._run_simulation(
|
|
432
457
|
fuel_specified_by=fuel_specified_by,
|
|
433
458
|
ignore_power_balance=ignore_power_balance,
|
|
@@ -480,11 +505,10 @@ class MachineryCalculation:
|
|
|
480
505
|
auxiliary_load_kw=auxiliary_power_kw,
|
|
481
506
|
time_is_given_as_interval=True,
|
|
482
507
|
)
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
)
|
|
508
|
+
# Time is given as intervals here, so there is no last-point truncation: the helper's
|
|
509
|
+
# alignment is a no-op (n_points == n_steps). Routed through the helper anyway to keep a
|
|
510
|
+
# single boiler-steam code path across all calculation methods.
|
|
511
|
+
self._set_boiler_steam_demand(steam_demand_kg_per_h)
|
|
488
512
|
return self._run_simulation(
|
|
489
513
|
fuel_specified_by=fuel_specified_by,
|
|
490
514
|
ignore_power_balance=ignore_power_balance,
|
|
@@ -13,6 +13,7 @@ from feems.components_model.component_mechanical import (
|
|
|
13
13
|
COGAS,
|
|
14
14
|
EngineMultiFuel,
|
|
15
15
|
FuelCharacteristics,
|
|
16
|
+
SteamBoiler,
|
|
16
17
|
)
|
|
17
18
|
from feems.fuel import FuelOrigin, FuelSpecifiedBy, TypeFuel
|
|
18
19
|
from feems.system_model import ElectricPowerSystem, FuelOption
|
|
@@ -20,6 +21,7 @@ from feems.types_for_feems import EngineCycleType, TypeComponent, TypePower
|
|
|
20
21
|
from MachSysS.convert_to_feems import convert_proto_propulsion_system_to_feems
|
|
21
22
|
from MachSysS.gymir_result_pb2 import (
|
|
22
23
|
GymirResult,
|
|
24
|
+
PropulsionPowerInstance,
|
|
23
25
|
PropulsionPowerInstanceForMultiplePropulsors,
|
|
24
26
|
SimulationInstance,
|
|
25
27
|
TimeSeriesResult,
|
|
@@ -461,3 +463,209 @@ def test_machinery_calculation_multifuel_coges():
|
|
|
461
463
|
rtol=1e-6,
|
|
462
464
|
), "Explicit LNG option should match default"
|
|
463
465
|
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
# --- Boiler steam demand carried through TimeSeriesResult ---------------------------------
|
|
469
|
+
#
|
|
470
|
+
# These tests cover boiler_steam_demand_kg_per_h on the TimeSeriesResult proto: the value is
|
|
471
|
+
# read back by the converter and applied to the system boiler, producing boiler fuel/CO2 that
|
|
472
|
+
# matches the equivalent table-mode path (calculate_..._from_propulsion_power_time_series with
|
|
473
|
+
# the same steam_demand_kg_per_h array). Both paths use a timestamp series, so the last point is
|
|
474
|
+
# dropped during integration; the helper that builds the proto and the table array shares the
|
|
475
|
+
# same epochs/steam values, so they stay aligned.
|
|
476
|
+
|
|
477
|
+
_BOILER_EPOCHS = [0, 60, 120, 180]
|
|
478
|
+
_BOILER_PROPULSION_KW = [1500.0, 1600.0, 1700.0, 1800.0]
|
|
479
|
+
_BOILER_AUX_KW = 200.0
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _make_steam_boiler() -> SteamBoiler:
|
|
483
|
+
flat_eff = np.array([[0.25, 0.85], [0.50, 0.85], [0.75, 0.85], [1.00, 0.85]])
|
|
484
|
+
return SteamBoiler(
|
|
485
|
+
name="test boiler",
|
|
486
|
+
rated_steam_production_kg_per_h=10_000.0,
|
|
487
|
+
working_pressure_barg=6.0,
|
|
488
|
+
thermal_efficiency_curve=flat_eff,
|
|
489
|
+
fuel_type=TypeFuel.HFO,
|
|
490
|
+
fuel_origin=FuelOrigin.FOSSIL,
|
|
491
|
+
feed_water_temperature_c=80.0,
|
|
492
|
+
uid="boiler-uid-ts",
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _build_boiler_machinery_calculation() -> MachineryCalculation:
|
|
497
|
+
"""Fresh MachineryCalculation (electric system) with a standalone boiler attached.
|
|
498
|
+
|
|
499
|
+
A fresh instance is required per run because boiler.steam_out_kg_per_h is mutated in place.
|
|
500
|
+
"""
|
|
501
|
+
package_dir = os.path.dirname(os.path.abspath(__file__))
|
|
502
|
+
path = os.path.join(package_dir, "system_proto.mss")
|
|
503
|
+
system_feems = convert_proto_propulsion_system_to_feems(retrieve_machinery_system_from_file(path))
|
|
504
|
+
system_feems.boiler = _make_steam_boiler()
|
|
505
|
+
return MachineryCalculation(feems_system=system_feems)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _table_mode_boiler_result(steam_demand_kg_per_h: np.ndarray):
|
|
509
|
+
series = pd.Series(index=_BOILER_EPOCHS, data=_BOILER_PROPULSION_KW)
|
|
510
|
+
return _build_boiler_machinery_calculation().calculate_machinery_system_output_from_propulsion_power_time_series(
|
|
511
|
+
propulsion_power=series,
|
|
512
|
+
auxiliary_power_kw=_BOILER_AUX_KW,
|
|
513
|
+
steam_demand_kg_per_h=steam_demand_kg_per_h,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def test_time_series_result_per_instance_boiler_steam_matches_table_mode():
|
|
518
|
+
"""AC1: per-timestep boiler_steam_demand_kg_per_h matches the table-mode path."""
|
|
519
|
+
steam = np.array([2000.0, 3000.0, 4000.0, 5000.0])
|
|
520
|
+
time_series = TimeSeriesResult(
|
|
521
|
+
propulsion_power_timeseries=[
|
|
522
|
+
PropulsionPowerInstance(
|
|
523
|
+
epoch_s=epoch,
|
|
524
|
+
propulsion_power_kw=prop,
|
|
525
|
+
auxiliary_power_kw=_BOILER_AUX_KW,
|
|
526
|
+
boiler_steam_demand_kg_per_h=s,
|
|
527
|
+
)
|
|
528
|
+
for epoch, prop, s in zip(_BOILER_EPOCHS, _BOILER_PROPULSION_KW, steam)
|
|
529
|
+
],
|
|
530
|
+
)
|
|
531
|
+
res_proto = _build_boiler_machinery_calculation().calculate_machinery_system_output_from_time_series_result(
|
|
532
|
+
time_series=time_series
|
|
533
|
+
)
|
|
534
|
+
res_table = _table_mode_boiler_result(steam)
|
|
535
|
+
|
|
536
|
+
# Boiler actually ran (guards against a silently-zero comparison).
|
|
537
|
+
assert res_proto.fuel_consumption_boiler_total.total_fuel_consumption > 0
|
|
538
|
+
assert res_proto.steam_production_boiler_total_kg > 0
|
|
539
|
+
# Independent absolute check so a bug in the shared steam-alignment helper can't be hidden by
|
|
540
|
+
# the proto==table comparison (both route through it). The last timestamp is dropped, so the
|
|
541
|
+
# integrated steam is [2000, 3000, 4000] kg/h over three 60 s intervals = 9000/3600*60 = 150 kg.
|
|
542
|
+
assert np.isclose(res_proto.steam_production_boiler_total_kg, 150.0)
|
|
543
|
+
assert np.isclose(
|
|
544
|
+
res_proto.fuel_consumption_boiler_total.total_fuel_consumption,
|
|
545
|
+
res_table.fuel_consumption_boiler_total.total_fuel_consumption,
|
|
546
|
+
)
|
|
547
|
+
assert np.isclose(
|
|
548
|
+
res_proto.fuel_consumption_boiler_total.get_total_co2_emissions().tank_to_wake_kg_or_gco2eq_per_gfuel,
|
|
549
|
+
res_table.fuel_consumption_boiler_total.get_total_co2_emissions().tank_to_wake_kg_or_gco2eq_per_gfuel,
|
|
550
|
+
)
|
|
551
|
+
assert np.isclose(
|
|
552
|
+
res_proto.steam_production_boiler_total_kg,
|
|
553
|
+
res_table.steam_production_boiler_total_kg,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def test_time_series_result_constant_boiler_steam_fallback():
|
|
558
|
+
"""AC2: all-zero per-instance values + non-zero top-level constant behaves as a constant."""
|
|
559
|
+
constant = 3500.0
|
|
560
|
+
time_series = TimeSeriesResult(
|
|
561
|
+
propulsion_power_timeseries=[
|
|
562
|
+
PropulsionPowerInstance(
|
|
563
|
+
epoch_s=epoch,
|
|
564
|
+
propulsion_power_kw=prop,
|
|
565
|
+
auxiliary_power_kw=_BOILER_AUX_KW,
|
|
566
|
+
# boiler_steam_demand_kg_per_h left at 0 on every instance
|
|
567
|
+
)
|
|
568
|
+
for epoch, prop in zip(_BOILER_EPOCHS, _BOILER_PROPULSION_KW)
|
|
569
|
+
],
|
|
570
|
+
boiler_steam_demand_kg_per_h=constant,
|
|
571
|
+
)
|
|
572
|
+
res_proto = _build_boiler_machinery_calculation().calculate_machinery_system_output_from_time_series_result(
|
|
573
|
+
time_series=time_series
|
|
574
|
+
)
|
|
575
|
+
res_table = _table_mode_boiler_result(np.full(len(_BOILER_EPOCHS), constant))
|
|
576
|
+
|
|
577
|
+
assert res_proto.fuel_consumption_boiler_total.total_fuel_consumption > 0
|
|
578
|
+
assert np.isclose(
|
|
579
|
+
res_proto.fuel_consumption_boiler_total.total_fuel_consumption,
|
|
580
|
+
res_table.fuel_consumption_boiler_total.total_fuel_consumption,
|
|
581
|
+
)
|
|
582
|
+
assert np.isclose(
|
|
583
|
+
res_proto.steam_production_boiler_total_kg,
|
|
584
|
+
res_table.steam_production_boiler_total_kg,
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def test_time_series_result_zero_boiler_steam_no_contribution():
|
|
589
|
+
"""AC3: all fields zero produces zero boiler contribution (no regression)."""
|
|
590
|
+
time_series = TimeSeriesResult(
|
|
591
|
+
propulsion_power_timeseries=[
|
|
592
|
+
PropulsionPowerInstance(
|
|
593
|
+
epoch_s=epoch,
|
|
594
|
+
propulsion_power_kw=prop,
|
|
595
|
+
auxiliary_power_kw=_BOILER_AUX_KW,
|
|
596
|
+
)
|
|
597
|
+
for epoch, prop in zip(_BOILER_EPOCHS, _BOILER_PROPULSION_KW)
|
|
598
|
+
],
|
|
599
|
+
)
|
|
600
|
+
res_proto = _build_boiler_machinery_calculation().calculate_machinery_system_output_from_time_series_result(
|
|
601
|
+
time_series=time_series
|
|
602
|
+
)
|
|
603
|
+
assert res_proto.fuel_consumption_boiler_total.total_fuel_consumption == 0.0
|
|
604
|
+
assert res_proto.steam_production_boiler_total_kg == 0.0
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def test_time_series_result_explicit_steam_demand_overrides_proto():
|
|
608
|
+
"""The explicit steam_demand_kg_per_h argument takes precedence over the proto value."""
|
|
609
|
+
explicit = np.array([2000.0, 3000.0, 4000.0, 5000.0])
|
|
610
|
+
# Proto carries a different (constant) value that must be ignored when the arg is given.
|
|
611
|
+
time_series = TimeSeriesResult(
|
|
612
|
+
propulsion_power_timeseries=[
|
|
613
|
+
PropulsionPowerInstance(
|
|
614
|
+
epoch_s=epoch,
|
|
615
|
+
propulsion_power_kw=prop,
|
|
616
|
+
auxiliary_power_kw=_BOILER_AUX_KW,
|
|
617
|
+
)
|
|
618
|
+
for epoch, prop in zip(_BOILER_EPOCHS, _BOILER_PROPULSION_KW)
|
|
619
|
+
],
|
|
620
|
+
boiler_steam_demand_kg_per_h=9999.0,
|
|
621
|
+
)
|
|
622
|
+
res_override = _build_boiler_machinery_calculation().calculate_machinery_system_output_from_time_series_result(
|
|
623
|
+
time_series=time_series,
|
|
624
|
+
steam_demand_kg_per_h=explicit,
|
|
625
|
+
)
|
|
626
|
+
res_table = _table_mode_boiler_result(explicit)
|
|
627
|
+
assert np.isclose(
|
|
628
|
+
res_override.fuel_consumption_boiler_total.total_fuel_consumption,
|
|
629
|
+
res_table.fuel_consumption_boiler_total.total_fuel_consumption,
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def test_time_series_result_multiple_propulsors_boiler_steam():
|
|
634
|
+
"""Per-instance boiler steam is carried through the multi-propulsor time-series path.
|
|
635
|
+
|
|
636
|
+
The boiler depends only on steam demand and the integration intervals, not on how propulsion
|
|
637
|
+
power is split across drives, so the boiler fuel/steam must match the single-propulsor
|
|
638
|
+
table-mode result for the same epochs and steam profile.
|
|
639
|
+
"""
|
|
640
|
+
steam = np.array([2000.0, 3000.0, 4000.0, 5000.0])
|
|
641
|
+
machinery_calculation = _build_boiler_machinery_calculation()
|
|
642
|
+
propulsor_names = [drive.name for drive in machinery_calculation.electric_system.propulsion_drives]
|
|
643
|
+
# Split the same total propulsion power equally across the drives.
|
|
644
|
+
per_drive_power = [[prop / len(propulsor_names)] * len(propulsor_names) for prop in _BOILER_PROPULSION_KW]
|
|
645
|
+
time_series = TimeSeriesResultForMultiplePropulsors(
|
|
646
|
+
propulsion_power_timeseries=[
|
|
647
|
+
PropulsionPowerInstanceForMultiplePropulsors(
|
|
648
|
+
epoch_s=epoch,
|
|
649
|
+
propulsion_power_kw=power_row,
|
|
650
|
+
auxiliary_power_kw=_BOILER_AUX_KW,
|
|
651
|
+
boiler_steam_demand_kg_per_h=s,
|
|
652
|
+
)
|
|
653
|
+
for epoch, power_row, s in zip(_BOILER_EPOCHS, per_drive_power, steam)
|
|
654
|
+
],
|
|
655
|
+
propulsor_names=propulsor_names,
|
|
656
|
+
)
|
|
657
|
+
res_proto = machinery_calculation.calculate_machinery_system_output_from_time_series_result(
|
|
658
|
+
time_series=time_series
|
|
659
|
+
)
|
|
660
|
+
res_table = _table_mode_boiler_result(steam)
|
|
661
|
+
|
|
662
|
+
assert res_proto.fuel_consumption_boiler_total.total_fuel_consumption > 0
|
|
663
|
+
assert np.isclose(res_proto.steam_production_boiler_total_kg, 150.0)
|
|
664
|
+
assert np.isclose(
|
|
665
|
+
res_proto.fuel_consumption_boiler_total.total_fuel_consumption,
|
|
666
|
+
res_table.fuel_consumption_boiler_total.total_fuel_consumption,
|
|
667
|
+
)
|
|
668
|
+
assert np.isclose(
|
|
669
|
+
res_proto.steam_production_boiler_total_kg,
|
|
670
|
+
res_table.steam_production_boiler_total_kg,
|
|
671
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|