|
128 | 128 | 'c-hexane': 'c-Hexane', |
129 | 129 | } |
130 | 130 |
|
| 131 | +# Approximate molar mass (g/mol) for common components, used to convert |
| 132 | +# mass flow (kg/h) to molar flow (kgmole/s) for the UniSim COM feed spec. |
| 133 | +_COMPONENT_MW = { |
| 134 | + 'nitrogen': 28.014, 'CO2': 44.010, 'methane': 16.043, |
| 135 | + 'ethane': 30.070, 'propane': 44.096, 'i-butane': 58.123, |
| 136 | + 'n-butane': 58.123, 'i-pentane': 72.150, 'n-pentane': 72.150, |
| 137 | + 'n-hexane': 86.177, 'n-heptane': 100.204, 'n-octane': 114.231, |
| 138 | + 'n-nonane': 128.258, 'nC10': 142.285, 'water': 18.015, |
| 139 | + 'hydrogen': 2.016, 'H2S': 34.081, 'oxygen': 31.998, |
| 140 | + 'argon': 39.948, 'helium': 4.003, 'CO': 28.010, |
| 141 | + 'methanol': 32.042, 'ethanol': 46.069, 'benzene': 78.114, |
| 142 | + 'toluene': 92.141, 'cyclohexane': 84.161, 'MEG': 62.068, |
| 143 | + 'TEG': 150.174, 'DEG': 106.120, 'ammonia': 17.031, |
| 144 | + 'SO2': 64.066, 'COS': 60.075, 'ethylene': 28.054, |
| 145 | + 'propene': 42.081, |
| 146 | +} |
| 147 | + |
131 | 148 | # NeqSim EOS model name -> UniSim property package name |
132 | 149 | NEQSIM_TO_UNISIM_PROPERTY_PACKAGE = { |
133 | 150 | 'SRK': 'SRK', |
@@ -587,6 +604,22 @@ def _to_internal_unit(value, unit_str): |
587 | 604 | return value |
588 | 605 | return None # unknown unit — skip Calculate fallback |
589 | 606 |
|
| 607 | + @staticmethod |
| 608 | + def _estimate_molar_mass(fluid: 'ParsedFluid') -> Optional[float]: |
| 609 | + """Estimate mixture molar mass (g/mol) from composition. |
| 610 | +
|
| 611 | + Returns None if any component MW is unknown. |
| 612 | + """ |
| 613 | + if not fluid or not fluid.components: |
| 614 | + return None |
| 615 | + mw_mix = 0.0 |
| 616 | + for comp, frac in fluid.components.items(): |
| 617 | + mw = _COMPONENT_MW.get(comp) |
| 618 | + if mw is None: |
| 619 | + return None # unknown component — can't compute |
| 620 | + mw_mix += frac * mw |
| 621 | + return mw_mix if mw_mix > 0 else None |
| 622 | + |
590 | 623 | def close(self): |
591 | 624 | """Close UniSim application.""" |
592 | 625 | if self._app is not None: |
@@ -654,30 +687,48 @@ def build_from_json(self, json_str: str, |
654 | 687 | except Exception as e: |
655 | 688 | self._warnings.append("Could not re-enable solver: %s" % e) |
656 | 689 |
|
657 | | - # Re-set feed stream flow rates (needs solver active + composition) |
| 690 | + # Re-set feed stream flow rates via MolarFlow.Calculate. |
| 691 | + # MolarFlow (internal unit: kgmole/s) is the most reliable way |
| 692 | + # to set flow in UniSim COM — MassFlow.Calculate persists on the |
| 693 | + # feed but doesn't propagate to downstream equipment. |
658 | 694 | for ps in systems: |
| 695 | + mw = self._estimate_molar_mass(ps.fluid) |
659 | 696 | for feed in ps.feed_streams: |
660 | 697 | if feed.flow_rate_kghr is not None: |
661 | 698 | ms = self._stream_objects.get(feed.name) |
662 | | - if ms is not None: |
| 699 | + if ms is None: |
| 700 | + continue |
| 701 | + # Try MolarFlow first (converts kg/h -> kgmole/s) |
| 702 | + flow_set = False |
| 703 | + if mw and mw > 0: |
| 704 | + kgmol_s = feed.flow_rate_kghr / 3600.0 / mw |
| 705 | + try: |
| 706 | + ms.MolarFlow.Calculate(kgmol_s) |
| 707 | + flow_set = True |
| 708 | + time.sleep(self.COM_DELAY_MEDIUM) |
| 709 | + logger.info( |
| 710 | + "Set flow on %s: %.0f kg/h " |
| 711 | + "(%.3f kgmol/s, MW=%.1f)", |
| 712 | + feed.name, feed.flow_rate_kghr, |
| 713 | + kgmol_s, mw) |
| 714 | + except Exception: |
| 715 | + pass |
| 716 | + # Fallback: MassFlow |
| 717 | + if not flow_set: |
663 | 718 | if not self._set_variable( |
664 | 719 | ms.MassFlow, feed.flow_rate_kghr, |
665 | 720 | 'kg/h', f"{feed.name}.F"): |
666 | 721 | self._warnings.append( |
667 | | - f"Could not re-set flow on '{feed.name}'") |
668 | | - else: |
669 | | - time.sleep(self.COM_DELAY_MEDIUM) |
670 | | - logger.info("Re-set flow on %s: %.0f kg/h", |
671 | | - feed.name, feed.flow_rate_kghr) |
| 722 | + f"Could not set flow on '{feed.name}'") |
672 | 723 |
|
673 | 724 | # Save if requested |
674 | 725 | if save_path: |
675 | 726 | abs_path = os.path.abspath(save_path) |
676 | 727 | try: |
677 | 728 | self._case.SaveAs(abs_path) |
678 | | - except Exception: |
679 | | - self._case.Save(abs_path) |
680 | | - logger.info(f"Saved UniSim case to: {abs_path}") |
| 729 | + logger.info("Saved UniSim case: %s", abs_path) |
| 730 | + except Exception as e: |
| 731 | + self._warnings.append(f"Could not save: {e}") |
681 | 732 |
|
682 | 733 | return self._case |
683 | 734 |
|
|
0 commit comments