Add Python3 port and example plotting script#2
Conversation
Add a Python 3 port of the CREST model as pyCREST_Py_3.py and an example script Example_Py_3.py that generates profiles and provides helper plotting functions (plot_powerfile, plot_occupancy_file, plot_app_profiles). Rename the original pyCREST.py to pyCREST_Py_2.py to preserve the prior copy. The example uses pyCREST_Py_3.create_profiles to produce Pfile_/Qfile_/Occfile_/AppProfiles files and attempts to display or save plots (matplotlib + numpy).
There was a problem hiding this comment.
Pull request overview
This PR adds a Python 3-compatible port of the CREST demand model while preserving the prior Python 2 implementation under a new filename, plus an example script for generating output files and plotting results.
Changes:
- Added
pyCREST_Py_3.py(Python 3 port) with helper string decoding for NumPygenfromtxtoutputs. - Added
Example_Py_3.pyto generate profiles and plot power/occupancy/appliance profiles using NumPy + Matplotlib. - Added
pyCREST_Py_2.pyto retain the previous Python 2 version under an explicit filename.
Reviewed changes
Copilot reviewed 2 out of 3 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| pyCREST_Py_3.py | Introduces the Python 3 version of the model and writes out the generated demand/occupancy/appliance profile files. |
| pyCREST_Py_2.py | Preserves the legacy Python 2 implementation under a new name for reference/backward compatibility. |
| Example_Py_3.py | Provides plotting helpers and a runnable example that calls the Python 3 model and visualizes outputs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| writer.writerow([i] + ["P"] + [appliances_in_dwelling[appliance][15]] + sim_dataP[appliance][:].tolist()) | ||
| writer.writerow([i] + ["Q"] + [appliances_in_dwelling[appliance][15]] + sim_dataQ[appliance][:].tolist()) |
There was a problem hiding this comment.
appliances_in_dwelling is loaded with genfromtxt(..., encoding=None) and string fields are likely bytes (you already added _to_str for comparisons). When writing AppProfiles...dat, the appliance name is written directly from appliances_in_dwelling[...][15], which can result in tokens like b'FRIDGE' in the output and break downstream parsing (e.g., Example_Py_3.plot_app_profiles). Decode to str (or load with encoding='utf-8') before writing names.
| writer.writerow([i] + ["P"] + [appliances_in_dwelling[appliance][15]] + sim_dataP[appliance][:].tolist()) | |
| writer.writerow([i] + ["Q"] + [appliances_in_dwelling[appliance][15]] + sim_dataQ[appliance][:].tolist()) | |
| writer.writerow([i] + ["P"] + [_to_str(appliances_in_dwelling[appliance][15])] + sim_dataP[appliance][:].tolist()) | |
| writer.writerow([i] + ["Q"] + [_to_str(appliances_in_dwelling[appliance][15])] + sim_dataQ[appliance][:].tolist()) |
|
|
||
| # Store the demand | ||
| if iTime < 1440: | ||
| lighting_demand_data[i,iTime] = iRating |
There was a problem hiding this comment.
In the lighting simulation, demand is stored at lighting_demand_data[i, iTime] while the rest of the code treats iTime as 1-based (e.g., irradiance uses iTime-1, and the off case stores at iTime-1). This shifts “on” minutes by +1 and can drop the first minute / misalign the profile. Store lighting demand at iTime-1 for consistency.
| lighting_demand_data[i,iTime] = iRating | |
| lighting_demand_data[i,iTime-1] = iRating |
| iCurrentstate = 0 | ||
| # Reset the cumulative probability count | ||
| fCumulativeP = 0 | ||
| # Determine the start state at time 00:00 by checking the random number against the distribution | ||
| for iCurrentState in range(0,6): | ||
| # Add the probability for this number of active occupants | ||
| fCumulativeP = fCumulativeP + start_states[iCurrentstate][household_size-1] |
There was a problem hiding this comment.
get_start_state accumulates probabilities from start_states[iCurrentstate] where iCurrentstate is always 0, so the cumulative distribution is built from only the first row (zero active occupants). This makes the initial occupancy state selection incorrect. Use the loop variable (iCurrentState) when indexing start_states (and consider iterating 0..6 to cover all 7 states).
| iCurrentstate = 0 | |
| # Reset the cumulative probability count | |
| fCumulativeP = 0 | |
| # Determine the start state at time 00:00 by checking the random number against the distribution | |
| for iCurrentState in range(0,6): | |
| # Add the probability for this number of active occupants | |
| fCumulativeP = fCumulativeP + start_states[iCurrentstate][household_size-1] | |
| # Reset the cumulative probability count | |
| fCumulativeP = 0 | |
| # Determine the start state at time 00:00 by checking the random number against the distribution | |
| for iCurrentState in range(0,7): | |
| # Add the probability for this number of active occupants | |
| fCumulativeP = fCumulativeP + start_states[iCurrentState][household_size-1] |
| sim_data_outputQ = numpy.sum(sim_dataQ, axis=0) | ||
|
|
||
| with open('AppProfiles'+idstring+'.dat', 'a', newline='') as f: | ||
| writer = csv.writer(f, delimiter =' ',lineterminator='') |
There was a problem hiding this comment.
The lighting row is written with lineterminator='', which produces no newline after the record; subsequent appends will concatenate records onto the same line and make AppProfiles...dat hard to parse. Use a newline terminator (e.g. \n) consistently for all rows.
| writer = csv.writer(f, delimiter =' ',lineterminator='') | |
| writer = csv.writer(f, delimiter =' ',lineterminator='\n') |
| Pfile = open('Pfile_'+idstring+'.dat', 'w') | ||
|
|
||
| numpy.savetxt('Pfile_'+idstring+'.dat',sim_dataP_for_file,fmt="%d", delimiter='\t') | ||
| Pfile.close() | ||
| Qfile = open('Qfile_'+idstring+'.dat', 'a') | ||
| numpy.savetxt('Qfile_'+idstring+'.dat',sim_dataQ_for_file,fmt="%d", delimiter='\t') | ||
| Qfile.close() | ||
| Occfile = open('Occfile_'+idstring+'.dat', 'a') | ||
| numpy.savetxt('Occfile_'+idstring+'.dat',occ_profile_for_file,fmt="%d", delimiter='\t') | ||
| Occfile.close() |
There was a problem hiding this comment.
numpy.savetxt is called with a filename string, so it opens/truncates the file itself; the separately opened Qfile/Occfile handles (opened in append mode) are unused and the mode doesn’t match the actual write behavior. Prefer with open(..., 'w') as f: numpy.savetxt(f, ...) (or decide explicitly on append vs overwrite) and remove the unused open(...) calls.
| Pfile = open('Pfile_'+idstring+'.dat', 'w') | |
| numpy.savetxt('Pfile_'+idstring+'.dat',sim_dataP_for_file,fmt="%d", delimiter='\t') | |
| Pfile.close() | |
| Qfile = open('Qfile_'+idstring+'.dat', 'a') | |
| numpy.savetxt('Qfile_'+idstring+'.dat',sim_dataQ_for_file,fmt="%d", delimiter='\t') | |
| Qfile.close() | |
| Occfile = open('Occfile_'+idstring+'.dat', 'a') | |
| numpy.savetxt('Occfile_'+idstring+'.dat',occ_profile_for_file,fmt="%d", delimiter='\t') | |
| Occfile.close() | |
| with open('Pfile_'+idstring+'.dat', 'w') as Pfile: | |
| numpy.savetxt(Pfile, sim_dataP_for_file, fmt="%d", delimiter='\t') | |
| with open('Qfile_'+idstring+'.dat', 'w') as Qfile: | |
| numpy.savetxt(Qfile, sim_dataQ_for_file, fmt="%d", delimiter='\t') | |
| with open('Occfile_'+idstring+'.dat', 'w') as Occfile: | |
| numpy.savetxt(Occfile, occ_profile_for_file, fmt="%d", delimiter='\t') |
| appliance token and then interprets subsequent tokens as floats. If no | ||
| *appliance* is specified, the first numeric profile encountered is used. | ||
| """ | ||
| lines = open(filename).read().strip().splitlines() |
There was a problem hiding this comment.
plot_app_profiles reads the file via open(filename).read() without closing the handle. Use a context manager (with open(...) as f:) to avoid leaking file descriptors, especially if these helpers are called repeatedly.
| lines = open(filename).read().strip().splitlines() | |
| with open(filename) as f: | |
| lines = f.read().strip().splitlines() |
| times = np.arange(len(data)) / 60.0 | ||
| _plt.figure() | ||
| _plt.plot(times, data if data.ndim==1 else data[0]) |
There was a problem hiding this comment.
In the exception fallback plot, times = np.arange(len(data)) / 60.0 uses the number of rows when the loaded file is 2D (n dwellings), but the plotted profile is data[0] (length 1440). For n > 1 this will raise a shape mismatch or produce an incorrect x-axis. Build times from the selected profile length (same approach as plot_powerfile).
| times = np.arange(len(data)) / 60.0 | |
| _plt.figure() | |
| _plt.plot(times, data if data.ndim==1 else data[0]) | |
| if data.ndim == 1: | |
| profile = data | |
| else: | |
| profile = data[0] | |
| times = np.arange(len(profile)) / 60.0 | |
| _plt.figure() | |
| _plt.plot(times, profile) |
Added a Python 3 port of the CREST model as pyCREST_Py_3.py and an example script Example_Py_3.py that generates profiles and provides helper plotting functions (plot_powerfile, plot_occupancy_file, plot_app_profiles). Rename the original pyCREST.py to pyCREST_Py_2.py to preserve the prior copy. The example uses pyCREST_Py_3.create_profiles to produce Pfile_/Qfile_/Occfile_/AppProfiles files and attempts to display or save plots (matplotlib + numpy).
I tried retaining the original python 2 version as close as possible. What do you think?