Change Log

This document records notable changes to QSDsan. We aim to follow Semantic Versioning.

1.5.3

  • Added runnable docstring examples to the Excretion, PitLatrine, and Trucking sanitation units (previously their Examples sections only linked to EXPOsan), and a supporting qsdsan.utils.create_example_sanitation_components() helper that builds the excreta/nutrient component set those units need (it is not part of Components.load_default). Also fixed the Incinerator docstring example, whose Component.from_chemical('S_CO2', search_ID='CO2', ...) call passed a search_ID keyword that from_chemical does not accept (it was silently swallowed by **data, so the chemical defaulted to the ID 'S_CO2' and the lookup raised LookupError); it now uses the supported chemical='CO2' form. These examples run under pytest --doctest-modules (already exercised in CI via the pytest.ini --doctest-modules default).

  • Extended the docstring-example coverage to more built-in unit operations. Added runnable Examples to the sanitation/static units MURT, UDDT, CropApplication, Lagoon, SepticTank, DryingBed, LiquidTreatmentBed, Screening, Sedimentation, and SludgePasteurization, and to the wastewater-treatment units ActivatedSludgeProcess, BiogasCombustion, CombinedHeatPower, AnaerobicBaffledReactor, AnaerobicDigestion, SludgeThickening, BeltThickener, and SludgeCentrifuge. Added qsdsan.utils.create_example_wwt_components() (a lumped COD-based component set with the active_biomass/inert_biomass/substrates groups those units expect) and extended qsdsan.utils.create_example_sanitation_components() with Struvite, HAP, MagnesiumHydroxide, and LPG so the struvite-recovery and pasteurization examples run. The BioSTEAM-wrapper units in qsdsan.unit_operations.bst now all link to the upstream BioSTEAM API documentation from their See Also sections (the Tank family gained the links the others already had). All examples run under pytest --doctest-modules.

  • Added a new section on operational flexibility to tutorials/6_System.ipynb (section 4): an AgileSystem example that runs an example treatment plant in two seasonal operation modes and compiles one annualized TEA and LCA across them, each mode weighted by its operating hours. Tutorials 7 (TEA) and 8 (LCA) gained cross-references to it.

  • Added a new section on controlling recycle convergence to tutorials/6_System.ipynb (section 5): how to read a non-convergence report, the System tolerance settings (molar_tolerance, relative_molar_tolerance, temperature_tolerance, relative_temperature_tolerance), maxiter, and the acceleration method with their live defaults, and what to adjust when a recycle loop will not converge, demonstrated on the tutorial’s own example system. The §1 recycle-convergence note was corrected to reference relative_molar_tolerance and temperature_tolerance (the prior rmol_tolerance is not a real attribute) and now links to the new section. The recycle-convergence pitfall in tutorials/14_Modeling_Notes_and_Pitfalls.ipynb (§2.2) deep-links to it, names maxiter, and had its fix example corrected (the molar_tolerance/temperature_tolerance values shown were tighter than the defaults, contradicting the “loosen” advice; they now sit above the defaults). Tutorial 11 (§1.1) notes that atol/rtol (and print_msg) forward to scipy.integrate.solve_ivp for dynamic-integration accuracy and diagnostics.

  • Fixed TEA so that net present value and other replacement-cost calculations work for an AgileSystem. The per-unit capital objects of an agile system expose their equipment lifetime as equipment_lifetime (a single unit uses lifetime); the replacement-cost loop now reads the correct attribute instead of raising AttributeError.

  • Fixed equipment and construction lifetimes so they reach equipment_lifetime, the attribute that TEA and LCA actually read. SanUnit reset equipment_lifetime to {} at construction (discarding BioSTEAM’s copy of the class-level _default_equipment_lifetime), and add_equipment_design, add_construction, and several units (ActivatedSludgeProcess, AnaerobicReactor, MembraneBioreactor, PolishingFilter, WWTpump) wrote lifetimes into _default_equipment_lifetime, which nothing read back. As a result every built-in equipment/construction lifetime was silently dropped, so equipment replacement costs (TEA) and construction-replacement impacts (LCA) were never charged. SanUnit now preserves the class-level lifetimes and routes equipment/construction lifetimes into the instance equipment_lifetime (no longer mutating the shared class dict). This changes TEA/LCA results for systems whose equipment lifetime is shorter than the project lifetime (the replacement is now charged); on the EXPOsan test systems the shift is within existing tolerances (for example, ~0.6% on a pump-bearing reclaimer TEA). Added tests/test_equipment_lifetime.py. Tutorial 7 (section 1.4) and the lifetime docstrings now use and recommend lifetime (the alias for the BioSTEAM equipment_lifetime attribute) as the name for a unit’s equipment lifetime.

  • Added tutorials/14_Modeling_Notes_and_Pitfalls.ipynb covering common modeling surprises across streams/components, unit design, TEA/LCA, and behavior inherited from BioSTEAM. Extended tutorial 5 §1.1 with simulate() / _summary call-graph mechanics.

  • Expanded process-specification coverage in the tutorials. Tutorial 5 §3.7 now explains what run=True does (re-running _run so a spec-changed parameter reaches the outlets) and the degrees-of-freedom intuition (one adjustable knob per target). Its example AerobicReactor3 previously cached its reaction at __init__ and ignored later conversion changes, so the spec was a silent no-op; _run now syncs aerobic_rxn.X to the live conversion, and the example reports the resulting effluent COD so the effect is visible. Tutorial 6 gains a new §3 on system-level specifications (System.add_specification and add_bounded_numerical_specification), driving a parameter until a system outlet hits a target, with cross-references between the two sections.

  • Deepened the cost and design coverage in the SanUnit (advanced) tutorial (notebook 5). The cost decorator section now documents the lb/ub/N parallel-unit behavior, the lifetime and custom f arguments, and stacking multiple @cost decorators on one class. A new §3.3 on auxiliary units shows how a unit can own another unit operation (e.g., a HXutility heat exchanger) via the auxiliary_unit_names class attribute, with its design, cost, and utilities rolling up into the parent automatically; the Equipment section now contrasts the two mechanisms. Later §3 subsections were renumbered accordingly.

  • Restructured the FAQ into a four-page faq/ subdirectory (errors, tips, styling, ai_assisted_coding), absorbing the previously standalone AI-assisted coding page from the tutorials section. The old FAQ.html bookmark redirects to the new index.

  • Fixed DynamicInfluent for non-cyclic input data: when the first and last rows of the data file differed, the constructor entered a branch that called df.append(y_end, ignore_index=True) to pad the time series with a phantom final point. The line was always a silent no-op (DataFrame.append returned a new frame rather than modifying in place; the result was discarded), and starting with pandas 2.0 append was removed entirely, so the same call now raises AttributeError: 'DataFrame' object has no attribute 'append' and the constructor fails outright. The branch now uses pd.concat and reassigns self._data so the phantom point is actually appended; self._t_end and the interpolants pick it up. Default-file behavior is unchanged because the shipped _inf_dry_2006.tsv is cyclic (first row equals last row) and the branch is skipped.

  • In EXPOsan, restored the documented exposan.bsm1.cmps / components / asm module attributes (the canonical handles used by the dynamic-simulation tutorial). bsm1.load() declared the three names global but the matching assignment was commented out, so after a fresh load() they stayed None and qs.set_thermo(bsm1.cmps) raised TypeError: 'NoneType' object is not iterable. The assignments (cmps = components = O1.components, asm = O1.suspended_growth_model) are now active.

  • Added a public dynamic_parameters accessor on Process and CompiledProcesses that returns the dictionary of attached DynamicParameter objects (keyed by symbol; empty when all parameters are static). This mirrors the existing parameters property, so users no longer have to reach into the private _dyn_params attribute to inspect what dynamic parameters a process has. Added test coverage in tests/test_process.py.

  • load_from_file() now supports per-process conservation rules through three composable forms. Previously every process in a batch-built model shared one conserved_for tuple, forcing users to fall back to per-process manual construction whenever (e.g.) decay shouldn’t be required to conserve carbon. conserved_for is now keyword-only and required (no default value) and accepts: a tuple (uniform — applied to every process, unchanged from before), a dict keyed by process ID (per-process rules; IDs absent from the dict fall back to ('COD', 'N', 'P', 'charge')), or None (defers to a new optional conserved_for column in the data file, which carries comma-separated material names per row; empty cells mean no enforcement). The file column is consumed during parsing and does not appear in the stoichiometry matrix. Resolution order, highest first: kwarg-dict entry for the listed process ID → kwarg-tuple (uniform) → file column → default tuple. Backward incompatibility: existing callers that omitted conserved_for (relying on the previous tuple default) will now raise TypeError and must pass an explicit value. The tutorial’s example datafile is now split into _bkm.csv (no column, demonstrates the tuple-then-dict workflow) and _bkm_with_conserved.csv (column-bearing, demonstrates conserved_for=None). Added test coverage in tests/test_process.py.

  • Fixed the SanUnit-mixin plumbing on eight bst-namespace wrappers (BinaryDistillation, ShortcutColumn, MESHDistillation, AdiabaticMultiStageVLEColumn, Flash, IsothermalCompressor, ProcessWaterCenter, HeatExchangerNetwork) whose MRO put the BioSTEAM parent ahead of SanUnit (or, for HeatExchangerNetwork, explicitly overwrote __init__). SanUnit.__init__ never ran, so the LCA/add-on attributes (construction, transportation, equipment, add_OPEX, uptime_ratio, lifetime, include_construction) silently did not exist on instances of these classes, and kwargs like lifetime= or add_OPEX= were dropped. Each affected wrapper now defines an __init__ that calls the BioSTEAM parent and then self._init_sanunit_addons(...) (a new method extracted from SanUnit.__init__ so the mixin-install logic has a single source of truth). Added a three-layer test for every bst class in tests/test_bst_units.py (smoke + parity-where-feasible + add-on persistence through simulate()) so any future mixin regression fails CI rather than silently propagating.

  • API: harmonized the six plotting helpers in qsdsan.stats so users can place a plot on a chosen axis and forward styling kwargs uniformly. Every plot_* now accepts ax=None and **plot_kws (forwarded to the primary underlying plotting call, i.e., the seaborn function or the matplotlib scatter/bar/errorbar). On plot_uncertainties, the existing center_kws/margin_kws are kept because the 2D seaborn.JointGrid has two axes; plot_kws merges into center_kws (center_kws wins on conflict). On plot_uncertainties 2D plots and on plot_correlations, ax= is documented as ignored because the underlying calls build their own figure. plot_correlations’s old **kwargs was renamed to **plot_kws; existing callers that pass named kwargs (every caller surveyed in EXPOsan and the tutorials) are unaffected. Also fixed a latent bug in the bar-only branch of plot_sobol_results that was dropping the user-supplied ax=.

  • Made qsdsan a more self-contained namespace so users typically don’t need to import biosteam/import thermosteam directly. Newly re-exported at the top level: settings, preferences, stream_utility_prices, Thermo, UtilityAgent, Facility, AgileSystem, get_OSBL, MissingStream, and the report submodule. qsdsan.unit_operations.bst now also surfaces the BioSTEAM units QSDsan does not customize (IsenthalpicValve, Stripper, MolecularSieve, BatchBioreactor, VacuumSystem, Boiler, BoilerTurbogenerator, ChilledWaterPackage, CoolingTower, SolidsCentrifuge), and qsdsan.utils now re-exports rho_to_V, V_to_rho, the @cost decorator, and var_columns/var_indices. A new Public API documentation page lists the full surface, and tests/test_public_api.py asserts each re-export still matches its BioSTEAM/Thermosteam source (so an upstream rename fails loudly in CI rather than reaching a user).

  • Fixed get_unit_annualized_equipment_cost() (used by annualized_equipment_cost): when a unit declared per-equipment lifetimes through an equipment_lifetime dict, the loop variable shadowed the cost accumulator, so the annualized equipment cost was computed incorrectly (the running sum was dropped and the bare-module factors mis-applied). It now iterates each unit’s installed_costs with a separate variable, annualizing every item over its own lifetime (falling back to the unit or TEA lifetime). Added tests/test_tea.py covering this case along with the other TEA cost metrics.

  • Fixed qsdsan.utils.indices.ChemPPI_by_year: the 2021 and 2022 entries had been mistakenly populated with CEPCI values (708.8, 816.0). They have been removed, so the chemical-PPI series now ends at 2020 until authentic values are added.

  • qsdsan.utils now thinly wraps BioSTEAM’s @cost decorator so it also accepts CEPCI as an alias for the reference cost-index argument CE (e.g., @cost(..., CEPCI=522)), for consistency with qsdsan.CEPCI/qsdsan.CEPCI_by_year. BioSTEAM’s CE keyword continues to work unchanged.

  • Fixed compatibility with newer Thermosteam releases that make Chemical.MW a read-only property: Component creation assigned self.MW = 1. as a placeholder for components without a formula or molecular weight, which raised AttributeError: cannot set molecular weight. The placeholder is now set via qsdsan._compat.set_chemical_MW, which uses the public MW setter when it is writable and falls back to Thermosteam’s internal constant-reset helper when it is read-only (so it works on both old and new Thermosteam, >=0.53.4). This unblocked Components.load_default and therefore nearly every system build.

  • Unified report generation across System, TEA, and LCA: calling save_report on any of the three now produces the same Excel workbook (system design, costs, and utilities, plus the QSDsan LCA tables on an LCA sheet whenever the system has an LCA). This works by wrapping BioSTEAM’s System.save_report at import (qsdsan.System is BioSTEAM’s System, and TEA.save_report already delegates to it); systems without an LCA behave exactly as before. save_report() now delegates to the system report, and its default filename changed from {system.ID}_lca.xlsx to {system.ID}_report.xlsx; for the LCA tables alone, use get_impact_table(). Added tests/test_lca.py covering the unified output. In EXPOsan, bwaise’s save_reports no longer writes a separate LCA file.

  • Added qsdsan.utils.create_example_treatment_systems, which builds the aerobic and anaerobic wastewater treatment systems shared by the TEA and LCA tutorials (a compact, realistic substrate for techno-economic and life cycle analyses). The LCA tutorial now uses it instead of the bwaise EXPOsan system.

  • Added section 5.6 (Cost and environmental trade-off) to tutorials/8_LCA.ipynb: a single table lining up the break-even treatment fee (cost per m³, from tutorial 7) against the GWP per m³ for the two example plants, so the cost-versus-carbon trade-off is visible in one place, with a note on comparing systems on the same functional unit and system boundary.

  • Added a time_frame argument to the LCA results methods (get_total_impacts(), get_unit_impacts(), get_impact_table(), get_allocated_impacts()/get_allocated_impact_table(), and save_report). It normalizes the results to a chosen time frame: 'lifetime' (or 'all', the default), 'yr' (equivalent to annual=True), 'month', 'week', 'day', or 'hr'. The existing annual flag is kept as a backward-compatible alias (annual=Truetime_frame='yr'); time_frame takes precedence when both are given.

  • Added get_normalized_impacts(), which expresses impacts per functional unit (per kg, per m³, or per MJ) of one or more reference streams, the LCA counterpart to TEA.solve_price. By default the total impacts are divided by the streams’ combined throughput; pass allocate_by to normalize the impacts allocated to those streams instead.

  • Fixed get_unit_impacts(): stream impacts were added twice (the accumulator was initialized to the stream-impact dict and then the stream impacts were added again), overstating the result. Each category is now counted once.

  • Fixed get_allocated_impacts() (and the new get_allocated_impact_table()): passing a function for allocate_by (a documented option) raised TypeError because callable was tested after iter(allocate_by), which fails on a non-iterable function. callable is now checked first, so a function returning the allocation ratios works; an invalid allocate_by now raises a clear ValueError.

  • Added get_allocated_impact_table(), the tabular counterpart of get_allocated_impacts() (impacts allocated to two or more streams, one row per stream, indicator columns, plus an 'Allocation factor' column). The unified report can include it as an opt-in 'LCA allocation' sheet by passing lca_allocate_streams (and optionally lca_allocate_by) to save_report. The shared allocation-ratio logic was factored into a private helper used by both methods.

  • Fixed get_impact_table(): the per-category Sum row was written via pandas chained assignment, which silently no-ops under Copy-on-Write (pandas ≥ 2.x), so the total row came out blank and ChainedAssignmentError warnings were emitted. The row totals are now assigned with .loc.

  • Fixed get_impact_table() for empty 'Stream' and 'Other' categories: building the total row on an empty table assigned a float into a column pandas 3.0 had inferred as the string dtype, raising TypeError: Invalid value ... for dtype 'str'. These categories now return a 'No ...-related impacts.' message when empty (matching the existing 'Construction'/'Transportation' behavior), which is also skipped when writing the report.

  • Fixed add_other_item(): when given a string ID with no matching ImpactItem, the raised ValueError reported None (the failed-lookup result) instead of the requested ID. It now names the missing ID.

  • Testing: added a conftest.py autouse fixture that resets the LCA registries and auto-ID ticket counters before each doctest, so the LCA doctests no longer leak state into one another (e.g., an indicator alias or auto-generated ID carrying over) when the modules run together.

  • In EXPOsan, replaced the per-test clear_lca_registries() calls (deprecated) with an autouse conftest.py fixture that resets the LCA registries before each test.

  • Documentation: expanded the tutorials with new sections on defining component groups (Components.define_group), unit specifications (add_specification), and inferring a System from a list of units (System.from_units), plus notes on flowsheet retrieval, recycle convergence, and exporting results.

  • Documentation: fixed dark-mode rendering of DataFrame tables and stderr (warning) output, standardized .diagram usage with a cross-reference between the System and Dynamic Simulation tutorials, and repaired a broken hyperlink.

  • Documentation: concluded the topical-tutorial polish pass. Tutorial 12 (Anaerobic Digestion Model No. 1) gained a new §1 with hand-authored light/dark SVG pairs of the ADM1 reaction network and DAE structure (under docs/source/images/adm1/), cross-tutorial connections to tutorials 10 and 11, and a data-grounded §3 discussion of the t = 10 d start-up transient (acidified plateau, biomass split into growing vs. declining groups). Tutorial 10 (Process) also gained a short Petersen-matrix paragraph that tutorial 12 references. Tutorial 13 (Process Modeling 101) was repositioned as the flowsheet-scale companion to tutorials 10-12: §1 was rewritten, cross-tutorial connections were added at the top of each §2 subsection (Component → tutorial 2, WasteStream → tutorial 3, Process → tutorial 10, SanUnit → tutorials 4/5/11, System → tutorials 6/11), §3 gained prose on the integrator-plus-recycle-convergence loop and on interpreting the steady-state output (SRT ≈ 10 d at the design point; nitrification / denitrification signatures; KLa-driven dissolved oxygen), and the prerequisites and learning objectives were updated to match. Tutorial 13’s seven embedded image attachments were extracted to assets/tutorial_13/ as snake_case files, clearing a Sphinx image.not_readable warning that the URL-encoded %20 paths produced. Two Run ?su.X runtime-help prompts in tutorial 13 were replaced with documentation links into the autodoc-generated API pages (with the ?su.X form retained as a parenthetical fallback).

