Skip to content

Fix crystallizer energy balance units and jacket paths#106

Open
andres9403 wants to merge 6 commits into
masterfrom
fix/issue-68-crystallizer-energy
Open

Fix crystallizer energy balance units and jacket paths#106
andres9403 wants to merge 6 commits into
masterfrom
fix/issue-68-crystallizer-energy

Conversation

@andres9403

@andres9403 andres9403 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Closes #68

Summary

  • Converts plain-liquid crystallizer inlet enthalpy from mass-specific to volumetric basis before it reaches MSMPR/Semibatch energy balances.
  • Adds the missing adiabatic MSMPR energy-balance path so it returns only the tank temperature derivative when no jacket state exists.
  • Reads Semibatch jacket inlet temperature, flow, Cp, and density from Utility, matching the established MSMPR/reactor pattern.
  • Copies slurry volume fractions inside Slurry.getCp before times_vliq scaling so callers do not see mutated volfracs.
  • Adds focused regression coverage for the four issue Critical: [Crystallizers] Energy balances crash in adiabatic/jacket modes and mix enthalpy bases #68 defects.

Tests

  • pytest tests/test_crystallizer_energy_balances.py
  • pytest (default environment: 32 passed, 6 skipped because Assimulo is unavailable there)
  • /home/andresfel9403/anaconda3/bin/conda run -n PharmaPy python -m pytest (38 passed with Assimulo 3.4.3)
  • git -c core.whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol diff --cached --check

Branch Hygiene

  • Base branch: master
  • Source branch point: origin/master at 4098d776613ecfab3d65e244a027cf58c3fe3c4a
  • Head branch: fix/issue-68-crystallizer-energy
  • Stacked status: not stacked; no prerequisite PRs.
  • Upstream sync: upstream/master at 400e1e13ad4ecd3ba202d5fae8c70ec73bdf9c3e is already contained in origin/master.

@andres9403
andres9403 marked this pull request as ready for review July 8, 2026 19:24

@andres9403 andres9403 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintainer review (posted as COMMENT because this account (andres9403) is the PR author; GitHub rejects author APPROVE/REQUEST_CHANGES). No blocking findings.

The PR resolves all four defects in #68, and each is locked by a test in the new tests/test_crystallizer_energy_balances.py. I verified the four tests fail against the pre-fix source (checked out the merge-base versions of Crystallizers.py/MixedPhases.py) and pass on the head, so they are not vacuously green:

  • Defect 1 (adiabatic MSMPR): energy_balances now short-circuits with ht_term = 0 and returns the scalar dtemp_dt when self.adiabatic or temp_ht is None, so unit_model's np.append yields the single temp derivative matching states_uo. No jacket dereference of temp_ht=None.
  • Defect 2 (Semibatch jacket): reads temp_in/vol_flow from self.Utility.get_inputs(time) and cp/rho from Utility, dropping the never-assigned self.temp_ht_in/self.flow_ht. Mirrors the MSMPR pattern.
  • Defect 3 (liquid-feed enthalpy basis): the plain-liquid branch now multiplies specific enthalpy by rho_liq_in, so h_in is volumetric (J/m^3), consistent with the volumetric h_sp in both MSMPR (flow_term = input_flow*(h_in - h_sp)) and Semibatch (flow_term = input_flow*h_in with accum_term in J/s).
  • Defect 4 (Slurry.getCp mutation): volfracs is copied before the times_vliq scaling, so the caller's list is no longer mutated; self.epsilon/self.densities are write-only elsewhere, so the list->ndarray type change is inert.

One non-blocking Question is left inline on the source_term annotation. See the summary below.

