From 93d44105343925028993b39b4d3a7266c2ed42c7 Mon Sep 17 00:00:00 2001 From: Allen Gilliland Date: Thu, 23 Feb 2017 10:41:14 -0800 Subject: [PATCH] Initial commit. --- .babelrc | 3 + .editorconfig | 12 + .gitignore | 19 ++ jasmine.json | 11 + karma.config.js | 37 +++ package.json | 28 +++ src/model/data.js | 194 +++++++++++++++ src/model/dynamic_parameters.js | 56 +++++ src/model/internal.js | 52 ++++ src/model/model.js | 362 ++++++++++++++++++++++++++++ src/model/solar.js | 93 ++++++++ src/model/transmission.js | 95 ++++++++ src/model/ventilation.js | 92 +++++++ src/util/building.js | 22 ++ src/util/climate.js | 28 +++ src/util/math.js | 20 ++ test/model/internal.spec.js | 69 ++++++ test/model/model.spec.js | 58 +++++ test/model/solar.spec.js | 30 +++ test/model/transmission.spec.js | 15 ++ test/testdata/epc.js | 410 ++++++++++++++++++++++++++++++++ test/util/building.spec.js | 11 + test/util/math.spec.js | 18 ++ webpack.config.js | 35 +++ 24 files changed, 1770 insertions(+) create mode 100644 .babelrc create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 jasmine.json create mode 100644 karma.config.js create mode 100644 package.json create mode 100644 src/model/data.js create mode 100644 src/model/dynamic_parameters.js create mode 100644 src/model/internal.js create mode 100644 src/model/model.js create mode 100644 src/model/solar.js create mode 100644 src/model/transmission.js create mode 100644 src/model/ventilation.js create mode 100644 src/util/building.js create mode 100644 src/util/climate.js create mode 100644 src/util/math.js create mode 100644 test/model/internal.spec.js create mode 100644 test/model/model.spec.js create mode 100644 test/model/solar.spec.js create mode 100644 test/model/transmission.spec.js create mode 100644 test/testdata/epc.js create mode 100644 test/util/building.spec.js create mode 100644 test/util/math.spec.js create mode 100644 webpack.config.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..af9969c --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets" : ["es2015"] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..eb7f650 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true + +[{package.json,bower.json,*.yml}] +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7603ffc --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +logs +*.log +npm-debug.log* +pids +*.pid +*.seed +lib-cov +coverage +.nyc_output +.grunt +.lock-wscript +build/Release +node_modules +jspm_packages +.npm +.node_repl_history + +# app specific ignores +/dist diff --git a/jasmine.json b/jasmine.json new file mode 100644 index 0000000..89452f1 --- /dev/null +++ b/jasmine.json @@ -0,0 +1,11 @@ +{ + "spec_dir": "test", + "spec_files": [ + "**/*[sS]pec.js" + ], + "helpers": [ + "helpers/**/*.js" + ], + "stopSpecOnExpectationFailure": false, + "random": false +} diff --git a/karma.config.js b/karma.config.js new file mode 100644 index 0000000..a06dfb0 --- /dev/null +++ b/karma.config.js @@ -0,0 +1,37 @@ +var path = require('path'); + +var TEST_DIR = path.resolve(__dirname, 'test'); + +var webpackConfig = require('./webpack.config'); +webpackConfig.resolve.alias.testdata = TEST_DIR + "/testdata"; + +module.exports = function(config) { + config.set({ + basePath: TEST_DIR, + files: [ + { pattern: '**/*.spec.js', watched: false, included: true, served: true } + ], + browsers: ['PhantomJS'], + frameworks: ['jasmine'], + reporters: ['progress'], + preprocessors: { + '**/*.spec.js': ['webpack'] + }, + webpack: { + module: { + loaders: [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader' + } + ] + }, + resolve: webpackConfig.resolve, + watch: true + }, + webpackServer: { + noInfo: true + } + }); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..c15f49a --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "buildzero-energy-model", + "version": "0.1.0", + "description": "", + "main": "", + "author": "", + "license": "AGPL-3.0", + "scripts": { + "build": "webpack -d", + "test": "karma start karma.config.js --single-run" + }, + "dependencies": { + "underscore": "^1.8.3" + }, + "devDependencies": { + "babel-core": "^6.10.4", + "babel-loader": "^6.2.4", + "babel-preset-es2015": "^6.9.0", + "jasmine": "^2.5.3", + "jasmine-core": "^2.5.2", + "karma": "^1.5.0", + "karma-cli": "^1.0.1", + "karma-jasmine": "^1.1.0", + "karma-phantomjs-launcher": "^1.0.2", + "karma-webpack": "^2.0.2", + "webpack": "^1.13.1" + } +} diff --git a/src/model/data.js b/src/model/data.js new file mode 100644 index 0000000..25b7e27 --- /dev/null +++ b/src/model/data.js @@ -0,0 +1,194 @@ +/* + often we need to pass in large groups of definitions for a given calculation, + such as in the solar gains and heat transfer by transmissions calcs which need + the entire building envelope definition including assemblies, etc. + + it may be ideal to maintain a separate set of data structures for the persisted + info about model, normalized for easy collection and management by category, but + to then create a working data structure for the model usage itself which is a + denormalized structure where things like assembly data is merged with areas to + make it easier to access the relevant attributes. +*/ + +let BUILDING = { + assemblies: ASSEMBLIES, + areas: AREAS, + windows: WINDOWS, + ground: GROUND +} + +let ASSEMBLIES = { + asm01: ASSEMBLY +}; + +let ASSEMBLY = { + id: "asm01", // unique identifier, UUID? + name: "Wall", // user defined name + type: "wall", // {"roof", "wall", "floor"} + adjacent_to: "outdoorair", // {"outdoorair", "ground", "ventilated"} + layers: [{ // and ORDERED list of layers making up the assembly + name: "gypsum", + thickness: 0.63, + r_per_inch: 0.9 + },{ + name: "cellulose", // user defined name + thickness: 3.5, // thickness of material + r_per_inch: 3.6 // r-value per inch of material + },{ + name: "sheathing", + thickness: 0.63, + r_per_inch: 1.4 + }], + u_value_supplement: 0, // a user defined supplement to the u-value + absorptivity: 0.6, // coefficient of heat absorption (TODO: ok?) + emissivity: 0.1, // coefficient of heat emission (TODO: ok?) + + // this stuff is all calculated + thickness: 13.25, // total thickness + r_value: 41.6 // total r-value (TODO: how to calc?) + u_value: 0, // total u-value (TODO: how to calc?) + g_value: 0.023 // absorptivity * r-value * u-value +}; + + +let WINDOWS = { + win01: WINDOW +}; + +let WINDOW = { + id: "win01", // unique identifier, UUID? + name: "Double Pane", // user defined name + + // this stuff is all calculated + thickness: 13.25, // total thickness + r_value: 41.6 // total r-value (TODO: how to calc?) + u_value: 0, // total u-value (TODO: how to calc?) + g_value: 0.023 // absorptivity * r-value * u-value +}; + + +let AREA = { + name: "Front Wall", + group: "front", + assembly: "asm01", + area: 100, // capture unit? + orientation: 74, + inclination: 30, + windows: [{ + name: "Large Bay Window", + area: 40, // capture unit? + component_id: "win01", + frame_id: "frame01", + // installation situation?? + shading: { + height: 0, + horizontal_distance: 0, + reveal_depth: 0, + distance_to_reveal: 0, + overhang_depth: 0, + distance_to_overhang: 0, + additional_reduction_winter: 0, + additional_reduction_summer: 0, + reduction_factor_Z_for_temporary: 0 + } + }] +} + + +// ELEMENT: +// name +// group (for logically organizing surfaces together) +// area (length * width + user determined addition - user determined subtraction) +// assembly id (defines physical qualities of element like R value) +// angle deviation from North (0=North, 90=East, 180=South, 270=West) +// angle of inclination from horizontal (0/180=flat, 90=vertical) +// ?? reduction factor shading +// ?? exterior absorptivity +// ?? exterior emissivity +// +// GLAZING ELEMENTS: +// name +// area (lenth * width + user determined) +// element id (where this is installed) +// glazing component id (window properties) +// frame component id (window frame properties) +// ?? installation situation (not sure exactly what this is for) +// +// GLAZING ELEMENT SHADING: +// height of shading object +// horizontal distance +// window reveal depth (lateral reveal) +// distance from glazing edge to reveal (lateral reveal) +// overhang depth (reveal/overhang) +// distance from upper glazing edge to overhang (reveal/overhang) +// additional reduction factor winter shading +// additional reduction factor summer shading +// reduction factor z for temporary sun protection (manual shade ??) + +let GENERAL_SETTINGS = { + floor_area: 336, // m2 + occupants: 17, // people + occupant_density: 20.0, // m2/person (this can be area/occupants) + metabolic_load: 88, // W/person + appliance_load: 10.0, // W/m2 + lighting_load: 10.0, // W/m2 + outdoor_air: 10.0, // liter/s/person + dhw: 5.0, // liter/m2/person + + // lighting specific + daylighting_factor: 1, + lighting_occupancy_factor: 1, + constant_illumination_factor: 1, + is_parasitic_lighting: 1, + annual_parasitic_load: 6 +}; + + +// should be an array with each element corresponding to an hour of the day (0-23) +// each entry is an object with the relevant data about usage for that hour +let SCHEDULE = [ + { + // NOTE: it is important to be able to tell if a given hour of the day + // is considered a night-time or day-time hour. + // default seems to be day-time = 9am-6pm + // weekday usage + wd_heat_point: 16, + wd_cool_point: 29, + wd_occupancy: 0.10, + wd_appliance: 0.10, + wd_lighting: 0.10, + // weekend usage + we_heat_point: 16, + we_cool_point: 29, + we_occupancy: 0.10, + we_appliance: 0.10, + we_lighting: 0.10 + } +]; + +// should be an array with each element corresponding to a month of year (0=jan, 11=dec) +// each entry is an object with the relevant data for the given month +let CLIMATE = [ + { + mon: "jan", + temp: 4, + wind_speed: 4.6, + avg_solar_S: 166, + avg_solar_SE: 119, + avg_solar_E: 64, + avg_solar_NE: 33, + avg_solar_N: 30, + avg_solar_NW: 38, + avg_solar_W: 85, + avg_solar_SW: 144, + avg_solar_HOR: 116, + solar_altitude: 22.4, + dry_air_humidity: 3.60, + atmospheric_pressure: 98645, + relative_humidity: 65, + dew_point: -3.033512786, + sky_cover: 0.616689098, + sky_temp: 260.3223678, + solar_transmittance: 0.92 + } +]; diff --git a/src/model/dynamic_parameters.js b/src/model/dynamic_parameters.js new file mode 100644 index 0000000..43f3a27 --- /dev/null +++ b/src/model/dynamic_parameters.js @@ -0,0 +1,56 @@ + +// Clause 12.x - Dynamic Parameters + + +// 12.2.1.1 (page 62) gainUtilizationFactor +function gainUtilizationFactor(heatBalanceRatio, heatingHeatGains, heatingHeatTransfer) { + // next calculate the building time constant + let buildingTimeConstant = buildingTimeConstant(); + + // finally we calculate the building inertia coefficient + let buildingInertia = buildingInertia(buildingTimeConstant); + + // depending on our heat balance ratio, determine the final approach + if (heatBalanceRatio === 1) { + // == buildingInertia / (buildingInertia + 1) + } else if (heatBalanceRatio > 0) { + // == 1 / buildingInertia + } else { + // == (1 - heatBalanceRatio^buildingInertia) / (1 - heatBalanceRatio^(buildingInertia + 1)) + } +} + +// Building Inertia - 12.2.1.1/12.2.1.2 (page 62,64) +function buildingInertia(buildingTimeConstant) { + // NOTE: these constants are specific to MONTHLY calculations + return 1.0 + (buildingTimeConstant / 15); +} + +// Internal heat capacity of the building - 12.3.1.1 (page 67) +// unit = J/K (joules/kelvin) +function internalHeatCapacity(buildingEnvelope) { + // sum the internal heat capacity of each building element in contact w/ internal air + + // defaults for element heat capacities + // very light = 80000 * areaOfElement + // light = 110000 * areaOfElement + // medium = 165000 * areaOfElement + // heavy = 260000 * areaOfElement + // very heavy = 370000 * areaOfElement +} + +// Building Time Constant - 12.2.1.3 (page 66) +// Characterizes the internal thermal inertia of the conditioned zone +// == (internalHeatCapacity * 3600) / (heatTransferCoefficientByTransmissionAdjusted + heatTransferCoefficientByVentilationAdjusted) +// unit == hours +function buildingTimeConstant(internalHeatCapacity, representativeHeatTransferCoefficientByTransmission, representativeHeatTransferCoefficientByVentilation) { + // the values for heat transfer coefficients used here are meant to be representative of the dominating season, + // so it should be values for a mid-winter month in the case of a heating dominated climate, or it should be a mid-summer + // month in the case of a cooling dominated climate + return (internalHeatCapacity / 3600) / (representativeHeatTransferCoefficientByTransmission + representativeHeatTransferCoefficientByVentilation); +} + +// 12.2.1.2 (page 72) lossUtilizationFactor +function lossUtilizationFactor(heatBalanceRatio) { + +} diff --git a/src/model/internal.js b/src/model/internal.js new file mode 100644 index 0000000..8bbff81 --- /dev/null +++ b/src/model/internal.js @@ -0,0 +1,52 @@ + +// Clause 10.x - Internal Heat Gains (pages 47-53) + + +// 10.4.2/10.4.3 - Heat gains from internal occupants, appliances, and lighting (page 50) +// unit = watts +// +// Input: +// * an object containing the general usage factors for the building at the given time +// * an object containing global building settings +// +// Output: +// * object +export function heatFlowRates(conditions, settings) { + // all of these heat flow rates are effectively calculated as (use_factor * load) + // we intend to calculate rates for: human occupancy, appliances, and lighting + + // if no daylighting factor is defined then default to 1.0 + let daylighting_factor = settings.daylighting_factor ? settings.daylighting_factor : 1.0; + + // our lighting load depends on several different factors from the input so + // put that all together here so it's easier to read. + let lighting_load = settings.lighting_load * daylighting_factor * settings.lighting_occupancy_factor * settings.constant_illumination_factor; + + // the parasitic lighting load is given to us annually as kWh/m2/yr + // so we need to convert that into W/m2 to match everything else + let parasitic_lighting_load = (settings.annual_parasitic_load*1000/8760); + + // calculate all the individual rates for both weekdays and weekends + let output = { + // Weekday (Mon-Fri) + occupancy_rate_wd: conditions.wd_occupancy * (settings.metabolic_load / settings.occupant_density), + appliance_rate_wd: conditions.wd_appliance * settings.appliance_load, + lighting_rate_wd: conditions.wd_lighting * lighting_load + parasitic_lighting_load, + + // Weekend (Sat-Sun) + occupancy_rate_we: conditions.we_occupancy * (settings.metabolic_load / settings.occupant_density), + appliance_rate_we: conditions.we_appliance * settings.appliance_load, + lighting_rate_we: conditions.we_lighting * lighting_load + parasitic_lighting_load + }; + + // add on our total heat flow rates which is our ultimate aim + // total rate = occupancy rate + appliances rate + lighting rate + output.total_rate_wd = output.occupancy_rate_wd + output.appliance_rate_wd + output.lighting_rate_wd; + output.total_rate_we = output.occupancy_rate_we + output.appliance_rate_we + output.lighting_rate_we; + + // and lastly lets calculate the total internal gains by multiply the heat flow rate against the floor area + output.gains_wd = output.total_rate_wd * settings.floor_area; + output.gains_we = output.total_rate_we * settings.floor_area; + + return output; +} diff --git a/src/model/model.js b/src/model/model.js new file mode 100644 index 0000000..59fdc9d --- /dev/null +++ b/src/model/model.js @@ -0,0 +1,362 @@ +import {heatFlowRates} from "model/internal"; +import { + effectiveSolarCollectingAreaForGlazedElement, + effectiveSolarCollectingAreaForOpaqueElement, + solarHeatFlowRateForElement, + thermalRadiationToSky +} from "model/solar"; + +import {surfaceHeatResistence} from "util/climate"; + + +// ISO 13790-2008 + +// basic data about the months of the year +// NOTE: weekend_cnt is the number of days of the month which fall on a weekend (sat/sun) +const MONTHS = { + 1: {days: 31, hours: 744, seconds: 2678400, weekend_cnt: 9}, + 2: {days: 28, hours: 672, seconds: 2419200, weekend_cnt: 8}, + 3: {days: 31, hours: 744, seconds: 2678400, weekend_cnt: 8}, + 4: {days: 30, hours: 720, seconds: 2592000, weekend_cnt: 10}, + 5: {days: 31, hours: 744, seconds: 2678400, weekend_cnt: 8}, + 6: {days: 30, hours: 720, seconds: 2592000, weekend_cnt: 8}, + 7: {days: 31, hours: 744, seconds: 2678400, weekend_cnt: 10}, + 8: {days: 31, hours: 744, seconds: 2678400, weekend_cnt: 8}, + 9: {days: 30, hours: 720, seconds: 2592000, weekend_cnt: 9}, + 10: {days: 31, hours: 744, seconds: 2678400, weekend_cnt: 9}, + 11: {days: 30, hours: 720, seconds: 2592000, weekend_cnt: 8}, + 12: {days: 31, hours: 744, seconds: 2678400, weekend_cnt: 10} +}; + + +// Total Calculation Steps +// * transmission heat transfer coefficient (building elements) +// * total heat capacity (basic settings) +// * internal gains (basic usage inputs) +// * solar gains (climate & building elements) + +// * set points +// * transmission heat transfer +// * ventilation heat transfer + +// * ventilation heat transfer coefficient +// * gain/loss utilization factors +// * heat gain/loss ratios +// * building time constant +// * + + +// Clause 7.x - Building energy need for space heating and cooling (pages 21-32) + +// 7.1 - Overall calculation +function energyDemand() { + // some of the final calculations require the ability to reference values from + // different months for things like our dynamic parameters, so what we do is + // take 2 passes over calculations for each month. the first pass calculates + // everything that we can safely calculate without any other dependencies. then + // the second pass uses the data from the first pass to finish calculating the + // things that have dependencies. lastly we pull it all together + + // start with a simple data structure we'll use for our model output + // we want to have results for each month of the year plus a "global" + // results section which has values which are constant across months + let results = {}; + + // calculate individual energy demand values for each month + for(let month in MONTHS) { + results[month.id] = basicEnergyDemands(month, setPoint, climate, buildingEnvelope, airflowElements, components, elements); + } + + // 12.2.1.1/12.2.1.2 - heat balance ratios + let heatBalanceRatioHeating = heatingHeatGain / coolingHeatTransfer; + let heatBalanceRatioCooling = coolingHeatGain / coolingHeatTransfer; + + // calculate global dynamic parameters, which are dependent on some of the values we just calculated above + // NOTE: we need to identify if we are in a heating or cooling dominated climate + // and then pull out the relevant data for a representative month depending on which + // see 12.2.1.3 (page 66) regarding the Building Time Constant to better understand why + // internalHeatCapacityOfBuilding + let buildingTimeConstant = buildingTimeConstant(); + let gainUtilizationFactor = gainUtilizationFactor(heatingHeatTransfer, heatingHeatGain, buildingTimeConstant); + let lossUtilizationFactor = lossUtilizationFactor(coolingHeatTransfer, coolingHeatGain, buildingTimeConstant); + + // claculate total heating/cooling loads monthly now that we have our dynamic parameters + for(let month in MONTHS) { + results[month.id].energyForHeating = energyForHeating(results[month.id], gainUtilizationFactor); + results[month.id].energyForCooling = energyForCooling(results[month.id], lossUtilizationFactor); + } + + // 7.4 - Length of heating and cooling seasons + // * for each month, ratio of energy need for heating and cooling +} + +// 7.2.1.1 - Energy need for heating +// +// Required: +// * basic energy demand calculations for the given time period +// * gain utilization factor for the given building definition +function energyForHeating(data, gainUtilizationFactor) { + return data.heatingHeatTransfer - (gainUtilizationFactor * data.heatingHeatGain); +} + +// 7.2.1.2 - Energy need for cooling +// +// Required: +// * basic energy demand calculations for the given time period +// * loss utilization factor for the given building definition +function energyForCooling(data, lossUtilizationFactor) { + return data.coolingHeatGain - (lossUtilizationFactor * data.coolingHeatTransfer); +} + + +function basicEnergyDemands(month, setPointHeating, setPointCooling, climate, buildingEnvelope, airflowElements, components, elements) { + // 8.x, 9.x, 10.x, 11.x - individual calculations for transfers and gains + let transmissionTransfer = heatTransferByTransmission(month, setPointHeating, setPointCooling, climate, buildingEnvelope); + let ventilationTransfer = heatTransferByVentilation(month, setPointHeating, setPointCooling, climate, airflowElements); + let internalGain = heatGainInternal(month, components); + let solarGain = heatGainSolar(month, climate, elements); + + // Total heat transfer + let heatingHeatTransfer = transmissionTransfer.heating + ventilationTransfer.heating; + let coolingHeatTransfer = transmissionTransfer.cooling + ventilationTransfer.cooling; + + // Total heat gain (this is the same in both heating and cooling mode) + let heatingHeatGain = internalGain + solarGain; + let coolingHeatGain = internalGain + solarGain; + + return { + transmissionTransfer, + ventilationTransfer, + internalGain, + solarGain, + heatingHeatTransfer, + coolingHeatTransfer, + heatingHeatGain, + coolingHeatGain + } +} + + + +// Clause 8.x - Heat Transfer by Transmission (pages 33-38) +// unit = megajoules +// +// Input: +// * a specific month of the year (1=January, 12=December) +// * set point of internal enviroment for heating +// * set point of internal enviroment for cooling +// * definition of climate +// * definition of all externally facing building elements (walls, roofs, windows, etc) +// +// NOTE: we are specifically doing the calculations for both heating and cooling +// mode and including both results in the output of this function. +// +// Output: +// * object containing 3 attributes: coefficient, heating, cooling +// +// Example Input: +// transferCoefficient = 18.2 W/K +// january = 2.678400 Ms +// set points = 20C for heating, 26C for cooling +// external temp january = 3.2C +// +// Example Output: +// { +// coefficient: 18.2, +// heating: 818.947584, +// cooling: 1111.428864 +// } +export function heatTransferTransmission(month, setPointHeating, setPointCooling, climate, buildingElements) { + // filter down our building envelope elements to just the external facing + // walls and windows which affect transmission + let walls = buildingElements.filter((elem) => elem.type === "wall" || elem.type === "roof"); + let windows = buildingElements.filter((elem) => elem.type === "window"); + + // calculate the transfer coefficient for the given building definition + let transferCoefficient = heatTransferByTransmissionCoefficient(walls, windows); + + // other values we need for the calculations + let climateExternalTemp = climate[month].temp; + let timePeriodSeconds = MONTHS[month].seconds; + + // run the calculation for both heating and cooling modes + return { + coefficient: transferCoefficient, + heating: heatTransferByTransmission(transferCoefficient, setPointHeating, climateExternalTemp, timePeriodSeconds), + cooling: heatTransferByTransmission(transferCoefficient, setPointCooling, climateExternalTemp, timePeriodSeconds) + } +} + + +// Clause 10.x - Internal Heat Gains (pages 47-53) +// unit = megajoules +// +// Input: +// * a specific month of the year (1=January, 12=December) +// * an array of hourly usage conditions for the building (24 entries) +// * an object containing the global building settings +// +// NOTE: we are explicitly skipping the portion of this calculation that is due +// to unconditioned adjacent spaces, which is considered unnecessary +// +// Output: +// { +// occupancy_rate: 123, (watts) +// occupancy: 456, (megajoules) +// appliance_rate: 123, (watts) +// appliance: 456, (megajoules) +// lighting_rate: 123, (watts) +// lighting: 456, (megajoules) +// total_rate: 369, (watts) +// total: 1368 (megajoules) +// } +// +export function heatGainInternal(month, hourlyConditions, settings) { + // calculate out the internal heat flow rates for each hour of a representative day + let hourlyFlowRates = hourlyConditions.map((conditions) => heatFlowRates(conditions, settings)); + + let output = { + occupancy_rate: avgFlowRate(month, hourlyFlowRates, settings.floor_area, "occupancy"), + appliance_rate: avgFlowRate(month, hourlyFlowRates, settings.floor_area, "appliance"), + lighting_rate: avgFlowRate(month, hourlyFlowRates, settings.floor_area, "lighting"), + }; + + output.total_rate = output.occupancy_rate + output.appliance_rate + output.lighting_rate; + + // final monthly gains is given by multiplying the avg by the timeperiod in megaseconds + output.occupancy = output.occupancy_rate * (MONTHS[month].seconds / 1000000); + output.appliance = output.appliance_rate * (MONTHS[month].seconds / 1000000); + output.lighting = output.lighting_rate * (MONTHS[month].seconds / 1000000); + output.total = output.total_rate * (MONTHS[month].seconds / 1000000); + + return output; +} + +// this helps us take the monthly time averaged heat flow rate for a given category +function avgFlowRate(month, flowRates, floorArea, attr) { + let avgRateWeekday = flowRates.reduce((val, data) => val + data[attr+"_rate_wd"], 0)/24; + let avgRateWeekend = flowRates.reduce((val, data) => val + data[attr+"_rate_we"], 0)/24; + let totalWeekdayRate = (MONTHS[month].days - MONTHS[month].weekend_cnt) * avgRateWeekday; + let totalWeekendRate = MONTHS[month].weekend_cnt * avgRateWeekend; + return ((totalWeekdayRate + totalWeekendRate) / MONTHS[month].days) * floorArea; +} + + +// Clause 11.x - Solar Heat Gains (pages 53-61) +// unit = megajoules +// +// Input: +// * a specific month of the year (1=January, 12=December) +// * definition of climate +// * definition of all externally facing building elements (walls, roofs, windows, etc) +// +// NOTE: we are explicitly skipping the portion of this calculation that is due +// to unconditioned adjacent spaces, which is considered unnecessary +// +// NOTE: the results for heat gains are the same in heating in cooling mode, so +// this calculation does not distinguish between them. +// +// Output: +// * solar gains (megajoules) +// +// Intermediate calculations: +// * the "effective collecting area" of the opaque part of an element can be +// calculated once we know its dimensions and assembly it uses. +// * we can determine the "frame area fraction" of a glazing element once we +// have the area information about the frame and overall window +// +// Climate dependent calculations: +// * the "effective collecting area" of a glazed component requires information +// about the solar irradiance for the appropriate latitude and sun angles. +// * "air temperature delta" requires having average air temps and sky temps. +// * "radiative heat transfer coefficient" requires avg of surface and sky temp. +// +export function heatGainSolar(month, climate, buildingElements) { + // filter down our building envelope elements to just the external facing + // walls and windows which affect transmission + let walls = buildingElements.filter((elem) => elem.type === "wall" || elem.type === "roof"); + let windows = buildingElements.filter((elem) => elem.type === "window"); + // TODO: how to account for "sunspaces"?? + + // TODO: !!!!!!!!!!!!! We may want to invert this calculation procedure + // so that we calculate all months of year grouped by + // element so that we can see how each element performs. + // By summing elements by month we have no insight into + // what parts of a building are underperforming. + + // TODO: shading reduction factor - accounts for effects of external shading. + // by setting this to 1.0 we are saying there is no shading (yet!) + let globalShadeReductionFactor = 1.0; + + // determine the surface heat resistance using the climate wind speed + let surfaceHR = surfaceHeatResistence(climate.wind_speed); + + // WALLS - iterate over all opaque wall definitions and calculate solar gains + let totalWallHeatFlow = 0; + for (var i = 0; i < walls.length; i++) { + let wall = walls[i]; + + // TODO: use the appropriate orientation to determine this + let orient = wall.orientation ? wall.orientation : "HOR"; + let solarIrradiance = climate["avg_solar_"+orient]; + + // NOTE: form factor is 0.5 for walls and 1.0 for roofs + let formFactor = (wall.type === "roof") ? 1.0 : 0.5; + + + // effective collecting area + let effectiveCollectingArea = effectiveSolarCollectingAreaForOpaqueElement(wall.area, wall.u_value, wall.absorptivity, surfaceHR); + + // calculate radiation to sky for our wall + let radiationToSky = thermalRadiationToSky(wall.area, wall.u_value, wall.emissivity, surfaceHR, climate.temp, climate.sky_temp); + + // calculate the total heat flow rate for this element + let heatFlowRate = solarHeatFlowRateForElement(globalShadeReductionFactor, effectiveCollectingArea, solarIrradiance, formFactor, radiationToSky); + + // console.info("wall", orient, " -> ", effectiveCollectingArea, radiationToSky, heatFlowRate); + + // add to total heat flow rate + totalWallHeatFlow += heatFlowRate; + } + + // WINDOWS - iterate over all window definitions and calculate solar gains + let totalWindowHeatFlow = 0; + for (var i = 0; i < windows.length; i++) { + let win = windows[i]; + + // TODO: movable shading factor. we need to account for things like a manual shade on the window + // which is different than the general shade reduction factor. + // NOTE: we can make a total shade reduction coefficient by multiplying + // the temporary shade reduction by the global shade reduction + let shadeReductionFactor = globalShadeReductionFactor * win.reduction_factor_Z_for_temporary; + + // TODO: window frame area fraction is 0.2 for heating-dominated climates + // and 0.3 for cooling-dominated climates per 11.4.5 + let frameAreaFraction = 0.3; + + // TODO: use the appropriate orientation to determine this + let orient = win.orientation ? win.orientation : "HOR"; + let solarIrradiance = climate["avg_solar_"+orient]; + + // NOTE: form factor is 0.5 for walls and 1.0 for roofs + let formFactor = (win.type === "roof") ? 1.0 : 0.5; + + + // effective collecting area + let effectiveCollectingArea = effectiveSolarCollectingAreaForGlazedElement(win.area, frameAreaFraction, climate.solar_transmittance, shadeReductionFactor); + + // calculate radiation to sky for our wall + let radiationToSky = thermalRadiationToSky(win.area, win.u_value, win.emissivity, surfaceHR, climate.temp, climate.sky_temp); + + // total solar gains + let heatFlowRate = solarHeatFlowRateForElement(shadeReductionFactor, effectiveCollectingArea, solarIrradiance, formFactor, radiationToSky); + + // console.info("window", orient, " -> ", effectiveCollectingArea, radiationToSky, heatFlowRate); + + // add to total heat flow rate + totalWindowHeatFlow += heatFlowRate; + } + + // multiply our time-averaged gains against our time period + return (totalWallHeatFlow + totalWindowHeatFlow) * (MONTHS[month].seconds / 1000000); +} diff --git a/src/model/solar.js b/src/model/solar.js new file mode 100644 index 0000000..b5e4b7a --- /dev/null +++ b/src/model/solar.js @@ -0,0 +1,93 @@ + +// Clause 11.x - Solar Heat Gains (pages 53-61) + + +// 11.3.2 - Heat flow by solar gains per building element (page 55) +// unit = watts +// +// Input: +// * shade reduction factor (0-1) +// * effective collecting area (m2) +// * solar irradiance for the given climate time of year & orientation +// * form factor between building element and sky (0-1) +// * radiation to sky (W) +// +// Output: +// * solar gains (watts) +export function solarHeatFlowRateForElement(shadeReductionFactor, effectiveCollectingArea, solarIrradiance, formFactor, radiationToSky) { + // Φsol,k = Fsh,ob,k * Asol,k * Isol,k − (Fr,k * Φr,k) + return (shadeReductionFactor * effectiveCollectingArea * solarIrradiance) - (formFactor * radiationToSky); +} + + +// 11.3.3 - Effective solar collecting area of glazed elements (page 56) +// unit = m2 (square meters) +// +// Input: +// * area of the element (m2) +// * frame area fraction (0-1) +// * solar energy transmittance +// * shade reduction factor (0-1) +// +// Output: +// * effective collecting area (m2) +export function effectiveSolarCollectingAreaForGlazedElement(area, frameAreaFraction, solarTransmittance, shadeReductionFactor) { + // Asol = Fsh,gl * ggl * (1 − FF) * Aw,p + + // 11.4.2 - Solar energy transmittance of glazed elements + // this is a value from 0-1 and is dependent on the orientation of the window + // correction factor for non-scattering window, default = 0.90 + // NOTE: if the window is scattering or has a solar shading provision then this + // calculation should be updated to account that. + // NOTE: the EPC use 0.70 for this value on windows so thats what we are following + let transmittance = 0.70 * solarTransmittance; + + return shadeReductionFactor * transmittance * (1.0 - frameAreaFraction) * area; +} + + +// 11.3.4 - Effective solar collecting area of opaque building elements (page 56) +// unit = m2 (square meters) +// +// Input: +// * area (m2) +// * u-value (thermal transmittance) per ISO 6946 (W/m2*K) +// * thermal absorptivity (W/m2*K) +// * surface heat resistance (m2*K/W) +// +// Output: +// * effective collecting area (m2) +export function effectiveSolarCollectingAreaForOpaqueElement(area, UValue, thermalAbsorptivity, surfaceHeatResistance) { + // Asol = αS,c * Rse * Uc * Ac + return thermalAbsorptivity * surfaceHeatResistance * UValue * area; +} + + +// 11.3.5 - Thermal radiation to the sky (page 57) +// unit = watts +// +// Input: +// * area (m2) +// * u-value (thermal transmittance) per ISO 6946 (W/m2*K) +// * thermal emissivity (W/m2*K) +// * surface heat resistance (m2*K/W) +// * avg ambient air temperature (C) +// * avg sky temperature (C) +// +// Output: +// * thermal radiation to the sky (watts) +// 31.5, 3.09, 0.84, 0.04, 4, 260.32 +export function thermalRadiationToSky(area, UValue, thermalEmissivity, surfaceHeatResistance, avgTemperature, skyTemperature) { + // Φr = Rse * Uc * Ac * hr * ∆θer + + // 11.4.6 - external radiative heat transfer coefficient + // TODO: we can calculate this in a more detailed fashion with more data + // default provided says to use 5.0 * thermal emissivity value + let radiativeHeatCoeff = 5.0 * thermalEmissivity; + + // Average difference between air temperature and sky temperature. + // defaults can be 9K in sub-polar areas, 13K in the tropics, and 11K in intermediate zones. + let skyToAirTemperatureDelta = (avgTemperature + 273.15) - skyTemperature; + + return surfaceHeatResistance * UValue * area * radiativeHeatCoeff * skyToAirTemperatureDelta; +} diff --git a/src/model/transmission.js b/src/model/transmission.js new file mode 100644 index 0000000..e285502 --- /dev/null +++ b/src/model/transmission.js @@ -0,0 +1,95 @@ + +// Clause 8.x - Heat Transfer by Transmission (pages 33-38) + + +// 8.2 - Total heat transfer by transmission (page 33) +// unit = MJ (megajoules) +// +// Input: +// * transmission heat transfer coefficient (watts/kelvin) +// * set point for the internal environment (celsius) +// * avg external climate temperature for the given time period (celsius) +// * time period length (seconds) +// +// Output: +// 818.94 (megajoules) +// +// Calculation Example: +// transferCoefficient = 18.2 W/K +// january = 2.678400 Ms +// set point = 20C for heating +// external temp january = 3.2C +// Result: +// 818.947584 MJ +export function heatTransferByTransmission(transferCoefficient, setPoint, climateExternalTemp, timePeriodSeconds) { + // W/K (watts/kelvin) * C (celsius) * Ms (megasecond) = MJ (megajoule) + return transferCoefficient * (setPoint - climateExternalTemp) * (timePeriodSeconds / 1000000); +} + + +// 8.3 - Transmission heat transfer coefficient (page 34) +// unit = W/K (watts/kelvin) +// +// Input: +// * array of all external facing walls (area & u_value) +// * array of all external facing windows (area & u_value) +// +// Output: +// 18.2 (watts/kelvin) +// +// Calculation Example: +// walls = +// windows = +// Result: +// 18.2 W/K +export function heatTransferByTransmissionCoefficient(walls, windows) { + // overall coefficient is the sum of 4 individual coefficients: + // external air, ground, unconditioned spaces, adjacent spaces + + // coefficient due to external environment + let external = transmissionCoefficientByExternal(walls, windows); + + // TODO: coefficient due to ground + let ground = 0; + + // coefficient due to "unconditioned spaces" (not applicable for single zone) + // coefficient due to "adjacent buildings" (not applicable for single zone) + + return external; +} + + +// The general equation for each individual transmission coefficient is given by: +// Hx = btr,x * [ΣiAi*Ui + Σklk*Ψk + Σjχj] + +// To do this calculation we need to have 3 sets of definitions: +// 1. building elements (i) including area and u-value +// 2. all linear thermal bridges (k) including length and linear thermal transmittence +// 3. all point thermal bridges (j) including point thermal transmittence +// 4. the adjustment factor (b) determined by 8.3.2 + + +// Transmission heat transfer coefficient due to external environment +// unit = watts/kelvin +function transmissionCoefficientByExternal(walls, windows) { + + // wall elements + let wallTransfers = walls.map((wall) => wall.area * wall.u_value); + let wallCoefficient = wallTransfers.reduce((a, b) => a + b, 0); + + // window (glazed) elements + let windowTransfers = windows.map((win) => win.area * win.u_value); + let windowCoefficient = windowTransfers.reduce((a, b) => a + b, 0); + + // 8.3.2.2.1 provides an equation which adjusts the u-value for a window if + // that window uses shutters intermittently over the time period. + // See G.2.2.2 for greater details and an example + + // TODO: calculate transfer due to thermal bridges + // TODO: calculate transfer due to point thermal bridges + + // NOTE: there is NO adjustment factor in the case where the transmittence + // is happening to the external air, so we skip that in this calc + + return wallCoefficient + windowCoefficient; +} diff --git a/src/model/ventilation.js b/src/model/ventilation.js new file mode 100644 index 0000000..efbdee5 --- /dev/null +++ b/src/model/ventilation.js @@ -0,0 +1,92 @@ + +// Clause 9.x - Heat Transfer by Ventilation (pages 38-46) +// unit = megajoules +// +// Required: +// * definition of climate +// * definition of all air flow building elements (air infiltration, natural ventilation, etc) +// * a specific month of the year (1=January, 12=December) +// * set point for internal enviroment in heating and cooling mode +// +// NOTE: we are specifically doing the calculations for both heating and cooling +// mode and including both results in the output of this function. +// +// Output: +// { +// coefficient: 7.7, +// heating: 346.47, +// cooling: 470.21 +// } +// +// Calculation Example: +// transferCoefficient = 7.7 W/K +// january = 2.678400 Ms +// set points = 20C for heating, 26C for cooling +// external temp january = 3.2C +// Result: +// heating = 346.477824 MJ (96 kWh) +// cooling = 470.219904 MJ (131 kWh) +export function heatTransferByVentilation(month, setPointHeating, setPointCooling, climate, airflowElements) { + // calculate the transfer coefficient for the given building definition + let transferCoefficient = heatTransferCoefficientByVentilation(airflowElements); + + // other values we need for the calculations + let climateExternalTemp = climate[month].external_temp; + let timePeriodSeconds = MONTHS[month].seconds; + + // we run the calculation below for both set points! + return { + coefficient: transferCoefficient, + heating: heatTransferByVentilationInternal(transferCoefficient, setPointHeating, climateExternalTemp, timePeriodSeconds), + cooling: heatTransferByVentilationInternal(transferCoefficient, setPointCooling, climateExternalTemp, timePeriodSeconds) + } +} + +// 9.2 - Total heat transfer by ventilation +function heatTransferByVentilationInternal(transferCoefficient, setPoint, climateExternalTemp, timePeriodSeconds) { + // W/K (watts/kelvin) * C (celsius) * Ms (megasecond) = MJ (megajoule) + // TODO: can we safely use farenheight here? or is celsius required? + return transferCoefficient * (setPoint - climateExternalTemp) * (timePeriodSeconds / 1000000); +} + + +// Ventilation heat transfer coefficient - 9.3 (page 39) +// +// Required: +// * list of each airflow element +// +// unit = watts/kelvin +function heatTransferByVentilationCoefficient(airflowElements) { + // calculate transfer flows for each airflow element + let elementFlows = airflowElements.map((element) => ventilationAirflowForElement(element)); + + // sum all the individual flows together + let totalFlows = elementFlows.reduce((a, b) => a + b, 0); + + // multiply the sum of our flows against the constant for the heat capacity of air per volume + return 1200 * totalFlows; +} + +function ventilationAirflowForElement(element) { + // for the temperature adjustment factor: + // if the air comes from the outside air then the value = 1 + // if there is a heat recovery unit then there is a more detailed equation (9.3.3.8) + // if element is a mechanical vent w/ central pre-heating/pre-cooling + // another option is free night cooling + + // 9.3.3 - Temperature adjustment factor for airflow element + // 9.3.3.1 - we may need to use different factors for heating/cooling modes + // 9.3.3.3 - the adjustment factor is 1 when the supply air is from external + // 9.3.3.4 - adjacent unconditioned space (N/A) + // 9.3.3.5 - adjacent sunspaces (N/A) + // 9.3.3.6 - adjacent buildings (N/A) + // 9.3.3.8 - heat recovery unit = 1 - airflowFraction * recoveryUnitEfficiency + // TODO: it's also possible to have 2 recovery units in series + // 9.3.3.9 - central pre-heating/pre-cooling + // 9.3.3.10 - free cooling and night-time ventilation + // 9.3.3.12 - Energy need for central pre-heating or pre-cooling + let temperatureAdjustmentFactor = 1; + + // NOTE: we should be able to pre-calc the time-average airflow rate of the element + return temperatureAdjustmentFactor * element.fractionOfOperation * element.airflowRate; +} diff --git a/src/util/building.js b/src/util/building.js new file mode 100644 index 0000000..57b1de3 --- /dev/null +++ b/src/util/building.js @@ -0,0 +1,22 @@ + +export function internalHeatCapacityByType(type) { + // Taken from 12.3.1.2 (page 68) + // Class Am factor Seasonal, Monthly Hourly Cm (J/K) + // Very Light: 80,000 * Af 2.5 80,000 *Af + // Light : 110,000 * Af 2.5 110,000 *Af + // Medium: 165,000 * Af 2.5 165,000 *Af + // Heavy: 260,000 * Af 3 260,000 *Af + // Very heavy: 370,000 * Af 3.5 370,000 *Af + if (type === "vlight") { + return 80000; + } else if (type === "light") { + return 110000; + } else if (type === "heavy") { + return 260000; + } else if (type === "vheavy") { + return 370000; + } else { + // default is "medium" + return 165000; + } +} diff --git a/src/util/climate.js b/src/util/climate.js new file mode 100644 index 0000000..1323d2d --- /dev/null +++ b/src/util/climate.js @@ -0,0 +1,28 @@ + +export function surfaceHeatResistence(windSpeed) { + // The external surface resistance + // Wind speed -> Rse + // 1 0.08 + // 2 0.06 + // 3 0.05 + // 4 0.04 + // 5 0.04 + // 7 0.03 + // 10 0.02 + + if (windSpeed <= 1.5) { + return 0.08; + } else if (windSpeed <= 2.5) { + return 0.06; + } else if (windSpeed <= 3.5) { + return 0.05; + } else if (windSpeed <= 4.5) { + return 0.04; + } else if (windSpeed <= 6.0) { + return 0.04; + } else if (windSpeed <= 8.5) { + return 0.03; + } else { + return 0.02; + } +} diff --git a/src/util/math.js b/src/util/math.js new file mode 100644 index 0000000..b8f7585 --- /dev/null +++ b/src/util/math.js @@ -0,0 +1,20 @@ +import _ from "underscore"; + + +function roundNumber(number, precision) { + var factor = Math.pow(10, precision); + var tempNumber = number * factor; + var roundedTempNumber = Math.round(tempNumber); + return roundedTempNumber / factor; +}; + + +export let MathHelper = { + round: roundNumber, + + roundValues: function(obj, precision) { + return _.mapObject(obj, function (v, k) { + return roundNumber(v, precision); + }); + } +}; diff --git a/test/model/internal.spec.js b/test/model/internal.spec.js new file mode 100644 index 0000000..eaf7e7b --- /dev/null +++ b/test/model/internal.spec.js @@ -0,0 +1,69 @@ +import {heatFlowRates} from 'model/internal'; +import {MathHelper} from 'util/math'; + +function roundOutputs(output) { + return { + occupancy_rate_wd: MathHelper.round(output.occupancy_rate_wd, 2), + appliance_rate_wd: MathHelper.round(output.appliance_rate_wd, 2), + lighting_rate_wd: MathHelper.round(output.lighting_rate_wd, 2), + total_rate_wd: MathHelper.round(output.total_rate_wd, 2), + gains_wd: MathHelper.round(output.gains_wd, 0), + + occupancy_rate_we: MathHelper.round(output.occupancy_rate_we, 2), + appliance_rate_we: MathHelper.round(output.appliance_rate_we, 2), + lighting_rate_we: MathHelper.round(output.lighting_rate_we, 2), + total_rate_we: MathHelper.round(output.total_rate_we, 2), + gains_we: MathHelper.round(output.gains_we, 0) + } +} + + +describe("Internal Gains", function() { + let conditions = { + // NOTE: it is important to be able to tell if a given hour of the day + // is considered a night-time or day-time hour. + // default seems to be day-time = 9am-6pm + // weekday usage + wd_heat_point: 16, + wd_cool_point: 29, + wd_occupancy: 0.10, + wd_appliance: 0.10, + wd_lighting: 0.10, + // weekend usage + we_heat_point: 16, + we_cool_point: 29, + we_occupancy: 1.0, + we_appliance: 1.0, + we_lighting: 1.0 + }; + + let settings = { + floor_area: 336, // m2 + occupants: 17, // people + occupant_density: 20.0, // m2/person (this can be area/occupants) + metabolic_load: 88, // W/person + appliance_load: 10.0, // W/m2 + lighting_load: 10.0, // W/m2 + daylighting_factor: 1, + lighting_occupancy_factor: 1, + constant_illumination_factor: 1, + annual_parasitic_load: 6 + }; + + // Heat flow rates (occupancy, appliances, lighting) + it("can calculate heat flow rates for occupancy, appliances, and lighting", function() { + expect(roundOutputs(heatFlowRates(conditions, settings))).toEqual({ + occupancy_rate_wd: 0.44, + appliance_rate_wd: 1.0, + lighting_rate_wd: 1.68, + total_rate_wd: 3.12, + gains_wd: 1050, + + occupancy_rate_we: 4.40, + appliance_rate_we: 10.0, + lighting_rate_we: 10.68, + total_rate_we: 25.08, + gains_we: 8429 + }); + }); +}); diff --git a/test/model/model.spec.js b/test/model/model.spec.js new file mode 100644 index 0000000..7cb195d --- /dev/null +++ b/test/model/model.spec.js @@ -0,0 +1,58 @@ +import {heatGainInternal, heatGainSolar} from "model/model"; +import {MathHelper} from "util/math"; + +import { + buildingSettings, + buildingElements, + climate, + dailyUsage, + + // results + internalGains +} from "testdata/epc"; + + +describe("Heat Transfer Transmission", function() { + it("can calculate heat transfer transmission monthly", function() { + expect(MathHelper.round(heatGainSolar(1, climate[0], buildingElements), 0)).toEqual(9903); + expect(MathHelper.round(heatGainSolar(2, climate[1], buildingElements), 0)).toEqual(9470); + expect(MathHelper.round(heatGainSolar(3, climate[2], buildingElements), 0)).toEqual(12339); + expect(MathHelper.round(heatGainSolar(4, climate[3], buildingElements), 0)).toEqual(11950); + expect(MathHelper.round(heatGainSolar(5, climate[4], buildingElements), 0)).toEqual(11947); + expect(MathHelper.round(heatGainSolar(6, climate[5], buildingElements), 0)).toEqual(11003); + expect(MathHelper.round(heatGainSolar(7, climate[6], buildingElements), 0)).toEqual(11586); + expect(MathHelper.round(heatGainSolar(8, climate[7], buildingElements), 0)).toEqual(12488); + expect(MathHelper.round(heatGainSolar(9, climate[8], buildingElements), 0)).toEqual(10802); + expect(MathHelper.round(heatGainSolar(10, climate[9], buildingElements), 0)).toEqual(11365); + expect(MathHelper.round(heatGainSolar(11, climate[10], buildingElements), 0)).toEqual(10180); + expect(MathHelper.round(heatGainSolar(12, climate[11], buildingElements), 0)).toEqual(9594); + }); +}); + + +describe("Internal Gains", function() { + it("can calculate internal gains monthly", function() { + for (var mon = 0; mon < 12; mon++) { + expect(MathHelper.roundValues(heatGainInternal(mon+1, dailyUsage, buildingSettings), 0)).toEqual(internalGains[mon]); + } + }); +}); + + +describe("Solar Gains", function() { + it("can calculate solar gains monthly", function() { + // NOTE: we diverge from the EPC data here because I believe they have some rounding errors in their sheet + expect(MathHelper.round(heatGainSolar(1, climate[0], buildingElements), 0)).toEqual(9903); + expect(MathHelper.round(heatGainSolar(2, climate[1], buildingElements), 0)).toEqual(9470); + expect(MathHelper.round(heatGainSolar(3, climate[2], buildingElements), 0)).toEqual(12339); + expect(MathHelper.round(heatGainSolar(4, climate[3], buildingElements), 0)).toEqual(11950); + expect(MathHelper.round(heatGainSolar(5, climate[4], buildingElements), 0)).toEqual(11947); + expect(MathHelper.round(heatGainSolar(6, climate[5], buildingElements), 0)).toEqual(11003); + expect(MathHelper.round(heatGainSolar(7, climate[6], buildingElements), 0)).toEqual(11586); + expect(MathHelper.round(heatGainSolar(8, climate[7], buildingElements), 0)).toEqual(12488); + expect(MathHelper.round(heatGainSolar(9, climate[8], buildingElements), 0)).toEqual(10802); + expect(MathHelper.round(heatGainSolar(10, climate[9], buildingElements), 0)).toEqual(11365); + expect(MathHelper.round(heatGainSolar(11, climate[10], buildingElements), 0)).toEqual(10180); + expect(MathHelper.round(heatGainSolar(12, climate[11], buildingElements), 0)).toEqual(9594); + }); +}); diff --git a/test/model/solar.spec.js b/test/model/solar.spec.js new file mode 100644 index 0000000..c8a923f --- /dev/null +++ b/test/model/solar.spec.js @@ -0,0 +1,30 @@ +import { + solarHeatFlowRateForElement, + effectiveSolarCollectingAreaForGlazedElement, + effectiveSolarCollectingAreaForOpaqueElement, + thermalRadiationToSky +} from 'model/solar'; +import {MathHelper} from "util/math"; + + +describe("Solar Gains", function() { + // Radiation to Sky + it("can calculate thermal radiation to sky", function() { + expect(MathHelper.round(thermalRadiationToSky(31.5, 3.09, 0.84, 0.04, 4, 260.32), 2)).toBe(275.21); + }); + + // Opaque Element + it("can calculate effective collecting area (opaque)", function() { + expect(MathHelper.round(effectiveSolarCollectingAreaForOpaqueElement(73.5, 0.35, 0.7, 0.04), 2)).toBe(0.72); + }); + + // Glazed Element + it("can calculate effective collecting area (glazed)", function() { + expect(MathHelper.round(effectiveSolarCollectingAreaForGlazedElement(31.5, 0.3, 0.92, 1.0), 2)).toBe(14.20); + }); + + // Heat Flow Rate + it("can calculate solar heat flow rate", function() { + expect(MathHelper.round(solarHeatFlowRateForElement(1.0, 14.20, 166, 0.5, 275.21), 2)).toBe(2219.59); + }); +}); diff --git a/test/model/transmission.spec.js b/test/model/transmission.spec.js new file mode 100644 index 0000000..42867b7 --- /dev/null +++ b/test/model/transmission.spec.js @@ -0,0 +1,15 @@ +import { + heatTransferByTransmissionCoefficient +} from "model/transmission"; +import {MathHelper} from "util/math"; + +import {buildingElements} from "testdata/epc"; + +describe("Transmission Heat Transfer", function() { + it("can calculate heat transfer coefficient", function() { + let walls = buildingElements.filter((elem) => elem.type === "wall" || elem.type === "roof"); + let windows = buildingElements.filter((elem) => elem.type === "window"); + + expect(MathHelper.round(heatTransferByTransmissionCoefficient(walls, windows), 2)).toBe(414.76); + }); +}); diff --git a/test/testdata/epc.js b/test/testdata/epc.js new file mode 100644 index 0000000..27053f2 --- /dev/null +++ b/test/testdata/epc.js @@ -0,0 +1,410 @@ + +// RESULTS +export let internalGains = [ + {occupancy_rate: 541, appliance_rate: 1230, lighting_rate: 1460, total_rate: 3232, occupancy: 1450, appliance: 3295, lighting: 3911, total: 8656}, + {occupancy_rate: 544, appliance_rate: 1236, lighting_rate: 1466, total_rate: 3246, occupancy: 1316, appliance: 2990, lighting: 3547, total: 7853}, + {occupancy_rate: 559, appliance_rate: 1271, lighting_rate: 1501, total_rate: 3331, occupancy: 1498, appliance: 3404, lighting: 4020, total: 8922}, + {occupancy_rate: 517, appliance_rate: 1176, lighting_rate: 1406, total_rate: 3100, occupancy: 1341, appliance: 3048, lighting: 3645, total: 8034}, + {occupancy_rate: 559, appliance_rate: 1271, lighting_rate: 1501, total_rate: 3331, occupancy: 1498, appliance: 3404, lighting: 4020, total: 8922}, + {occupancy_rate: 554, appliance_rate: 1260, lighting_rate: 1490, total_rate: 3305, occupancy: 1437, appliance: 3266, lighting: 3862, total: 8565}, + {occupancy_rate: 523, appliance_rate: 1190, lighting_rate: 1420, total_rate: 3133, occupancy: 1402, appliance: 3186, lighting: 3802, total: 8390}, + {occupancy_rate: 559, appliance_rate: 1271, lighting_rate: 1501, total_rate: 3331, occupancy: 1498, appliance: 3404, lighting: 4020, total: 8922}, + {occupancy_rate: 536, appliance_rate: 1218, lighting_rate: 1448, total_rate: 3202, occupancy: 1389, appliance: 3157, lighting: 3754, total: 8300}, + {occupancy_rate: 541, appliance_rate: 1230, lighting_rate: 1460, total_rate: 3232, occupancy: 1450, appliance: 3295, lighting: 3911, total: 8656}, + {occupancy_rate: 554, appliance_rate: 1260, lighting_rate: 1490, total_rate: 3305, occupancy: 1437, appliance: 3266, lighting: 3862, total: 8565}, + {occupancy_rate: 523, appliance_rate: 1190, lighting_rate: 1420, total_rate: 3133, occupancy: 1402, appliance: 3186, lighting: 3802, total: 8390} +]; + + +// INPUTS +export let buildingSettings = { + floor_area: 336, // m2 + occupants: 17, // people + occupant_density: 20.0, // m2/person (this can be area/occupants) + metabolic_load: 88, // W/person + appliance_load: 10.0, // W/m2 + lighting_load: 10.0, // W/m2 + daylighting_factor: 1, + lighting_occupancy_factor: 1, + constant_illumination_factor: 1, + annual_parasitic_load: 6 +}; + +// weekday and weekend usage factors for each hour of a representative day +export let dailyUsage = [ + {wd_heat_point: 16.0, wd_cool_point: 29.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 0.10, wd_appliance: 0.10, wd_lighting: 0.10, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 16.0, wd_cool_point: 29.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 0.10, wd_appliance: 0.10, wd_lighting: 0.10, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 16.0, wd_cool_point: 29.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 0.10, wd_appliance: 0.10, wd_lighting: 0.10, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 16.0, wd_cool_point: 29.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 0.10, wd_appliance: 0.10, wd_lighting: 0.10, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 16.0, wd_cool_point: 29.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 0.10, wd_appliance: 0.10, wd_lighting: 0.10, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 16.0, wd_cool_point: 29.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 0.10, wd_appliance: 0.10, wd_lighting: 0.10, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 16.0, wd_cool_point: 29.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 0.10, wd_appliance: 0.10, wd_lighting: 0.10, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 16.0, wd_cool_point: 29.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 0.10, wd_appliance: 0.10, wd_lighting: 0.10, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 21.0, wd_cool_point: 24.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 1.0, wd_appliance: 1.0, wd_lighting: 1.0, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 21.0, wd_cool_point: 24.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 1.0, wd_appliance: 1.0, wd_lighting: 1.0, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 21.0, wd_cool_point: 24.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 1.0, wd_appliance: 1.0, wd_lighting: 1.0, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 21.0, wd_cool_point: 24.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 1.0, wd_appliance: 1.0, wd_lighting: 1.0, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 21.0, wd_cool_point: 24.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 1.0, wd_appliance: 1.0, wd_lighting: 1.0, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 21.0, wd_cool_point: 24.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 1.0, wd_appliance: 1.0, wd_lighting: 1.0, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 21.0, wd_cool_point: 24.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 1.0, wd_appliance: 1.0, wd_lighting: 1.0, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 21.0, wd_cool_point: 24.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 1.0, wd_appliance: 1.0, wd_lighting: 1.0, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 21.0, wd_cool_point: 24.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 1.0, wd_appliance: 1.0, wd_lighting: 1.0, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 21.0, wd_cool_point: 24.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 1.0, wd_appliance: 1.0, wd_lighting: 1.0, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 16.0, wd_cool_point: 29.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 0.10, wd_appliance: 0.10, wd_lighting: 0.10, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 16.0, wd_cool_point: 29.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 0.10, wd_appliance: 0.10, wd_lighting: 0.10, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 16.0, wd_cool_point: 29.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 0.10, wd_appliance: 0.10, wd_lighting: 0.10, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 16.0, wd_cool_point: 29.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 0.10, wd_appliance: 0.10, wd_lighting: 0.10, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 16.0, wd_cool_point: 29.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 0.10, wd_appliance: 0.10, wd_lighting: 0.10, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10}, + {wd_heat_point: 16.0, wd_cool_point: 29.0, we_heat_point: 16.0, we_cool_point: 29.0, wd_occupancy: 0.10, wd_appliance: 0.10, wd_lighting: 0.10, we_occupancy: 0.10, we_appliance: 0.10, we_lighting: 0.10} +]; + + +// definition of external facing building walls and windows +export let buildingElements = [ + { + type: "wall", + orientation: "S", + area: 73.5, + u_value: 0.35, + absorptivity: 0.7, + emissivity: 0.7 + }, + { + type: "window", + orientation: "S", + area: 31.5, + u_value: 3.09, + emissivity: 0.84, + solar_transmittance: 0.7, + reduction_factor_Z_for_temporary: 1 + }, + { + type: "wall", + orientation: "E", + area: 42.0, + u_value: 0.35, + absorptivity: 0.7, + emissivity: 0.7 + }, + { + type: "window", + orientation: "E", + area: 18.0, + u_value: 3.09, + emissivity: 0.84, + solar_transmittance: 0.7, + reduction_factor_Z_for_temporary: 1 + }, + { + type: "wall", + orientation: "N", + area: 73.5, + u_value: 0.35, + absorptivity: 0.7, + emissivity: 0.7 + }, + { + type: "window", + orientation: "N", + area: 31.5, + u_value: 3.09, + emissivity: 0.84, + solar_transmittance: 0.7, + reduction_factor_Z_for_temporary: 1 + }, + { + type: "wall", + orientation: "W", + area: 42.0, + u_value: 0.35, + absorptivity: 0.7, + emissivity: 0.7 + }, + { + type: "window", + orientation: "W", + area: 18.0, + u_value: 3.09, + emissivity: 0.84, + solar_transmittance: 0.7, + reduction_factor_Z_for_temporary: 1 + }, + { + type: "roof", + area: 112.0, + u_value: 0.25, + absorptivity: 0.85, + emissivity: 0.85 + } +]; + + +// region is Atlanta +// should be an array with each element corresponding to a month of year (0=jan, 11=dec) +// each entry is an object with the relevant data for the given month +export let climate = [ + { + mon: "jan", + temp: 4, + wind_speed: 4.6, + avg_solar_S: 166, + avg_solar_SE: 119, + avg_solar_E: 64, + avg_solar_NE: 33, + avg_solar_N: 30, + avg_solar_NW: 38, + avg_solar_W: 85, + avg_solar_SW: 144, + avg_solar_HOR: 116, + solar_altitude: 22.4, + dry_air_humidity: 3.60, + atmospheric_pressure: 98645, + relative_humidity: 65, + dew_point: -3.033512786, + sky_cover: 0.616689098, + sky_temp: 260.3223678, + solar_transmittance: 0.92 + }, + { + mon: "feb", + temp: 8, + wind_speed: 5.2, + avg_solar_S: 162, + avg_solar_SE: 127, + avg_solar_E: 78, + avg_solar_NE: 39, + avg_solar_N: 33, + avg_solar_NW: 46, + avg_solar_W: 94, + avg_solar_SW: 143, + avg_solar_HOR: 139, + solar_altitude: 26.3, + dry_air_humidity: 4.28, + atmospheric_pressure: 98371, + relative_humidity: 58, + dew_point: -1.325595238, + sky_cover: 0.580505952, + sky_temp: 264.706049, + solar_transmittance: 0.92 + }, + { + mon: "mar", + temp: 14, + wind_speed: 4.9, + avg_solar_S: 162, + avg_solar_SE: 137, + avg_solar_E: 103, + avg_solar_NE: 63, + avg_solar_N: 49, + avg_solar_NW: 79, + avg_solar_W: 136, + avg_solar_SW: 167, + avg_solar_HOR: 194, + solar_altitude: 32.9, + dry_air_humidity: 5.72, + atmospheric_pressure: 98195, + relative_humidity: 55, + dew_point: 3.800672043, + sky_cover: 0.571639785, + sky_temp: 271.9444134, + solar_transmittance: 0.88 + }, + { + mon: "apr", + temp: 17, + wind_speed: 4.0, + avg_solar_S: 138, + avg_solar_SE: 141, + avg_solar_E: 127, + avg_solar_NE: 89, + avg_solar_N: 64, + avg_solar_NW: 109, + avg_solar_W: 156, + avg_solar_SW: 160, + avg_solar_HOR: 239, + solar_altitude: 37.9, + dry_air_humidity: 7.74, + atmospheric_pressure: 98254, + relative_humidity: 62, + dew_point: 8.81, + sky_cover: 0.448472222, + sky_temp: 276.7888189, + solar_transmittance: 0.82 + }, + { + mon: "may", + temp: 21, + wind_speed: 3.5, + avg_solar_S: 117, + avg_solar_SE: 139, + avg_solar_E: 144, + avg_solar_NE: 114, + avg_solar_N: 85, + avg_solar_NW: 130, + avg_solar_W: 162, + avg_solar_SW: 148, + avg_solar_HOR: 263, + solar_altitude: 40.4, + dry_air_humidity: 10.48, + atmospheric_pressure: 98033, + relative_humidity: 67, + dew_point: 13.69435484, + sky_cover: 0.593682796, + sky_temp: 281.8729926, + solar_transmittance: 0.75 + }, + { + mon: "jun", + temp: 25, + wind_speed: 3.9, + avg_solar_S: 107, + avg_solar_SE: 135, + avg_solar_E: 150, + avg_solar_NE: 125, + avg_solar_N: 98, + avg_solar_NW: 144, + avg_solar_W: 170, + avg_solar_SW: 144, + avg_solar_HOR: 280, + solar_altitude: 42.8, + dry_air_humidity: 12.61, + atmospheric_pressure: 97934, + relative_humidity: 63, + dew_point: 16.44902778, + sky_cover: 0.483333333, + sky_temp: 286.199821, + solar_transmittance: 0.69 + }, + { + mon: "jul", + temp: 26, + wind_speed: 3.2, + avg_solar_S: 108, + avg_solar_SE: 134, + avg_solar_E: 145, + avg_solar_NE: 119, + avg_solar_N: 91, + avg_solar_NW: 134, + avg_solar_W: 161, + avg_solar_SW: 142, + avg_solar_HOR: 263, + solar_altitude: 41.7, + dry_air_humidity: 15.08, + atmospheric_pressure: 98213, + relative_humidity: 71, + dew_point: 19.84758065, + sky_cover: 0.540188172, + sky_temp: 288.5518314, + solar_transmittance: 0.72 + }, + { + mon: "aug", + temp: 27, + wind_speed: 3.4, + avg_solar_S: 129, + avg_solar_SE: 139, + avg_solar_E: 134, + avg_solar_NE: 101, + avg_solar_N: 76, + avg_solar_NW: 118, + avg_solar_W: 157, + avg_solar_SW: 154, + avg_solar_HOR: 242, + solar_altitude: 39.1, + dry_air_humidity: 16.01, + atmospheric_pressure: 98008, + relative_humidity: 73, + dew_point: 20.72446237, + sky_cover: 0.549731183, + sky_temp: 289.3616457, + solar_transmittance: 0.79 + }, + { + mon: "sep", + temp: 23, + wind_speed: 3.1, + avg_solar_S: 135, + avg_solar_SE: 127, + avg_solar_E: 105, + avg_solar_NE: 69, + avg_solar_N: 51, + avg_solar_NW: 81, + avg_solar_W: 124, + avg_solar_SW: 141, + avg_solar_HOR: 182, + solar_altitude: 35.2, + dry_air_humidity: 12.70, + atmospheric_pressure: 97812, + relative_humidity: 72, + dew_point: 16.82388889, + sky_cover: 0.466944444, + sky_temp: 284.1824533, + solar_transmittance: 0.86 + }, + { + mon: "oct", + temp: 16, + wind_speed: 4.0, + avg_solar_S: 164, + avg_solar_SE: 127, + avg_solar_E: 83, + avg_solar_NE: 43, + avg_solar_N: 35, + avg_solar_NW: 54, + avg_solar_W: 109, + avg_solar_SW: 153, + avg_solar_HOR: 160, + solar_altitude: 28.2, + dry_air_humidity: 8.80, + atmospheric_pressure: 98087, + relative_humidity: 74, + dew_point: 10.77755376, + sky_cover: 0.484677419, + sky_temp: 276.1970761, + solar_transmittance: 0.91 + }, + { + mon: "nov", + temp: 12, + wind_speed: 4.2, + avg_solar_S: 166, + avg_solar_SE: 121, + avg_solar_E: 69, + avg_solar_NE: 35, + avg_solar_N: 32, + avg_solar_NW: 40, + avg_solar_W: 89, + avg_solar_SW: 144, + avg_solar_HOR: 123, + solar_altitude: 24.1, + dry_air_humidity: 5.86, + atmospheric_pressure: 98102, + relative_humidity: 63, + dew_point: 4.173472222, + sky_cover: 0.519166667, + sky_temp: 270.3190692, + solar_transmittance: 0.93 + }, + { + mon: "dec", + temp: 8, + wind_speed: 4.7, + avg_solar_S: 161, + avg_solar_SE: 116, + avg_solar_E: 60, + avg_solar_NE: 29, + avg_solar_N: 28, + avg_solar_NW: 33, + avg_solar_W: 76, + avg_solar_SW: 135, + avg_solar_HOR: 105, + solar_altitude: 20.8, + dry_air_humidity: 4.53, + atmospheric_pressure: 98424, + relative_humidity: 64, + dew_point: 0.261342282, + sky_cover: 0.555704698, + sky_temp: 265.0909842, + solar_transmittance: 0.94 + } +]; diff --git a/test/util/building.spec.js b/test/util/building.spec.js new file mode 100644 index 0000000..cb87fbd --- /dev/null +++ b/test/util/building.spec.js @@ -0,0 +1,11 @@ +import {internalHeatCapacityByType} from "util/building"; + + +describe("internalHeatCapacityByType", function() { + it("can look up internal heat capacity", function() { + expect(internalHeatCapacityByType("light")).toEqual(110000); + expect(internalHeatCapacityByType("vheavy")).toEqual(370000); + // default case + expect(internalHeatCapacityByType("foobar")).toEqual(165000); + }); +}); diff --git a/test/util/math.spec.js b/test/util/math.spec.js new file mode 100644 index 0000000..75a7bb2 --- /dev/null +++ b/test/util/math.spec.js @@ -0,0 +1,18 @@ +import {MathHelper} from "util/math"; + + +describe("MathHelper", function() { + it("can round a single number", function() { + expect(MathHelper.round(12.1248234, 0)).toEqual(12); + expect(MathHelper.round(12.1248234, 1)).toEqual(12.1); + expect(MathHelper.round(12.1248234, 2)).toEqual(12.12); + expect(MathHelper.round(12.1248234, 3)).toEqual(12.125); + }); + + it("can round all values of a map", function() { + expect(MathHelper.roundValues({a: 12.1248234, b: 44.2932}, 0)).toEqual({a: 12, b: 44}); + expect(MathHelper.roundValues({a: 12.1248234, b: 44.2932}, 1)).toEqual({a: 12.1, b: 44.3}); + expect(MathHelper.roundValues({a: 12.1248234, b: 44.2932}, 2)).toEqual({a: 12.12, b: 44.29}); + expect(MathHelper.roundValues({a: 12.1248234, b: 44.2932}, 3)).toEqual({a: 12.125, b: 44.293}); + }); +}); diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..31f38f9 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,35 @@ +var webpack = require('webpack'); +var path = require('path'); + +var SRC_DIR = path.resolve(__dirname, 'src'); +var BUILD_DIR = path.resolve(__dirname, 'dist'); + +var config = { + entry: { + model: SRC_DIR + "/model/model.js" + }, + output: { + path: BUILD_DIR, + filename: '[name].bundle.js', + libraryTarget: 'amd' + }, + module: { + loaders: [ + { + test : /\.js$/, + include : SRC_DIR, + loader : 'babel' + } + ] + }, + resolve: { + extensions: ["", ".webpack.js", ".js"], + alias: { + "model": SRC_DIR + "/model", + "util": SRC_DIR + "/util", + "underscore": __dirname + "/node_modules/underscore/underscore-min.js", + } + } +}; + +module.exports = config;