1.5.2

  • Fixed packaging: qsdsan/units_of_measure.txt (the pint unit-definition file loaded at import) was not declared in package-data, so non-editable installs (wheels) omitted it and import qsdsan raised FileNotFoundError. It is now included in the distributed package.

  • Fixed copy_like(): when called with copy_price=True or copy_impact_item=True, the price/impact item were copied in the wrong direction (overwriting the source stream and leaving the target unchanged). They are now correctly copied from the source into the target.

  • Added doctest examples to copy(), copy_like(), and copy_flow() documenting what each method copies (flows, temperature/pressure, price, and impact item).

  • Added .. warning:: notes to pH and SAlk clarifying that these are not calculated from stream composition (no acid-base model yet) and should be treated as user-provided inputs.

  • Fixed HXutility: its _units dictionary was inadvertently set to None (it was assigned the return value of dict.update, which is always None), which broke results() with an AttributeError and mutated BioSTEAM’s shared _units. The unit-of-measure entries are now built with a dict merge.

  • Re-exported the Thermosteam reaction classes (Reaction, ReactionItem, ReactionSet, ParallelReaction, SeriesReaction, ReactionSystem, and the Rxn/RxnI/RxnS/PRxn/SRxn/RxnSys aliases) from the top-level qsdsan namespace, so they can be imported with from qsdsan import Reaction instead of reaching into BioSTEAM/Thermosteam.

  • Added qsdsan.CEPCI, a settable view of the global Chemical Engineering Plant Cost Index (which BioSTEAM abbreviates as CE), so the costing index can be read and set (e.g., qsdsan.CEPCI = qsdsan.CEPCI_by_year[2023]) without importing biosteam.

  • Fixed TEA: its CEPCI argument defaulted to bst.CE, which Python binds once at import time (freezing it at 567.5). Every TEA created without an explicit CEPCI therefore reset the global cost index, silently overriding a deliberately set qsdsan.CEPCI/bst.CE. CEPCI now defaults to None (the current index is left untouched), and a provided CEPCI is applied before simulation so it actually affects costing.

  • CEPCI_by_year now returns qsdsan.CEPCI_by_year (was BioSTEAM’s table) for consistency, and qsdsan.CEPCI_by_year now merges in BioSTEAM’s design_tools.CEPCI_by_year years not already present (e.g., pre-1990), with qsdsan’s more precise values taking precedence on overlapping years. QSDsan’s built-in hydroprocessing/hydrothermal units now source their @cost reference indices from qsdsan.CEPCI_by_year as well (a sub-0.1% cost change from the more precise values).

  • In EXPOsan, systems that set the global cost index (htl and saf) now do so via qsdsan.CEPCI = ... instead of bst.CE = ..., for consistency with the new qsdsan.CEPCI handle (functionally identical, since qsdsan.CEPCI is a live view of bst.CE).

  • Renamed the cost-index dicts in qsdsan.utils.indices and the keys of qsdsan.utils.tea_indices to the *_by_year convention ('CEPCI_by_year', 'ChemPPI_by_year', 'labor_by_year', 'PCEPI_by_year'). Update any code that used the old short keys, e.g., tea_indices['CEPCI'] becomes tea_indices['CEPCI_by_year'].

  • Tutorial updates ongoing.