Comment thread PharmaPy/Crystallizers.py Outdated
source_term = dh_cryst*cryst_rate * vol
# Energy terms
flow_term = input_flow * (h_in - h_sp) # [J/s]
source_term = dh_cryst*cryst_rate * vol # [J/s]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question (non-blocking): this line is annotated # [J/s] for dh_cryst*cryst_rate * vol, but the analogous SemibatchCryst.energy_balances annotates source_term = dh_cryst * cryst_rate (no * vol) also as # [J/s]. Both can only be [J/s] if cryst_rate (the transf returned by material_balances) carries different units in the two classes ([kg/m**3/s] here vs [kg/s] in Semibatch). That is pre-existing numeric code, not changed by this PR, so it is not blocking. But since #68 is about basis/unit consistency in exactly these balances, could you confirm the two cryst_rate bases and annotate cryst_rate at the material_balances boundary so the [J/s] labels are unambiguous?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 57dd0d2: annotated the material_balances return basis for transf and the paired source_term comments. MSMPR uses cryst_rate [kg/m**3/s] and multiplies by vol; Batch/Semibatch use cryst_rate [kg/s].

Comment thread PharmaPy/Crystallizers.py Outdated
rho_ht = 1000
vol_ht = self.vol_tank*0.14 # m**3
ht_media = self.Utility.get_inputs(time)
tht_in = ht_media['temp_in'] # [K]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What did it changed from "degC" to "#[K]" are we sure about the input units of ht_media['temp_in']

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still seems to be an issue. Let’s please resolve it

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this happened since in the first commit i asked to add units to the touched functions. I believe we need a different issue for documented non touched lines, making sure the source of the units since this change is an indication that is not checking it and assuming changing units to the required format and SI.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 57dd0d2: kept the utility inlet temperature on an absolute-temperature basis and clarified it inline as # [K], Utility inlet temperature. I also verified this against CoolingWater.temp_in defaulting to 298.15 and the jacket temperature state using K.

@andres9403 andres9403 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintainer review (posted as COMMENT because this account (andres9403) authored the PR; GitHub rejects author APPROVE/REQUEST_CHANGES). Reviewing head 47c01c7, which has advanced past my prior review of 19f9ecb (the "Narrow unit annotations" commit).

Verdict

No blocking findings. The four issue #68 defects are each fixed correctly, the fixes match the established MSMPR/reactor Utility pattern, and the new tests genuinely lock in each fix (they are red against the pre-fix code shape). I would consider this merge-ready on the merits; a formal approval must come from another maintainer account since this account cannot approve its own PR (no branch-protection review gate is currently configured on master, so this is only the GitHub author restriction, not a reported required-review gate).

What I verified

  • Defect 1 — MSMPR adiabatic branch (Crystallizers.py:1877-1891): if self.adiabatic: ht_term = 0 and the early return dtemp_dt when self.adiabatic or temp_ht is None correctly avoid the temp_ht=None dereference and the spurious jacket tuple. test_msmpr_adiabatic_energy_balance_has_no_jacket_equation asserts a scalar return; pre-fix this hit the elif 'temp' in self.states_uo path and raised TypeError on temp - None.
  • Defect 2 — Semibatch jacket inputs (Crystallizers.py:2218-2227): now reads temp_in/vol_flow via self.Utility.get_inputs(time) and cp/rho from Utility. Confirmed against Utilities.py:16-17,67-71 that get_inputs returns those keys and cp/rho exist. No lingering references to the removed self.temp_ht_in/self.flow_ht. test_semibatch_jacket_uses_utility_inputs was red pre-fix (AttributeError).
  • Defect 3 — liquid-feed enthalpy basis (Crystallizers.py:593-594): h_in = h_in_mass * rho_liq_in converts J/kg → J/m**3, consistent with the volumetric slurry enthalpy h_sp. test_liquid_feed_enthalpy_passed_to_energy_balance_is_volumetric asserts 123*950; pre-fix returned the bare 123 (mass basis).
  • Defect 4 — Slurry.getCp mutation (MixedPhases.py:331-341): volfracs = np.array(volfracs, copy=True) protects the caller's list before the times_vliq scaling, and self.epsilon = volfracs.copy() snapshots the pre-scaling fractions. self.epsilon on Slurry has no external readers, so the snapshot change is behavior-neutral outside this method. test_slurry_getcp_times_vliq_does_not_mutate_volfracs was red pre-fix.

Linked issue

Closes #68 is accurate: the PR addresses all four defects enumerated in the issue (not an adjacent subset), so the closing link is correct.