1.5.1

  • Component now validates the particle_size, degradability, and organic arguments at creation, for both the constructor and from_chemical(). Invalid values (e.g., a misspelled particle_size) that were previously accepted silently now raise a ValueError.

  • The f_BOD5_COD, f_uBOD_COD, and f_Vmass_Totmass fractions are now range-checked to [0, 1] (this check was previously unreachable).

  • The Component constructor now accepts a chemical keyword to build a component directly from an existing thermosteam.Chemical. from_chemical() is now a thin wrapper around it, with unchanged behavior.

  • Documented the ignore_inaccurate_molar_weight and adjust_MW_to_measured_as options of compile(), and substantially revised the topical tutorials.

1.5.0

  • All LCA registry types (ImpactIndicator, ImpactItem, Construction, Transportation) are now isolated per flowsheet. Switching between systems via set_flowsheet() atomically swaps all four registries, so no manual clear_lca_registries() calls are needed between systems. clear_lca_registries() is deprecated.

  • Added SanUnit._construction_specs — a class-level tuple of dicts for declaring default construction materials. Specs are resolved lazily by LCA at creation time, so ImpactItem objects do not need to exist when the unit is instantiated.

  • Reorganized unit operations and process models into clearer namespaces.

    • qsdsan.sanunits is renamed to qsdsan.unit_operations and reorganized into three behavior-based sub-namespaces:

      • qsdsan.unit_operations.bst — BioSTEAM-inherited unit operations (mixers, splitters, pumps, heat exchangers, distillation columns, tanks, etc.)

      • qsdsan.unit_operations.static — steady-state QSDsan unit operations (sanitation fixtures, treatment beds, clarifiers, sludge handling, hydrothermal/hydroprocessing units, etc.)

      • qsdsan.unit_operations.dynamic — unit operations with explicit dynamic-state behavior (bioreactors, dynamic influent, junctions, membrane bioreactors, etc.)

    • qsdsan.processes is renamed to qsdsan.process_models.

    • All existing imports via qsdsan.sanunits and qsdsan.processes remain valid for backward compatibility.

  • Restructured API documentation to mirror the new package layout, with dedicated pages for each sub-namespace.

  • Bug fixes in rhos_asm2d():

    • Added if X_MeOH > 0 and if X_MeP > 0 guards to the precipitation and redissolution reactions, consistent with the existing guards for X_H, X_PAO, and X_AUT. Without these guards, the BDF solver’s polynomial extrapolation could produce tiny positive floating-point values for X_MeOH, causing spurious X_MeP accumulation over long simulations.

    • Added a S_F + S_A > 0 guard to the heterotrophic growth substrate-partitioning terms. When the BDF Newton iterations drive both fermentable substrate (S_F) and acetate (S_A) to zero simultaneously, the partition fractions S_F/(S_F+S_A) and S_A/(S_F+S_A) produce 0/0 and raise a FloatingPointError; zero substrate correctly implies zero growth.

  • Multiple bug fixes and improvements to PolishingFilter:

    • Moved O2 deficit calculation before the effluent split so dissolved oxygen is correctly accounted for in all outlet streams.

    • Added an aerobic-only guard for air injection: air is now only added when self.has_pump is False and the unit operates in aerobic mode.

    • Fixed a missing _freeboard attribute that caused AttributeError on initialization.

    • Restored the _design_anaerobic method that had been inadvertently removed.

    • Corrected the slab concrete volume formula.

    • Added a biomass_ID parameter to allow users to specify which component tracks active biomass.

    • Renamed internal attributes gas/soluble/solid to gases/solubles/solids for consistency with the rest of the codebase.

    • Fixed the condition in get_digestion_rxns and corrected the argument order in _refresh_rxns.

    • Fixed a SanStream.degassing AttributeError that occurred when the polishing filter effluent was a plain SanStream.

    • Fixed an O2 double-counting error in air_out that produced a ~0.6% mass-balance error.

  • Fixed HydraulicDelay: added missing _update_state and _update_dstate methods so the unit correctly propagates state during dynamic simulation.

  • Added a deprecation warning to SimpleTEA to guide users toward the updated TEA interface.

  • Lazy-imported optional heavy dependencies (SALib, seaborn, chaospy, sympy) so that import qsdsan no longer pays the startup cost of those libraries unless they are actually used.

  • Added a GitHub Actions release workflow that automatically publishes to PyPI and creates a GitHub release when a v*.*.* tag is pushed.

1.4.0

  • A lot of the updates have been focused on the dynamic simulation, now the open-loop Benchmark Simulation Model No. 2 (BSM2) configuration has been implemented with new process models and unit operation including

    • qsdsan.processes.ADM1p

    • qsdsan.processes.ADM1_p_extension

    • qsdsan.processes.ModifiedADM1

    • qsdsan.processes.mASM2d

    • qsdsan.sanunits.IdealClarifier

    • qsdsan.sanunits.PrimaryClarifier

    • qsdsan.sanunits.PrimaryClarifierBSM2

    • qsdsan.sanunits.GasExtractionMembrane

    • qsdsan.sanunits.Thickener

    • qsdsan.sanunits.Centrifuge

    • qsdsan.sanunits.Incinerator

    • qsdsan.sanunits.BatchExperiment

    • qsdsan.sanunits.PFR

    • qsdsan.sanunits.BeltThickener

    • qsdsan.sanunits.SludgeCentrifuge

    • qsdsan.sanunits.SludgeThickener

  • New publications

    • Feng et al., Environmental Science & Technology, on the sustainability of hydrothermal liquefaction (HTL) for resource recovery from a range of wet organic wastes.

1.3.0

  • Enhance and use QSDsan’s capacity for dynamic simulation for emerging technologies and benchmark configurations (see EXPOsan METAB and PM2 (on the algae branch, still under development) modules).

  • New publications

    • The paper introducing DMsan, the package developed for decision-making of sanitation and resource recovery technologies, is published in ACS Environmental Au!

    • QSDsan was used to evaluate the sustainability of the NEWgenerator system as in this paper on ACS Environmental Au!

  • New modules

    • qsdsan.processes.KineticReaction

  • QSDsan now has a website to host all of the resources!

  • QSDsan’s documentation is getting a new look!

  • Add new units to enable dynamic simulation of systems with multiple process models. Check out qsdsan.sanunits.Junction, qsdsan.sanunits.ADMtoASM, qsdsan.sanunits.ASMtoADM and their use in the interface system demo.

  • In online testing, we dropped the test for Python 3.8 and added Python 3.10. The main developing environment for QSDsan is 3.9.