Tests run

  • pytest tests/test_crystallizer_energy_balances.py → 4 passed (Assimulo stubbed in-test).
  • pytest (default env) → 32 passed, 6 skipped (Assimulo unavailable; the skipped set is the integration tests, not the changed-behavior tests). Matches the PR body.
  • Warnings observed (invalid escape sequence '\m' at Crystallizers.py:1057+) are pre-existing in untouched plotting code, not introduced here.

Merge-readiness

  • PR-head checks: Core tests SUCCESS, Assimulo integration tests SUCCESS on 47c01c7.
  • mergeStateStatus: CLEAN; file set matches (gh pr diff --name-only == local merge-base diff).
  • No blocking issues.

Comment thread PharmaPy/Crystallizers.py Outdated
@andres9403

Copy link
Copy Markdown
Collaborator Author

Addressed the remaining Bernalde units feedback on the Semibatch jacket lines and kept the 0.14 magic-number follow-up scoped to #113 as discussed.

Commits pushed:

  • 467777a Annotate Semibatch utility units
  • 97f6676 Merge current origin/master into the PR branch
  • a47d68b Scope crystallizer Assimulo test shim

Main changes:

  • Added explicit inline units for ht_media['temp_in'] ([K]), ht_media['vol_flow'] ([m**3/s]), self.Utility.cp ([J/kg/K]), self.Utility.rho ([kg/m**3]), and vol_ht ([m**3]) in SemibatchCryst.energy_balances.
  • Merged current master append-only so the PR is tested against the current base.
  • Scoped the crystallizer test's temporary Assimulo shim so it no longer leaks fake assimulo modules into later collection. This fixed the transient GitHub core collection failure seen after merging the current base.

Tests run:

  • python3 -m pytest --collect-only -> 48 collected, 2 skipped
  • python3 -m pytest tests/ -m "not assimulo" -> 42 passed, 2 skipped, 6 deselected
  • ./.venv/bin/pytest --collect-only -> 50 collected
  • ./.venv/bin/pytest tests/test_crystallizer_energy_balances.py -> 4 passed
  • ./.venv/bin/pytest tests/ -m "not assimulo" -> 42 passed, 8 deselected
  • ./.venv/bin/pytest tests/ -v -m assimulo -> 8 passed, 42 deselected
  • git -c core.whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol diff --check -> clean

GitHub checks on a47d68b59792c409cf2f13cfaff55719fd8c0776:

  • Core tests: SUCCESS
  • Assimulo integration tests: SUCCESS
  • mergeStateStatus: CLEAN

Not expanded in this PR:

Remaining gate:

  • GitHub still reports reviewDecision: CHANGES_REQUESTED until Bernalde updates or dismisses that review.

@andres9403
andres9403 requested a review from bernalde July 15, 2026 14:14
Comment thread PharmaPy/Crystallizers.py Outdated
rho_ht = 1000
vol_ht = self.vol_tank*0.14 # m**3
ht_media = self.Utility.get_inputs(time)
tht_in = ht_media['temp_in'] # [K]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still seems to be an issue. Let’s please resolve it