1.2.0

  • The QSDsan paper is accepted by Environmental Science: Water Research & Technology!

  • The first paper using QSDsan for the design of sanitation is accepted by ACS Environmental Au! Read the Biogenic Refinery paper and check out the system module in QSDsan/EXPOsan.

  • Added multiple systems (including their unit operations), check out the details on the Developed System page!

    • Biogenic Refinery

    • Eco-San

    • Reclaimer

  • Added the anaerobic digestion model no. 1 (ADM1) process model and the unit qsdsan.sanunits.AnaerobicCSTR, the corresponding system can be found in EXPOsan.

  • Other new unit operations:

    • Encapsulation Bioreactors:

      • qsdsan.sanunits.CH4E

      • qsdsan.sanunits.H2E

1.1.0

  • Fully tested dynamic simulation capacity, refer to the BSM1 system in EXPOsan for an example implementation.

  • Added many new qsdsan.SanUnit and reorganized package/documentation structure, new unit operations include:

    • qsdsan.sanunits.AnMBR

    • qsdsan.sanunits.CHP

    • qsdsan.sanunits.InternalCirculationRx

    • qsdsan.sanunits.SludgeHandling

      • qsdsan.sanunits.BeltThickener

      • qsdsan.sanunits.SludgeCentrifuge

    • qsdsan.sanunits.PolishingFilter

    • qsdsan.sanunits.WWTpump

  • Continue to enhance documentation (e.g., qsdsan.Process, qsdsan.stats, util functions).

1.0.0

Official release of QSDsan v1.0.0!

  • Added system-wise dynamic simulation capacity. To use the dynamic simulation function, a unit needs to have several supporting methods to initialize its state and compile ordinary differential equations (ODEs), refer to the units included in the BSM1 system below for usage, documentation and tutorial will be coming soon!

  • Developed the benchmark simulation system no.1 (BSM1) model on EXPOsan with comparison against the MATLAB/Simulink model developed by the International Water Association (IWA) Task Group on Benchmarking of Control Strategies. See the README for details

  • Significantly expanded the tutorials with demo videos on YouTube. Now tutorials cover all non-dynamic major classes (tutorials on dynamic classes will be included in the next major release).

0.3.0

0.2.0

0.1.0

0.0.3

0.0.2

  • Added the all three sanitation scenarios as described in Trimmer et al., including uncertainty/sensitivity analyses with tutorial.

  • Inclusion of GPX models for estimation of qsdsan.WasteStream properties.

  • Live documentation for the latest and beta version.

  • New classes:

    • All units in Trimmer et al.

    • Added descriptors (qsdsan.utils.descriptors) and decorators (qsdsan.utils.checkers) to check user-input values.

    • qsdsan.utils.setters.AttrSetter, qsdsan.utils.setters.DictAttrSetter, and qsdsan.utils.getters.FuncGetter for batch-setting of uncertainty analysis parameters.

  • Added save_report() function to qsdsan.LCA for report exporting.

0.0.1

  • First public release.