Comment thread PharmaPy/Crystallizers.py Outdated
@@ -1884,6 +1887,9 @@ def energy_balances(self, time, params, cryst_rate, u_inputs, rhos, mu_n,
# Balance inside the tank
dtemp_dt = (flow_term - source_term - ht_term) / vol / capacitance

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Units are missing here

@andres9403 andres9403 Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this comment refer to add units to not added line. If so I prefer not to add the units since as commented in the next commented line I believe the agent it is not checking the unit source but only using the SI units. I believe this requires a separate EPIC issue for documentation and to addressed model by model (Crystallizers, Dryer, etc)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree, every file we are touching should now incorporate units since in the process of fixing the bugs we should have the context to already fix this. Unless we encounter bugs, we can do this as a side cleanup. Adding units to any new/modified line is non-negotiable

@andres9403 andres9403 Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont thing it has already the context of fixing the units alongside while fixing the bugs for the entire function, and my fear is since we are adding units during this issues fixing we would believe its already sort it out. Thats why I needed to revert the changes in the first commit.

Thats why I propose a different issue (maybe not an epic) that only deals with documentation and unit adding.

@andres9403 andres9403 Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have a confusion here. I agree with the adding units in the add/modified lines. Now since in the first commit I also ask to the agent that for any touched function the units should be added, when I realized that it changed the units of already commented ones (e.g changed "degC-> ["K"]) with no reason I reverted the changes, and even though the code line was not touched, due the change in the units you see this as a modified line.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 57dd0d2: dtemp_dt is now annotated as [K/s], and the adjacent MSMPR source term now records the volumetric cryst_rate [kg/m**3/s] basis.

Comment thread PharmaPy/Crystallizers.py Outdated
@@ -2210,11 +2216,12 @@ def energy_balances(self, time, params, cryst_rate, u_inputs, rhos,
# print(dtemp_dt)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is some leftover comments that we should clean up

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 57dd0d2: removed the leftover # print(dtemp_dt) debug comment from the Semibatch energy balance.

Comment thread tests/test_crystallizer_energy_balances.py
@andres9403

Copy link
Copy Markdown
Collaborator Author

Addressed the remaining review feedback on current head 57dd0d2.

Commits pushed:

  • 57dd0d2 Address crystallizer review unit notes

Main changes:

  • Annotated transf/cryst_rate bases at the material-balance boundaries: Batch/Semibatch [kg/s], MSMPR [kg/m**3/s].
  • Aligned source-term comments with those bases and added missing local energy units, including dtemp_dt as [K/s].
  • Clarified ht_media['temp_in'] as the Utility inlet temperature in [K].
  • Removed the leftover # print(dtemp_dt) comment in SemibatchCryst.energy_balances.
  • Added a standard module docstring to tests/test_crystallizer_energy_balances.py.

Tests run:

  • ./.venv/bin/pytest tests/test_crystallizer_energy_balances.py -> 4 passed
  • ./.venv/bin/pytest --collect-only -> 50 collected
  • ./.venv/bin/pytest tests/ -m "not assimulo" -> 42 passed, 8 deselected
  • ./.venv/bin/pytest tests/ -v -m assimulo -> 8 passed, 42 deselected
  • ./.venv/bin/pytest -> 50 passed
  • git -c core.whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol diff --check -> clean

No separate lint/type-check command is configured in pyproject.toml or CI.

GitHub checks on 57dd0d2b1e852a975a7ade9e84d51106e39a7d7a:

  • Core tests: SUCCESS
  • Assimulo integration tests: SUCCESS
  • mergeStateStatus: CLEAN

Not intentionally declined:

Remaining gate:

  • GitHub still reports reviewDecision: CHANGES_REQUESTED until Bernalde updates or dismisses that review.

Comment thread PharmaPy/Crystallizers.py
@@ -1545,7 +1546,7 @@ def energy_balances(self, time, params, cryst_rate, u_inputs, rhos,
height_liq = vol / (np.pi/4 * self.diam_tank**2)
area_ht = np.pi * self.diam_tank * height_liq + self.area_base # m**2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Units]

Comment thread PharmaPy/Crystallizers.py
dmaterial_dt = np.concatenate((ddistr_dt, dliq_dt))

return dmaterial_dt, transf
return dmaterial_dt, transf # transf [kg/s]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the units of dmaterial_dt?

Comment thread PharmaPy/Crystallizers.py
h_in_mass = self.Inlet.getEnthalpy(temp=inlet_temp) # [J/kg]
h_in = h_in_mass * rho_liq_in # [J/kg]*[kg/m**3] -> [J/m**3]

phis_in = [1, 0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Units of phi?

Comment thread PharmaPy/Crystallizers.py
dmaterial_dt = np.concatenate((ddistr_dt, dcomp_dt))

return dmaterial_dt, transf
return dmaterial_dt, transf # transf [kg/m**3/s]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are the units different from the case above?

Comment thread PharmaPy/Crystallizers.py
return dmaterial_dt, transf
return dmaterial_dt, transf # transf [kg/m**3/s]

def energy_balances(self, time, params, cryst_rate, u_inputs, rhos, mu_n,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function documentation would avoid the confusing units declaration below (where, in a single line cryst_rrate is also defined)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Critical: [Crystallizers] Energy balances crash in adiabatic/jacket modes and mix enthalpy bases

2 participants